Spring Boot jar包启动分析

a 夏天 提交于 2020-08-04 19:12:16

Spring Boot jar包启动分析

首先,为了了解Spring Boot Jar包的启动情况,我们需要构建一个Spring的FAT jar 看看其中都有哪些东西。

解压Spring Boot Maven Plugin 打包的jar

Spring Boot项目打包后,通过mvn package方式,可以看到在代码的target目录下,生成了一个jar文件和一个jar.original文件。这两个文件有什么区别呢?

  • jar文件:这个是通过SpringBootMavenPlugin插件重新包装出来的,他里面包含了我们项目中所依赖的所有jar包文件
  • jar.original文件:这个是通过maven打包后最原始的文件,里面没有项目中所以来的jar包,因此这个文件通常比较小

我们通过比较解压后的文件结构来了解下两种jar的区别。

jar 文件

├─BOOT-INF
│  │  classpath.idx
│  │
│  ├─classes
│  │  │  application.yml
│  │  │
│  │  └─com
│  │      └─chillax
│  │          │  Application.class
│  │          │
│  │          └─controller
│  │                  HelloController.class
│  │
│  └─lib
│          hutool-all-5.3.8.jar
│          .............jar
│
├─META-INF
│  │  MANIFEST.MF
│  │
│  └─maven
│      └─com.chillax
│          └─spring-boot-loader
│                  pom.properties
│                  pom.xml
│
└─org
    └─springframework
        └─boot
            └─loader
                │  ClassPathIndexFile.class
                │  ...........class
                │
                ├─archive
                │      Archive$Entry.class
                │      ..........class
                ├─data
                │      ..........class
                │
                ├─jar
                │      AsciiBytes.class
                │      .......class
                │
                ├─jarmode
                │      JarMode.class
                │      ............class
                │
                └─util
                        SystemPropertyUtils.class
  • BOOT-INF/classes 目录存放应用编译后的class文件
  • BOOT-INF/lib 目录存放应用以来的jar包
  • MEA-INF目录存放应用的相关元信息。
  • org目录存放Spring Boot相关classs

jar.original 文件

│  application.yml
│
├─com
│  └─chillax
│      │  Application.class
│      │
│      └─controller
│              HelloController.class
│
└─META-INF
    │  MANIFEST.MF
    │
    └─maven
        └─com.chillax
            └─spring-boot-loader
                    pom.properties
                    pom.xml

我们可以看到内容比较少,主要是少了,依赖的jar和Spring Boot相关classes。因为这个源文件会被Spring Boot的Maven插件重新打包,形成可运行的Spring Boot FatJar

FAT JAR执行模块-spring-boot-loader

当使用java -jar 命令时,按照官方文档提供的标准,默认的启动类必须要在/META-INF/MANIFEST.MF文件中指明启动的类。

jar 文件官方规范 参考这个链接 https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html

我们来看一下,Spring Boot项目中生成的清单文件是什么样子的

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Archiver-Version: Plexus Archiver
Built-By: chillax
Start-Class: com.chillax.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.3.1.RELEASE
Created-By: Apache Maven 3.6.1
Build-Jdk: 1.8.0_251
Main-Class: org.springframework.boot.loader.JarLauncher

我们可以看到项目的相关元信息。可以看到Main-Class被插件包装为了JarLauncher。并且指定的Start-class为我们的最常见的SpringApplication写的入口类。那我们所得到的信息是,Spring Boot 通过打包插件,讲项目的元信息进行再次封装后,通过spring-boot-loader#JarLauncher类启动我们的Spring Boot的入口类,从而实现spring boot 项目的jar启动。

spring-boot-loader查看

这里面类的层级关系,我们可以通过idea查看到

一般情况下,我们的FatJAR再我们执行java -jar 后,会执行JarLauncher#main方法,然后通过JarLauncher的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 {
		if (!isExploded()) {
			JarFile.registerUrlProtocolHandler();
		}
		ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
		String jarMode = System.getProperty("jarmode");
		String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
		launch(args, launchClass, classLoader);
	}

launch方法主要是执行了三部操作

  • 注册自己的Jar协议解析器

  • 加载自己的类加载器,用于加载自身classes和依赖的classes

    Archive

    SpringBoot定义了一个描述资源的接口org.springframework.boot.loader.archive.Archive

    • org.springframework.boot.loader.archive.ExplodedArchive
      • 用于在文件夹目录下寻找资源
    • org.springframework.boot.loader.archive.JarFileArchive
      • 用于在jar包环境下寻找资源。

    而在SpringBoot打包的fatJar中,则是使用后者。

    这里主要是解析Spring自己抽象的资源类,将所有关联的class加载

  • launch方法利用反射,启动MANIFEST.MF文件中指定的start-class的Main方法

为什么要注册自己的Jar协议解析器呢?

因为,默认情况下,JDK提供的ClassLoader只能识别Jar中的class文件以及加载classpath下的其他jar包中的class文件。对于在jar包中的jar包是无法加载的。

Spring Boot是如何重写协议解析器

首先,在java中描述一种资源,通常使用URL,与URL关联的协议通常有一个URLStreamHandler的实现类,

在JDK中可以找到file、http、jar的内置协议,这些实现类在sum.net.www.protocol下且类名必须为Handler

这些处理器的读取规则有如下几种:

  • 实现URLStreamHandlerFactory接口,通过方法URL.setURLStreamHandlerFactory设置。该属性是一个静态属性,且只能被设置一次。
  • 直接提供URLStreamHandler的子类,作为URL的构造方法的入参之一。但是在JVM中有固定的规范要求:
  • 子类的类名必须是 Handler ,同时最后一级的包名必须是协议的名称。比如自定义了Http的协议实现,则类名必然为xx.http.Handler
  • JVM 启动的时候,需要设置 java.protocol.handler.pkgs 系统属性,如果有多个实现类,那么中间用 | 隔开。因为JVM在尝试寻找Handler时,会从这个属性中获取包名前缀,最终使用包名前缀.协议名.Handler,使用Class.forName方法尝试初始化类,如果初始化成功,则会使用该类的实现作为协议实现。

因此,Spring Boot为了能够加载到当前jar中的class文件以及classpath下及其jar包下所有的class文件,spring官方扩展了URLStreamHandler实现了自己的jar读取协议该处理类在org.springframework.boot.loader.jar.Handler这里,感兴趣的可以去看看源码是怎么实现协议的扩展的。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!