Spring Boot Jar包启动流程
启动流程概览
-
首先,我们通过java -jar命令启动jar包,此时调用Launcher$AppClassLoader类加载器加载,此时涉及了MANIFEST.MF中Main-Class对应的属性
-
然后构建Spring Boot加载器,通过调用LaunchedURLClassLoader类加载器加载jar包中/BOOT-INF/classes/和/BOOT-INF/lib/*.jar文件
-
基于反射调用应用程序的启动方法,此时涉及了MANIFEST.MF中的Start-Class对应的属性
Jar包结构
首先我们来看下jar包的结构:
- 其中BOOT-INF就是我们自己写的应用的Class,包括:
- classes:我们在spring boot项目中实现的业务对象
- lib:存放的就是spring boot项目中的maven依赖,就是一个个的jar包
- META-INF:
- maven:包含了基本的maven配置文件
- MANIFEST.MF:在spring boot的jar包中,这个文件是不能缺少的,否则就无法正常启动
- org.springframework.boot.loader是来自于spring-boot-loader.jar包的文件,这里为什么不把这个jar包放到lib目录下,而是将它的文件拷贝出来单独放在一个文件夹下呢?这个留待后面解答
MANIFEST.MF文件
首先我们看看文件的内容:
该文件中最重要的就是Main-Class和Start-Class这两个属性对应的启动类。
Start-Class是我们真正要启动的类,该文件存储在BOOT-INF/classes目录下。
从这里我们可以解答刚开始的那个问题:为什么将spring-boot-loader.jar文件的内容单独拷贝出来;这是因为我们在MANIFEST.MF中配置了Main-Class这个属性,那么当jar包启动的时候,就必须能够找到这个类,如果是在lib下的话,则启动时无法加载到这个类,所以这里将这些文件直接拷贝出来丢到jar包里面,让JarLuncher在启动jar包时可以直接启动。
我们来看看这个JarLauncher在哪里:
Jar中不同类对应的类加载器
spring boot加载器能直接加载BOOT-INF/*下的jar包和类吗?
jdk自己实现的协议无法加载jar包中的jar包,我们通过如下程序测试下:
@Test
public void test() throws IOException, ClassNotFoundException {
// 这里的地址替换为自己本地jar包的位置运行即可,spring-boot的jar包基本都会包含这个spring-beans的jar包
URL nested_url = new URL("jar:file:///D:/spring_project/myblog/target/demo-0.0.1-SNAPSHOT.jar!/BOOT-INF/lib/spring-beans-5.2.7.RELEASE.jar");
// 这里没有加载嵌套的jar包,只加载了我们自己打包的文件
URL url = new URL("file:///D:/spring_project/myblog/target/demo-0.0.1-SNAPSHOT.jar");
URLClassLoader loader = new URLClassLoader(new URL[]{nested_url, url}, null);
Class<?> aClass = loader.loadClass("org.springframework.boot.loader.JarLauncher");
System.out.println(aClass);
// 由于无法加载jar中jar所以这里会报错!具体情况请看下图
loader.loadClass("org.springframework.beans.BeanUtils");
}
那么Spring Boot到底如何解决这个问题呢?
首先,我们来看看通过URL协议我们是如何加载这些文件的。对于每种协议(包括file、ftp、http、jar等),jdk都提供了对应的Handler实现类,如图所示:
既然通过这些Handler无法读取jar里面的jar文件,那么spring boot的开发者就另辟蹊径,替换掉了读取jar的Handler。首先来看看Java中URL包含协议的请求流程:
java的URL类中,会根据协议的名称进行包名的拼接,然后选择对应的Handler,我们可以在URL源码中看到这一点:
覆盖jar中的Handler有三种方法:
- 指定URL HandlerFactory
- 修改默认匹配包名
- 采用默认包名
我们可以设置URLStreamHandlerFactory来替换Handler,但是这个URLStreamHandlerFactory只能被替换一次。
正常情况下,这是没问题的,但是我们使用tomcat启动的时候就会报错,因为tomcat已经设置过URLStreamHandlerFactory一次,所以spring boot如果再次设置的话还是会报错。但是spring boot的还是有自己的办法解决这个问题,我们看看它是如何做的。
此时我们又要回到MANIFEST.MF文件,看看这个Main-Class对应的org.springframework.boot.loader.JarLauncher的main启动方法:
最终其实我们可以发现,spring-boot通过System.getProperty方法设定了默认包名的路径!
我们可以在刚才失败的方法上面添加这个方法,看是否能成功。此时还要修改以下jar协议,要在jar的后面加上"!/",那么新的测试类如下:
@Test
public void test() throws IOException, ClassNotFoundException {
JarFile.registerUrlProtocolHandler();
// 加载jar中jar
URL nested_url = new URL("jar:file:///D:/spring_project/myblog/target/demo-0.0.1-SNAPSHOT.jar!/BOOT-INF/lib/spring-beans-5.2.7.RELEASE.jar!/");
URL url = new URL("file:///D:/spring_project/myblog/target/demo-0.0.1-SNAPSHOT.jar");
URLClassLoader loader = new URLClassLoader(new URL[]{nested_url, url}, null);
Class<?> aClass = loader.loadClass("org.springframework.boot.loader.JarLauncher");
System.out.println(aClass);
loader.loadClass("org.springframework.beans.BeanUtils");
}
此时运行就不会报错了!
我们可以调试,看看不调用JarFile.registerUrlProtocolHandler方法与调用方法对应的Handler
执行JarFile.registerUrlProtocolHandler方法后:
可以看到两者的区别,他们对应的handler不同,一个是java底层的,一个是spring-boot-loader自定义的。
此时我们就了解了为什么spring能加载jar中jar了!
继续spring的jar包启动原理!
基于反射调用Start-Class
接下来就要讲第三步通过反射找到Start-Class并调用。
这里最终没搞好调试的环境,所以直接点进源码看了:
调用到这里就会执行我们启动类的方法。
接下来我们看看启动方法调用链: