Springboot 启动过程
用于源码分析的代码:Github
我们首先从springboot的jar包的启动开始,因为这个里面有个知识点,java 程序的启动都是通过一个Main Class的main方法作为整个程序的入口来启动的,而启动的命令是通过jdk安装目录里的bin文件夹下的java命令脚本来启动的,jar包的启动命令就是 java -jar spring-boot-learn-1.0-SNAPSHOT.jar 这个命令没有指定main class,那么JVM是怎么知道启动这个jar里的那个class的main方法呢?
一、Jar包的启动流程
这里先了解下jar包启动的知识点
1、classpath
我们运行java类时,比如使用java com.hj.learn.HelloWorld 时,JVM需要知道在哪里找到这个com.hj.learn.HelloWorld.class文件,所以就需要指定classpath,这里有两种指定classpath的方式。一:设置环境变量,这个不推荐,有兴趣的可以去搜下,因为这种是全局指定了路径,而我们的java代码可能是在不同的目录下;二:在启动命令里指定classpath,使用java -cp 来制定(-cp是-classpath的缩写,如果没有指定的话,就默认是当前目录即:./),比如hj.learn.HelloWorld.class文件是在/Users/workspace/java/spring-boot-learn/target/classes下,那么就可以使用java -cp /Users/workspace/java/spring-boot-learn/target/classes com.hj.learn.HelloWorld 来运行,运行这个命令时,jvm会先在./当前目录查找,没查到时,会再去/Users/workspace/java/spring-boot-learn/target/classes/com/hj/learn下查找HelloWorl.class(这里会将package路径换成文件路径)。
2、java -jar命令
当使用-jar时,classpath就默认是当前指定的这个jar包,比如使用java -jar /Users/workspace/java/spring-boot-learn/target/spring-boot-learn-1.0-SNAPSHOT.jar 那么这个路径也就是classpath,JVM会到这个jar里找main class,但是在这个命令里没有指定main class,JVM又是怎么找到main class的,这个需要看jar包里的META-INF里的MANIFEST.MF文件。我们使用 jar -xvf spring-boot-learn-1.0-SNAPSHOT.jar 来解压出来,找到这个文件后,能看到里面有个Main-Class: org.springframework.boot.loader.JarLauncher,那么这个就是整个程序的入口。
居然不是我们代码里的Application类!我们就从这个类开始debug,阅读源码。由于这个类所在的jar包是业务代码用不上的,是springboot的maven插件帮我们打包进去的,我们查看源码时,需要自己引用依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
二、源码分析
1、待解答的问题:
- 为什么要从org.springframework.boot.loader.JarLauncher启动,JarLauncher这个类到底做了什么?
- 为什么这么做,为什么不能从我们代码里的Application类启动?
- 学到了哪些东西
2、debug前的准备:
在idea里直接启动项目时,idea不是从org.springframework.boot.loader.JarLauncher这个入口启动的,我们需要debug这个源码,所以需要通过jar包启动时,通过远程debug jar的进程:
- 使用mvn package打好jar包
- cd到jar包所在的目录
- 使用命令方式启动jar包,在启动参数上开启远程debug模式,命令为 java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 -jar spring-boot-learn-1.0-SNAPSHOT.jar 这里的debug端口是5005
- idea里配置好remote的debug配置,我用的是IntelliJ IDEA,远程debug配置
3、开始debug
使用命令启动jar包,看到
Listening for transport dt_socket at address: 5005
表示在等待连接debug
在org.springframework.boot.loader.JarLauncher的main方法打断点,启动idea的远程debug。
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
首先看JarLauncher类图,发现其继承自ExecutableArchiveLauncher
new JarLauncher(),JarLauncher实例化时候,会首先实例化父类ExecutableArchiveLauncher,所以此处使用红色step into会debug进父类ExecutableArchiveLauncher的构造器。里面会实例化一个JarFileArchive,这个类可以理解为封装了访问这个jar的一些功能,从这个jar包里获取文件信息。比如读取MANIFEST.MF信息。
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
接着debug金launch方法:
protected void launch(String[] args) throws Exception {
// 2.1代码
JarFile.registerUrlProtocolHandler();
// 2.2代码
ClassLoader classLoader = createClassLoader(getClassPathArchives());
//2.3代码
launch(args, getMainClass(), classLoader);
}
代码2.1
此处代码进去后根据方法的注释,可以理解为此处可以根据设置的环境变量,在后续解析jar时,生成对应的URLStreamHandler 来处理jar,此处可以忽略。
代码2.2
此处代码step into进去后,能看到getClassPathArchives方法 是获取这个jar下BOOT-INF/classes/ 和 BOOT-INF/lib/下的所有文档,并抽象成JarFileArchive对象。
createClassLoader方法是创建一个自定义的类加载器LaunchedURLClassLoader,而这个类加载器负责的路径就是BOOT-INF/classes/ 和 BOOT-INF/lib/下的所有资源。
java的类加载器ClassLoader知识可以自己搜索看下,主要是loadClass 和 findClass两个方法,通过代码可以看到在构造LaunchedURLClassLoader时, BOOT-INF/classes/ 和 BOOT-INF/lib/下的所有资源被抽象成Url数组传给了UrlClassLoader构造器,而通过UrlClassLoader的findClass方法,可以看到这个类加载器就是从这些位置来加载对应的class资源。
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
// 父类构造器
public URLClassLoader(URL[] urls, ClassLoader parent) {
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.acc = AccessController.getContext();
ucp = new URLClassPath(urls, acc); // 将url数组保存到了ucp对象了
}
// defindClass方法,这个是类加载器加载类时需要调用的方法
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
// 通过ucp来获取指定的资源
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
2.3 代码
launch(args, getMainClass(), classLoader)
getMainClass()就是通过JarFile对象获取jar包下的META-INF/MANIFEST.MF里配置的Start-Class,我么debug进JarFile的构造方法就能发现:
继续debug进launch方法:
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
// 代码3.1
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
public void run() throws Exception {
// 代码3.2
Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
代码3.1
此处将生成的LaunchedURLClassLoader设置成线程上下文加载器,后面会从这个地方去除LaunchedURLClassLoader来加载我们代码里的Application.class
代码3.2
使用3.1处set进去的LaunchedURLClassLoader来加载我们的Application.class,并通过反射执行main方法
总结及思考
org.springframework.boot.loader.JarLauncher主要做的事:
1、最主要就是为了生成一个自定义的LaunchedURLClassLoader类加载器,指定好这个类加载器负责BOOT-INF/classes/和BOOT-INF/lib/下的所有jar包。然后再把这个类加载器放在上下文加载器里(Thread.currentThread().setContextClassLoader(classLoader);),以保证整个Spring启动过程中优先使用这个上下文加载器来加载类。
2、找到META-INF/MANIFEST.MF下的Start-Class配置(即:Application.class),通过反射启动我们业务代码里的Application.class,并通过反射启动Application.class的main方法,拉起整个Spring容器。
为什么这么做,为什么不能从我们代码里的Application类启动?
我先说结论:我们项目是通过maven打成jar包的,使用java -jar启动jar包时,需要指定classpath来告知jvm从什么位置加载类文件,如果想在启动命令里手动指定classpath是非常繁琐和容易出错的,因为你的classpath里需要包含所有的jar路径,为了省去这个麻烦,就在JarLauncher里指定一个类加载器,并指定这个加载器负责path为BOOT-INF/classes/和BOOT-INF/lib/下的所有jar。而我们在idea里直接启动项目时就是这个方式,只不过是idea帮我们把classpath都加好了,我们在idea启动好进程后,通过ps -ef|grep pid 的方式能看到启动参数:
如果自己来配的话,可能要疯了,并且每次改下依赖后,启动参数也要改…。那么为了避免这种情况,定义了一个类JarLauncher来作为Main-class,自定义一个类加载器,并指定这个加载器负责的路径是BOOT-INF/classes/和BOOT-INF/lib/下的所有jar,这样就省去了我们自己配置启动参数。这时候整个项目的类加载器结构就是下图了:
学到了哪些东西?
了解java类加载器的机制,双亲委派原则,至于什么是双亲委派,可以自己百度下,看到这个图时,有人会问图里的应用程序类加载器负责的是spring-boot-learn.jar下的资源,不是正好包括BOOT-IN/lib 和 BOOT-INF/classes么,那直接使用应用程序类加载器不就可以了么,这里要先理解类加载器是怎么查找class资源的,比如Application Class Loader类加载器负责spring-boot-learn.jar下的资源,那么当需要加载org.springframework.boot.loader.JarLauncher时,第一步:会将package路径替换成文件夹路径,org.springframework.boot.loader变成org/springframework/boot/loader 第二步:在spring-boot-learn.jar的下一级目录找org/springframework/boot/loader,也就是在spring-boot-learn.jar!/org/springframework/boot/loader/下去找JarLauncher.class文件,而如果要加载com.hj.learn.Application,也就会去spring-boot-learn.jar!/com/hj/learn/下去找Application.class文件,所以是将类的package替换成文件夹路径,明确去到这些路径下查找对应的class文件,不会去查BOOT-IN/lib 和 BOOT-INF/classes。
其中启动类加载器是写在JVM里的,而扩展类加载器对应java里的sun.misc.Launcher$ExtClassLoader,通过代码可以看到这个类负责的是JAVA_HOME/lib/ext下的jar资源,自己可以打印出这个环节变量看下
应用程序类加载器对应的是 sun.misc.Launcher$AppClassLoader,通过代码可以看到这个类负责的是classpath下的jar资源
那为什么说LaunchedURLClassLoader类加载器是最底层的自定义加载器呢,通过debug可以看到这个类加载器的父类是引用程序类加载器sun.misc.Launcher$AppClassLoader,那LaunchedURLClassLoader在上面的层次图里就是最底层了。
本文地址:https://blog.csdn.net/caoyuanyenang/article/details/107086790
上一篇: 杨修为什么会被曹操所杀?是因为他的聪明害死了他吗?
下一篇: Java手写简易版HashMap