当前位置: 移动技术网 > IT编程>开发语言>Java > Springboot 启动过程

Springboot 启动过程

2020年11月27日  | 移动技术网IT编程  | 我要评论
用于源码分析的代码: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方法呢?一、J

用于源码分析的代码: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
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Zr7tUQA-1593672604035)(quiver-image-url/077E5E475085EDD5937EEE23FEF6BC49.jpg =285x246)]
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/下的所有资源。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zTr7VDM7-1593672604037)(quiver-image-url/31D68CC131107D8C6FC1E2C172AB27FA.jpg =376x328)]
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的构造方法就能发现:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XrHPgWLA-1593672604040)(quiver-image-url/F92E723CC9C2D7CACC29232B82C4D7D2.jpg =962x616)]
在这里插入图片描述

继续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 的方式能看到启动参数:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ns0X53Qa-1593672604042)(quiver-image-url/AB19B352BDA088D4A35F96C15B8A6C8D.jpg =1431x631)]](https://img-blog.csdnimg.cn/2020070214532110.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Nhb3l1YW55ZW5hbmc=,size_16,color_FFFFFF,t_70如果自己来配的话,可能要疯了,并且每次改下依赖后,启动参数也要改…。那么为了避免这种情况,定义了一个类JarLauncher来作为Main-class,自定义一个类加载器,并指定这个加载器负责的路径是BOOT-INF/classes/和BOOT-INF/lib/下的所有jar,这样就省去了我们自己配置启动参数。这时候整个项目的类加载器结构就是下图了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IWz37hT7-1593672604044)(quiver-image-url/144F1D7C30C989DC69C17B74F378EAB1.jpg =588x539)]

学到了哪些东西?

了解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资源,自己可以打印出这个环节变量看下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9WWvfmfe-1593672604045)(quiver-image-url/E2FBA58BBF2DCE3C26F643FD7EC6B63F.jpg =1493x682)]

应用程序类加载器对应的是 sun.misc.Launcher$AppClassLoader,通过代码可以看到这个类负责的是classpath下的jar资源
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8eQ18nwV-1593672604049)(quiver-image-url/31168276D0D04E8EAB6727892B7B13AB.jpg =1475x630)]

那为什么说LaunchedURLClassLoader类加载器是最底层的自定义加载器呢,通过debug可以看到这个类加载器的父类是引用程序类加载器sun.misc.Launcher$AppClassLoader,那LaunchedURLClassLoader在上面的层次图里就是最底层了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S0yd91v4-1593672604050)(quiver-image-url/CE2D17D6A95FB5475FCB9CD830D06753.jpg =1046x394)]

本文地址:https://blog.csdn.net/caoyuanyenang/article/details/107086790

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网