SpringBoot之Jar启动原理

SpringBoot之Jar启动原理

SpringBoot是如何通过jar包启动的

得益于SpringBoot的封装,我们可以直接通过java -jar <name>.jar就可以启动一个Web项目.再也不用操心搭建Tomcat等相关Web容器.

java -jar做了什么

官网:

If the -jar option is specified, its argument is the name of the JAR file containing class and resource files for the application. The startup class must be indicated by the Main-Class manifest header in its source code.
复制代码

翻译:

使用-jar参数时,后面的参数是的jar文件名

该jar文件中包含的是class和资源文件

在manifest文件中有Main-Class的定义

Main-Class的源码中指定了整个应用的启动类

小结:

java -jar会去找jar中的mainfest文件,在那里面找到真正的启动类

先给出结论:

通过spring-boot-plugin生成了MANIFEST.MF文件,其中main-class指定了运行java -jar的主程序,把依赖的jar文件打包在了Fat Jar.当我们执行指令java -jar,它就会去运行JarLauncher

所有依赖的jar文件都在/BOOT-INF/lib目录下,以及所有依赖的class文件在BOO-INF/classes目录下,JarLauncher会根据路径去加载jar和class,加载之后,会去找到Start-Class然后使用反射去调用本地应用程序的Main方法

jar的打包插件

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>${spring-boot.version}</version>
        </plugin>
    </plugins>
</build>
复制代码

在MANIFEST.MF文件中内容:

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: 12_springboot_starter
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: linc.fun.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.6.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher
复制代码

其中有这么一行内容

Start-Class: linc.fun.Application
复制代码

在前面的java官方文档中,只提到了Main-Class,并没有提到Start-Class

Start-Class的值是linc.fun.Application,这是我们代码的启动类

所以问题来了:

理论上,执行java -jar指令时,JarLauncher类会被执行,但是实际上linc.fun.Application被执行了,这其中发生了什么呢?为什么要这么做呢?

况且Java没有提供任何标准的方式来加载嵌套的jar文件(它们本身包含在jar中的jar文件)

在我们执行mvn clean package之后会生成两个文件:

12_springboot_starter-0.0.1-SNAPSHOT.jar
12_springboot_starter-0.0.1-SNAPSHOT.jar.original
复制代码

spring-boot-maven-plugin项目存在于spring-boot-tools目录中.

spring-boot-maven-plugin默认有5个goals:

  • repackage
  • run
  • start
  • stop
  • build-info

在打包的时候默认使用的是repackage

spring-boot-maven-plugin的repackage能够将mvn package生成的软件包,再次打包为可执行的软件包,并将mvn package生成的软件包重命名为.original

spring-boot-maven-plugin的repackage在代码层面调用了RepackageMojo#execute方法,而在该方法中又调用了repackage方法.

repackage方法代码及操作解析如下:

private void repackage() throws MojoExecutionException {
   // maven生成的jar,最终的命名将加上.original后缀
   Artifact source = getSourceArtifact();
   // 最终为可执行jar,即fat jar
   File target = getTargetFile();
   // 获取重新打包器,将maven生成的jar重新打包成可执行jar
   Repackager repackager = getRepackager(source.getFile());
   // 查找并过滤项目运行时依赖的jar
   Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
         getFilters(getAdditionalFilters()));
   // 将artifacts转换成libraries
   Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,
         getLog());
   try {
      // 获得Spring Boot启动脚本
      LaunchScript launchScript = getLaunchScript();
      // 执行重新打包,生成fat jar
      repackager.repackage(target, libraries, launchScript);
   }catch (IOException ex) {
      throw new MojoExecutionException(ex.getMessage(), ex);
   }
   // 将maven生成的jar更新成.original文件
   updateArtifact(source, target, repackager.getBackupFile());
}
复制代码

jar包目录结构

12_springboot_starter-0.0.1-SNAPSHOT
├── META-INF
│   └── MANIFEST.MF
├── BOOT-INF
│   ├── classes
│   │   └── 应用程序类
│   └── lib
│       └── 第三方依赖jar
└── org
    └── springframework
        └── boot
            └── loader
                └── springboot启动程序
复制代码

META-INF内容

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: 12_springboot_starter
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: linc.fun.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.6.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher
复制代码
  • Main-Class: org.springframework.boot.loader.JarLauncher
    • 这是jar启动的Main函数
  • Start-Class: linc.fun.Application
    • 这是我们自己项目的Main函数

Archive

archive也就是归档文件,通常就是一个tar/zip格式的压缩包,jar是zip格式

SpringBoot抽象了Archive的概念,一个Archive可以是jar(JarFileArchive),可以是一个文件目录(ExplodeArchive),可以抽象为一个统一访问资源的逻辑层.

SpringBoot中Archive的源码:

public interface Archive extends Iterable<Archive.Entry> {
    // 获取该归档的url
    URL getUrl() throws MalformedURLException;
    // 获取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF
    Manifest getManifest() throws IOException;
    // 获取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jar
    List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
}
复制代码

SpringBoot定义了一个接口用于描述资源,也就是org.springframework.boot.loader.archive.Archive

这个接口有两个实现类:

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

在SpringBoot打包的fatJar中,就是使用的JarFileArchive

JarFile:

对jar包的封装,每个JarFileArchive都会对应一个JarFile.

JarFile被构造的时候会解析内部结构,去获取jar包里的各个文件或文件夹,这些文件或文件夹会被封装到Entry中,也存储在JarFileArchive中.

如果Entry是个jar,会解析成JarFileArchive

比如一个JarFileArchive对应的URL jar:file:/Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/

它对应的JarFile为 /Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar

这个JarFile有很多Entry,比如

META-INF/
META-INF/MANIFEST.MF
spring/
spring/study/
....
spring/study/executablejar/ExecutableJarApplication.class
lib/spring-boot-starter-1.3.5.RELEASE.jar
lib/spring-boot-1.3.5.RELEASE.jar
...
复制代码

JarFileArchive内部的一些依赖jar对应的URL,SpringBoot使用org.springframework.boot.loader.jar.Handler处理器来处理这些URL jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-starter-web-1.3.5.RELEASE.jar!/

jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class

我们看到如果有jar包中包含jar,或者jar包中包含jar包里面的class文件,那么会使用 !/ 分隔开,这种方式只有org.springframework.boot.loader.jar.Handler能处理,它是SpringBoot内部扩展出来的一种URL协议

JarLauncher

从MANIFEST.MF可以看到Main函数是JarLauncher,JarLauncher类的继承结构是

public class JarLauncher extends ExecutableArchiveLauncher

public abstract class ExecutableArchiveLauncher extends Launcher

JarLauncher可以加载内部/BOOT-INF/lib下的jar以及BOO-INF/classes下的应用class

public static void main(String[] args) throws Exception {
    (new JarLauncher()).launch(args);
}
复制代码

它主入口新建了JarLauncher并且调用父类Launcher中的launch方法启动程序.

在创建JarLauncher的时候,父类ExecutableArchiveLauncher找到自己所在的jar,并且创建archive.

JarLauncher继承ExecutableArchiveLauncher,这个类的无参构造最主要的功能就是构建当前main方法所在的FatJar的JarFileArchive对象.

launch

该方法主要是做了2个事情:

  • 以FatJar为file作为入参,构造JarFileArchive对象.获取其中所有的资源目标,取得其Url,将这些URL作为参数,构建了一个URLClassLoader
  • 以第一步构建的ClassLoader加载MANIFEST.MF文件中Start-Class指向的业务类,并且执行静态方法main.进而启动整个程序
public abstract class ExecutableArchiveLauncher extends Launcher {
    private final Archive archive;
    public ExecutableArchiveLauncher() {
        try {
            // 找到自己所在的jar,并创建Archive
            this.archive = createArchive();
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }
}
 
 
public abstract class Launcher {
    protected final Archive createArchive() throws Exception {
        ProtectionDomain protectionDomain = getClass().getProtectionDomain();
        CodeSource codeSource = protectionDomain.getCodeSource();
        URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
        String path = (location == null ? null : location.getSchemeSpecificPart());
        if (path == null) {
            throw new IllegalStateException("Unable to determine code source archive");
        }
        File root = new File(path);
        if (!root.exists()) {
            throw new IllegalStateException(
                    "Unable to determine code source archive from " + root);
        }
        return (root.isDirectory() ? new ExplodedArchive(root)
                : new JarFileArchive(root));
    }
}
复制代码

在Launcher的launch方法中,通过以上archive的getNestedArchives方法找到/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录所对应的archiv,通过这些archives的url生成LaunchedURLClassLoader,并将其设置为线程上下文类加载器,启动应用

至此,才执行我们应用程序主入口类的main方法,所有应用程序类文件均可通过/BOOT-INF/classes加载,所有依赖的第三方jar均可通过/BOOT-INF/lib加载

URLStreamHandler

java中描述资源常使用URL.

而URL有一个方法用于打开链接java.net.URL#openConnection()

由于URL用于表达各种各样的资源,打开资源的具体动作由java.net.URLStreamHandler这个类的子类来完成

根据不同的协议,会有不同的handler实现

而JDK内置了相当多的handler实现用于应对不同的协议.比如jar、file、http等等.URL内部有一个静态HashTable属性,用于保存已经被发现的协议和handler实例的映射

获得URLStreamHandler有三种方法:

(1)实现URLStreamHandlerFactory接口,通过方法URL.setURLStreamHandlerFactory设置。该属性是一个静态属性,且只能被设置一次。

(2)直接提供URLStreamHandler的子类,作为URL的构造方法的入参之一.但是在JVM中有固定的规范要求:

子类的类名必须是Handler,同时最后一级的包名必须是协议的名称.比如自定义了Http的协议实现,则类名必然为xx.http.Handler

JVM启动的时候,需要设置java.protocol.handler.pkgs系统属性,如果有多个实现类,那么中间用|隔开.

因为JVM在尝试寻找Handler时,会从这个属性中获取包名前缀,最终使用包名前缀.协议名.Handler,使用Class.forName方法尝试初始化类,如果初始化成功,则会使用该类的实现作为协议实现.

为了实现这个目标,SpringBoot首先从支持jar in jar中内容读取做了定制,也就是支持多个!/分隔符的url路径.

SpringBoot定制了以下两个方面:

(1)实现了一个java.net.URLStreamHandler的子类org.springframework.boot.loader.jar.Handler.该Handler支持识别多个!/分隔符,并且正确的打开URLConnection。打开的Connection是SpringBoot定制的org.springframework.boot.loader.jar.JarURLConnection实现

(2)实现了一个java.net.JarURLConnection的子类org.springframework.boot.loader.jar.JarURLConnection.该链接支持多个!/分隔符,并且自己实现了在这种情况下获取InputStream的方法.而为了能够在org.springframework.boot.loader.jar.JarURLConnection正确获取输入流,SpringBoot自定义了一套读取ZipFile的工具类和方法.这部分和ZIP压缩算法规范紧密相连,就不拓展了.

Jar应用启动总结

  • JarLauncher通过加载BOOT-INF/classes目录及BOOT-INF/lib目录下jar文件,实现了fat jar的启动
  • SpringBoot通过扩展JarFile、JarURLConnection及URLStreamHandler,实现了jar in jar中资源的加载
  • SpringBoot通过扩展URLClassLoader–LauncherURLClassLoader,实现了jar in jar中class文件的加载
  • WarLauncher通过加载WEB-INF/classes目录及WEB-INF/lib和WEB-INF/lib-provided目录下的jar文件,实现了war文件的直接启动及web容器中的启动

猜你喜欢

转载自juejin.im/post/7090604832578338852