SpringBoot(二) 启动分析JarLauncher | BladeCode
我们在开发过程中,使用 java -jar you-jar-name.jar 命令来启动应用,它是如何启动?以及它如何去寻找 .class
文件并执行这些文件?本节就带着这两个问题,让我们一层层解开 SpringBoot 项目的 jar 启动过程,废话不多说,跟着我的脚步一起去探索 spring-boot-load 的秘密。
在 SpringBoot(一)初识 已经解释了为什么在编译后的 jar 中根目录存在 org/springframework/boot/loader 内容,以及为了方便学习研究,我们需要在项目的依赖中导入 org.springframework.boot:spring-boot-loader 依赖。同时我们在解压的 you-jar-name.jar 文件中,查看对应的清单文件 MANIFEST.MF 内容,其中明确指出了应用的入口 org.springframework.boot.loader.JarLauncher 因此我们就从 JarLauncher 开始一步步深入
spring-boot-loader-jarlauncher
先用Diagrams来表述 JarLauncher 类之间的结构及方法等相关信息
jarlauncher
从Diagrams可知
关于图上图标含义,这里就不再赘述,烦请移步 IntelliJ IDEA Icon reference
对于 Java 标准的 jar 文件来说,规定在一个 jar 文件中,我们必须要将指定 main.class 的类直接放置在文件的顶层目录中(也就是说,它不予许被嵌套),否则将无法加载,对于 BOOT-INF/class/ 路径下的 class
因为不在顶层目录,因此也是无法直接进行加载, 而对于 BOOT-INF/lib/ 路径的 jar 属于嵌套的(Fatjar),也是不能直接加载,因此 Spring 要想启动加载,就需要自定义实现自己的类加载器去加载。
关于 jar 官方标准说明请移步
- JAR File Specification
- JAR (file format)
main 方法
根据清单文件 MANIFEST.MF 中 Main-Class 的描述,我们知道入口类就是 JarLauncher;先看下这个类的 javadoc 介绍
1 2 3 4 5 6 7 8 9 10 11 |
/** * {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are * included inside a {@code /BOOT-INF/lib} directory and that application classes are * included inside a {@code /BOOT-INF/classes} directory. * * 用于基于JAR的归档。这个启动程序假设依赖jar包含在{@code /BOOT-INF/lib}目录中, * 应用程序类包含在{@code /BOOT-INF/classes}目录中 * * @author Phillip Webb * @author Andy Wilkinson */ |
紧接着,要进行源码分析,那肯定是找到入口,一步步深入,那么对于 JarLauncher 就是它的 main 方法了
1 2 3 4 |
public static void main(String[] args) throws Exception { // launch 方法是调用父类 Launcher 的 launch 方法 new JarLauncher().launch(args); } |
那我们去看一看 Launcher 的 launch 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** * Launch the application. This method is the initial entry point that should be * called by a subclass {@code public static void main(String[] args)} method. * * 启动一个应用,这个方法应该被初始的入口点,这个入口点应该是一个Launcher的子类的 * public static void main(String[] args)这样的方法调用 * * @param args the incoming arguments * @throws Exception if the application fails to launch */ protected void launch(String[] args) throws Exception { // 1. 注册一些 URL的属性 JarFile.registerUrlProtocolHandler(); // 2. 创建类加载器(LaunchedURLClassLoader),加载得到集合要么是BOOT-INF/classes/ // 或者BOOT-INF/lib/的目录或者是他们下边的class文件或者jar依赖文件 ClassLoader classLoader = createClassLoader(getClassPathArchives()); // 3. 启动给定归档文件和完全配置的类加载器的应用程序 launch(args, getMainClass(), classLoader); } |
getClassPathArchives 方法
launch 方法的第一步的相关内容比较简单,这里不做过多说明,主要后面两步,我们先看第二步,创建一个类加载器(ClassLoader),其中 getClassPathArchives() 方法是一个抽象方法,具体的实现有(ExecutableArchiveLauncher 和 PropertiesLauncher ,因为我们研究的 JarLauncher 是继承 ExecutableArchiveLauncher ,因此我们这里看 ExecutableArchiveLauncher 类中 getClassPathArchives() 方法的实现)我们要看看这个方法中它做了什么
1 2 3 4 5 6 7 8 9 10 11 |
@Override protected List |
this.archive 位于当前类 ExecutableArchiveLauncher 的构造方法中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public ExecutableArchiveLauncher() { try { // 调用 createArchive() 方法得到Archive this.archive = createArchive(); } catch (Exception ex) { throw new IllegalStateException(ex); } } / // 紧接着我们查看 createArchive() 方法都做了什么 // / // Launcher.class 中的 createArchive()方法 // 得到我们运行文件的Archive相关的信息 protected final Archive createArchive() throws Exception { ProtectionDomain protectionDomain = getClass().getProtectionDomain(); CodeSource codeSource = protectionDomain.getCodeSource(); URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null; String path = (location != null) ? location.getSchemeSpecificPart() : null; if (path == null) { throw new IllegalStateException("Unable to determine code source archive"); } // 返回我们要执行的jar文件的绝对路径(java -jar xxx.jar中 xxx.jar的绝对路径) File root = new File(path); if (!root.exists()) { throw new IllegalStateException("Unable to determine code source archive from " + root); } return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); } |
对于 getNestedArachives() 方法,它是 Archive 的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/** * Returns nested {@link Archive}s for entries that match the specified filter. * * 返回与过滤器相匹配的嵌套归档文件 * * @param filter the filter used to limit entries * @return nested archives * @throws IOException if nested archives cannot be read */ List |
而 this::isNestedArchive 方法引用,我们查看 isNestedArchive
抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/** * Determine if the specified {@link JarEntry} is a nested item that should be added * to the classpath. The method is called once for each entry. * * 确定指定的{@link JarEntry}是否是应该添加到类路径的嵌套项。对每个条目调用该方法一次 * * @param entry the jar entry * @return {@code true} if the entry is a nested item (jar or folder) */ protected abstract boolean isNestedArchive(Archive.Entry entry); / // 紧接着我们查看 isNestedArchive() 实现 // / // JarLauncher.class 中的 isNestedArchive()方法 @Override protected boolean isNestedArchive(Archive.Entry entry) { // 如果是目录判断是不是BOOT-INF/classes/目录 if (entry.isDirectory()) { return entry.getName().equals(BOOT_INF_CLASSES); } // 如果是文件判断文件的前缀是不是BOOT-INF/lib/开头 return entry.getName().startsWith(BOOT_INF_LIB); } |
createClassLoader 方法
把符合条件的 Archives 作为参数传入到 createClassLoader() 方法,创建一个类加载器,我们跟进去,查看 createClassLoader() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
/** * Create a classloader for the specified archives. * * 创建一个所指定归档文件的类加载器 * * @param archives the archives * @return the classloader * @throws Exception if the classloader cannot be created */ protected ClassLoader createClassLoader(List |
super() 方法是调用父类的方法,这样一层层跟进去,最终到了 JDK 的 ClassLoader
类,它也是所有类加载器的顶类
launch 方法
launch 方法的第二个参数,getMainClass() 是一个抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/** * Returns the main class that should be launched. * @return the name of the main class * @throws Exception if the main class cannot be obtained */ protected abstract String getMainClass() throws Exception; / // 紧接着我们查看 getMainClass() 实现 // / @Override protected String getMainClass() throws Exception { Manifest manifest = this.archive.getManifest(); String mainClass = null; if (manifest != null) { // 获取到 Manifest 文件中属性为`Start-Class`对应的值,也就是当前项目工程启动的类的完整路径 mainClass = manifest.getMainAttributes().getValue("Start-Class"); } if (mainClass == null) { throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this); } return mainClass; } |
接着我们看 launch 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/** * Launch the application given the archive file and a fully configured classloader. * * 加载指定存档文件和完全配置的类加载器的应用程序 * * @param args the incoming arguments * @param mainClass the main class to run * @param classLoader the classloader * @throws Exception if the launch fails */ protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { // 将应用的加载器换成了自定义的 LaunchedURLClassLoader 加载器,然后入到线程类加载器中 // 最终在未来的某个地方,通过线程的上下文中取出类加载进行加载 Thread.currentThread().setContextClassLoader(classLoader); // 创建一个主方法运行器运行 createMainMethodRunner(mainClass, args, classLoader).run(); } /** * Create the {@code MainMethodRunner} used to launch the application. * * 创建一个 MainMethodRunner 用于启动这个应用 * * @param mainClass the main class * @param args the incoming arguments * @param classLoader the classloader * @return the main method runner */ protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) { return new MainMethodRunner(mainClass, args); } |
返回一个 MainMethodRunner
对象,我们紧接着去看看这个对象,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/** * Utility class that is used by {@link Launcher}s to call a main method. The class * containing the main method is loaded using the thread context class loader. * * 被 Launcher 使用来调用 main 方法的辅助类,使用线程类加载来加载包含 main 方法的类 * * @author Phillip Webb * @author Andy Wilkinson */ public class MainMethodRunner { private final String mainClassName; private final String[] args; /** * Create a new {@link MainMethodRunner} instance. * @param mainClass the main class * @param args incoming arguments */ public MainMethodRunner(String mainClass, String[] args) { this.mainClassName = mainClass; this.args = (args != null) ? args.clone() : null; } // 关键方法 public void run() throws Exception { // 获取到当前线程上下文的类加载器,实际就是 springboot 自定义的加载器(LaunchedURLClassLoader) // 加载 this.mainClassName所对应的类,实际也就是清单文件中对应 Start-Class 属性的类 Class> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName); // 通过反射获取到 main 方法和参数 Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); // 调用目标方法运行 // invoke 方法参数一:是被调用方法所在对象,这里为 null,原因是我们所调用的目标方法是一个静态方法 // invoke 方法参数二:被调用方法所接收的参数 mainMethod.invoke(null, new Object[] { this.args }); } } |
到此为止,invoke 方法成功调用,那么我们项目中的main 方法就执行了,这时我们的所编写的 springboot 应用就正式的启动了。那么关于 springboot 的 loader 加载过程已经分析完
summary-jarlauncher
从 jar 规范的角度出发,我们深入分析了 springboot 项目启动的整个过程,这个过程到底对不对,我们口说无凭,需要实际检验我们分析
首先,我们先思考,项目的应用启动入口是不是必须是 main.class 方法,以及为什么要默认这么做?
其次,我们再思考,在编辑器中通过图标运行启动程序(或者是通过命令启动程序),比较将程序编译成 jar 包,然后通过命令启动程序他们之间是否相同,如果不同请解释为什么?
项目的应用启动入口可以不是 main.class 方法,只是为什么会默认为 main.class 方法,原因是在 springboot 的 MainMethodRunner类的 run 方法中,是固定写死的 main ,为什么要这么写,答案是,我们可以在编辑器中已右键或其他图标启动的方式快速启动 springboot 项目(就像是在运行一个 Java 的 main 方法一样,不再向之前需要乱七八糟各种的配置)。
答案是不相同,我们可以在项目的应用启动 main.class 方法中,打印出加载类 System.out.println(项目启动加载类 + SpringbootStartApplication.class.getClassLoader()); ,这样就可以检验我们的分析是否正确。分别使用两种不同的方式
gradle bootRun
)或者使用 IDEA 上的运行应用运行按钮,结果如下
1 |
项目启动加载类sun.misc.Launcher$AppClassLoader@18b4aac2 |
1 |
项目启动加载类org.springframework.boot.loader.LaunchedURLClassLoader@439f5b3d |
通过打印出来的信息,可以验证我们的分析,方式一的运行,实际上是应用类加载器启动,而方式二是 spring-boot-load 包中自定义的 LaunchedURLClassLoader
来启动项目
在实际的生产开发中,有时我们的分析需要进行验证(或者找问题),而此时服务又部署在生成环境或者非本机上,通常用的方式是看应用的日志输出,在日志中去定位问题,而有时我们需要断点的方式去找问题,那该如何去操作呢?对于这个问题,在实际开发中是有方法去处理,请看下篇《SpringBoot(三) JDWP远程调用》