结论:说明 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 应用。
package org.springframework.boot.loader;
import org.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
*/
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
类关系图
从图中可以看出来,最顶层的启动器是 Launcher,其次是 ExecutableArchiveLauncher。最终的实现分别两种模式,一个是 JarLauncher(针对于 jar 归档文件),另一个是 WarLauncher(针对于 war 归档文件)。从这里也就说明了为什么 Spring Boot 的应用可以通过 jar 和 war 两种方式运行。
源码分析
通过代码可以发现,实际程序的入口是在:
new JarLauncher().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
* @throws Exception if the application fails to launch
*/
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
定位当前执行的具体 jar 文件或者文件目录,通过磁盘上的绝对路径来定位,返回一个 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");
}
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));
}
getNestedArchives
返回与指定过滤器(EntryFilter)所匹配的嵌套归档文件。
@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
List<Archive> nestedArchives = new ArrayList<>();
for (Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
}
isNestedArchive
EntryFilter 过滤器,判断 entry 所指定的文件(具体 jar 文件或者文件目录)是否满足条件 ,满足条件的应该添加到 classpath 里,每个指定的文件都会调用一次。
条件:在 BOOT-INF/classes/ 目录下的工程文件或者在 BOOT-INF/lib/ 目录下的第三方 jar 包。
注意:其实这里就是在判断,要执行的 jar 文件是否是按照 Spring Boot 特有的目录结构来放置工程文件,以及所依赖的第三方 jar 包。只有满足条件的工程文件或所依赖的第三方 jar 包才会进入下一步,也就是通过自定义类加载器来加载这些满足条件的文件,这里就需要 jar 文件规范相关知识。
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
postProcessClassPathArchives
是一个空方法,事后处理方法,回调方法。
/**
* Called to post-process archive entries before they are used. Implementations can
* add and remove entries.
* @param archives the archives
* @throws Exception if the post processing fails
*/
protected void postProcessClassPathArchives(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
* @throws Exception if the classloader cannot be created
*/
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return createClassLoader(urls.toArray(new URL[0]));
}
LaunchedURLClassLoader
Spring Boot 提供的自定义类加载器,urls 表示所有需要加载文件的 url(jar 文件的绝对路径),getClass().getClassLoader() 表示父加载器(也就是应用类加载器)。
/**
* Create a classloader for the specified URLs.
* @param urls the URLs
* @return the classloader
* @throws Exception if the classloader cannot be created
*/
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
@Override
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException(
"No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
launch
通过给定的归档文件和全新的 classloader 来启动应用。
/**
* 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 {
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
*/
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
ClassLoader classLoader) {
return new MainMethodRunner(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
*/
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 {
Class<?> mainClass = Thread.currentThread().getContextClassLoader()
.loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
}
run
当我们使用下面这个指令的时候,就会启动 Spring Boot 的应用,原理就在这个 run 方法。
@SpringBootApplication
public class MicroservicesApplication {
public static void main(String[] args) {
System.out.println(MicroservicesApplication.class.getClassLoader());
SpringApplication.run(MicroservicesApplication.class, args);
}
}
jar
通过指令运行。从结果可以看出来是采用的 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 到内容中,再通过自定义类加载器加载自己的应用。