结论:说明 Spring Boot Gradle Plugin 在用 bootJar 任务对 Spring Boot 工程进行打包的时候,是把 spring-boot-loader.jar 这个 jar 解压,随后把解压内容放置在 Spring Boot 的应用 jar 里。而 spring-boot-loader.jar 这个依赖,肯定是在 Spring Boot Gradle Plugin 里被依赖的。
疑问:为什么不直接把这个依赖传递给实际的 Spring Boot 应用,而是采用这么麻烦的方式,把这个依赖的内容打包进实际的 Spring Boot 应用。
packageorg.springframework.boot.loader;importorg.springframework.boot.loader.archive.Archive;/** * {@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. * * @author Phillip Webb * @author Andy Wilkinson */publicclassJarLauncherextendsExecutableArchiveLauncher {staticfinalString BOOT_INF_CLASSES ="BOOT-INF/classes/";staticfinalString BOOT_INF_LIB ="BOOT-INF/lib/";publicJarLauncher() { }protectedJarLauncher(Archive archive) { super(archive); } @OverrideprotectedbooleanisNestedArchive(Archive.Entry entry) {if (entry.isDirectory()) {returnentry.getName().equals(BOOT_INF_CLASSES); }returnentry.getName().startsWith(BOOT_INF_LIB); }publicstaticvoidmain(String[] args) throwsException {newJarLauncher().launch(args); }}
类关系图
从图中可以看出来,最顶层的启动器是 Launcher,其次是 ExecutableArchiveLauncher。最终的实现分别两种模式,一个是 JarLauncher(针对于 jar 归档文件),另一个是 WarLauncher(针对于 war 归档文件)。从这里也就说明了为什么 Spring Boot 的应用可以通过 jar 和 war 两种方式运行。
源码分析
通过代码可以发现,实际程序的入口是在:
newJarLauncher().launch(args);
实际调用的 launch 方法是 Launcher.java 里的 launch 方法。
/** * 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. * @param args the incoming arguments * @throwsException if the application fails to launch */protectedvoidlaunch(String[] args) throws Exception {JarFile.registerUrlProtocolHandler();ClassLoader classLoader =createClassLoader(getClassPathArchives());launch(args, getMainClass(), classLoader);}
/** * Called to post-process archive entries before they are used. Implementations can * add and remove entries. * @param archives the archives * @throwsException if the post processing fails */protectedvoidpostProcessClassPathArchives(List<Archive> archives) throws Exception {}
createClassLoader
上面的所有方法,都是为了准备 List 对象,所有符合条件的 jar(BOOT-INF/lib/ )和工程文件(BOOT-INF/classes/ ),并包装成一个类型为 Archive 的 List 对象。
***Create a classloader for the specified archives.* @param archives the archives* @return the classloader* @throwsExceptionif the classloader cannot be created*/protectedClassLoadercreateClassLoader(List<Archive> archives) throws Exception {List<URL> urls =newArrayList<>(archives.size());for (Archive archive : archives) {urls.add(archive.getUrl()); }returncreateClassLoader(urls.toArray(newURL[0]));}
LaunchedURLClassLoader
Spring Boot 提供的自定义类加载器,urls 表示所有需要加载文件的 url(jar 文件的绝对路径),getClass().getClassLoader() 表示父加载器(也就是应用类加载器)。
/** * Create a classloader for the specified URLs. * @param urls the URLs * @return the classloader * @throwsException if the classloader cannot be created */protectedClassLoadercreateClassLoader(URL[] urls) throws Exception {returnnewLaunchedURLClassLoader(urls, getClass().getClassLoader());}
/** * 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 * @throwsException if the launch fails */protectedvoidlaunch(String[] args,String mainClass,ClassLoader classLoader) throws Exception {Thread.currentThread().setContextClassLoader(classLoader);createMainMethodRunner(mainClass, args, classLoader).run();}
第一行代码把自定义的 classloader 设置到当前线程上下文类加载器,在默认情况下,当前线程上下文类加载器就是 AppClassLoader。通过这种方式就把当前线程上下文的默认类加载器换成了 Spring Boot 自定义的类加载器。
转换:AppClassLoader --> LaunchedURLClassLoader
现在是把类加载器放置进去,在未来某处肯定会从当前线程中取出这个上下文类加载器,然后进行类加载。
createMainMethodRunner
创建 MainMethodRunner,用于启动和加载应用。
其实这里的 classLoader 并没有用到。
/** * Create the {@code MainMethodRunner} used to launch the application. * @param mainClass the main class * @param args the incoming arguments * @param classLoader the classloader * @return the main method runner */protectedMainMethodRunnercreateMainMethodRunner(String mainClass,String[] args,ClassLoader classLoader) {returnnewMainMethodRunner(mainClass, args);}
MainMethodRunner
使用当前线程上下文类加载器,加载一个包含了 main 方法的主类,然后调用这个主类的 main 方法。主要看这个 run 方法。
/** * 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. * * @author Phillip Webb * @author Andy Wilkinson */publicclassMainMethodRunner {privatefinalString mainClassName;privatefinalString[] args; /** * Create a new {@link MainMethodRunner} instance. * @param mainClass the main class * @param args incoming arguments */publicMainMethodRunner(String mainClass,String[] args) {this.mainClassName= mainClass;this.args= (args !=null) ?args.clone() :null; }publicvoidrun() throwsException {Class<?> mainClass =Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);Method mainMethod =mainClass.getDeclaredMethod("main",String[].class);mainMethod.invoke(null,newObject[] { this.args }); }}
run
当我们使用下面这个指令的时候,就会启动 Spring Boot 的应用,原理就在这个 run 方法。
通过指令运行。从结果可以看出来是采用的 LaunchedURLClassLoader,也就是采用 Spring Boot 提供的全新类加载器。
java -jar microservices-0.0.1-SNAPSHOT.jar
main
直接在开发阶段的 ide(eclipse、idea)里右键 run 方式运行。从结果可以看出来是采用的 AppClassLoader,也就是采用应用类加载器。
总结
打包机制
Spring Boot 打包机制,通过 gradle plugin 的 org.springframework.boot 插件对应用打成 jar 包,在 jar 包的根目录放置 spring-boot-loader.jar 这个 jar 包解压缩后的所有文件。自定义 BOOT-INF 和 META-INF 两个目录,BOOT-INF/classes 放置工程文件,BOOT-INF/lib 放置工程三方依赖,MANIFEST.MF 文件中定义 Start-Class 和 Main-Class 属性。
运行机制
Spring Boot 运行机制,首先应用加载器(系统加载器)加载 org.springframework.boot.loader.JarLauncher。在加载 JarLauncher 的同时,创建一个 Spring Boot 特有的类加载器 LaunchedURLClassLoader,用这个特有的类加载器加载 BOOT-INF 下的工程文件和三方依赖。最后通过反射调用 Start-Class 应用入口类的 main 方法启动应用程序。
上面所讲的是 jar 包形式运行,开发阶段直接在 ide 里右键 run 运行工程,则是直接调用系统的 AppClassLoader 类加载器。
优雅方式
Spring Boot 通过自定义类加载器这种方式,优雅的解决了 jar 文件规范问题。至于把 spring-boot-loader.jar 这个 jar 包里的文件原封不动的拷贝过来,是为了给应用类加载器(系统加载器)一个程序入口。先加载 JarLauncher 到内容中,再通过自定义类加载器加载自己的应用。