使用maven或gradle插件将Spring Boot工程打包之后,生成的jar是可以直接运行的,这个jar被称为FatJar。我们探究一下,这个jar是如何来工作的。
Spring Boot FatJar的构成
jar本身是一个zip文件,随便找一个SpringBoot工程的jar解压,会看到包含三个文件夹:BOOT-INF、META-INF、org。

BOOT-INF文件夹包含了应用本身的代码以及依赖的第三方jar包,META-INF包含重要的清单文件MANIFEST.MF,org是springBoot的启动器。
我们先打开MANIFEST.MF文件,会发现该jar的Main-Class是org.springframework.boot.loader.JarLauncher,并不是我们自己写的类,但Start-Class的值是咱们自己开发的包含SpringBootApplication注解的入口类。下面我们来阅读一下JarLauncher的源码。
JarLauncher类分析
JarLauncher默认是不再classpath下的,要想用IDE看到源码需加入依赖spring-boot-loader。之后就可以打开其源码了。看源码之前,咱们先看一下它的JavaDoc:
Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a BOOT-INF/lib directory and that application classes are included inside a BOOT-INF/classes directory.
里面描述的很清楚,这个启动器假定第三方依赖在/BOOT-INF/lib下,应用的类位于/BOOT-INF/classes下。这就是为什么打包之后是以上所说的目录结构。
JarLauncher的继承关系如下:

我们看其main方法,该方法new一个JarLauncher,调用父类的lanuch方法。在其父类ExecutableArchiveLauncher构造方法中会创建archive字段,其代表当前的FatJar,并实现了迭代器,可遍历其所有文件。我们进入Launcher的launch方法,这个函数有三行代码,其中第二行创建了一个自定义的LaunchedURLClassLoader,其加载类的路径就是/BOOT-INF/lib与/BOOT-INF/classes。
1protected void launch(String[] args) throws Exception {
2 JarFile.registerUrlProtocolHandler();
3 ClassLoader classLoader = createClassLoader(getClassPathArchives());
4 launch(args, getMainClass(), classLoader);
5}
SpringBoot为何要自己定义一个ClassLoader呢?这是由于默认情况下,系统类加载器AppClassLoader是不会加载jar包当中的jar文件以及嵌套目录中的类的,也就是说BOOT-INF目录下的类都无法加载。但Spring通过自定义ClassLoader的方式解决了该问题。
第三行代码中getMainClass()方式是从jar包MANIFEST.MF文件中获取Start-Class的值,也就是应用代码入口。
1@Override
2protected String getMainClass() throws Exception {
3 Manifest manifest = this.archive.getManifest();
4 String mainClass = null;
5 if (manifest != null) {
6 mainClass = manifest.getMainAttributes().getValue("Start-Class");
7 }
8 if (mainClass == null) {
9 throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
10 }
11 return mainClass;
12}
接着进入重载的launch方法,这里有一行特别重要的代码Thread.currentThread().setContextClassLoader(classLoader)。
1 protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
2 Thread.currentThread().setContextClassLoader(classLoader);
3 createMainMethodRunner(mainClass, args, classLoader).run();
4 }
将当前线程的上下文类加载器换成自定义的ClassLoader,这样当前线程及其子线程加载新的类都是使用SpringBoot自定义的类加载器。随后定义了MainMethodRunner对象来运行应用入口代码。
可以看到,它是先用上下文ClassLoader加载应用入口类,通过反射的方式调用main方法。
1public class MainMethodRunner {
2
3 private final String mainClassName;
4
5 private final String[] args;
6
7 public void run() throws Exception {
8 Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
9 Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
10 mainMethod.invoke(null, new Object[] { this.args });
11 }
12
13}
总结
•FatJar中分成三个目录,分别存放应用程序class与依赖、jar配置清单、SpringBoot自定义的启动器;•SpringBoot通过自定义类加载器的方式非常巧妙地实现了jar in jar的class加载;•线程加载的类都使用上下文加载器,默认是AppClassLoader,如需加载特殊目录或实现类的隔离可自定义加载器;




