SpringBoot源码解读与原理分析(四十)基于jar/war包的运行机制

前言

在一个SpringBoot项目开发完成后,最终需要项目部署到服务器使其正常运行,以提供功能服务使用。部署运行SpringBoot项目的方法一般采用打包部署为主。

第14章 运行SpringBoot应用

14.1 部署打包的两种方式

大多数情况下,会选择将SpringBoot项目打包为一个可独立运行的jar包,或者去掉内置的嵌入式Web容器,以war包形式部署到外置的容器中,这取决于开发者最终要部署的目标环境。

14.1.1 以可独立运行jar包的方式

将SpringBoot项目打包为一个可独立运行的jar包,需要在pom.xml文件中引入spring-boot-maven-plugin插件。

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

配置好后,执行mvn package命令,就可在项目根目录的target目录下获得一个可执行的jar包,直接执行java -jar xxx.jar命令就可以启动该项目。

14.1.2 以war包的方式

将SpringBoot项目打包为一个war包,需要在pom.xml文件中额外添加一些配置。

<packaging>war</packaging>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
    </dependency>
</dependencies>

然后还要修改主启动类或者新建一个类,使其继承SpringBootServletInitializer类,并重写configure方法指定配置源为当前项目的主启动类。

@SpringBootApplication
public class WebFluxApp extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(WebFluxApp.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(WebFluxApp.class);
    }
}

修改完成后,重新执行mvn package命令,就可以生成一个可以部署到外置Web容器的war包。

14.2 基于jar包的独立运行机制

14.2.1 可独立运行jar包的相关知识

从Oracle的官网上可以找到有关 jar文件规范的文档

The signature related files are:

  • META-INF/MANIFEST.MF
  • META-INF/*.SF
  • META-INF/*.DSA
  • META-INF/*.RSA
  • META-INF/SIG-*

文档中指出,可独立运行jar包的一个核心目录是 META-INF/,这个目录会存放当前jar包的一些扩展和配置数据,其中一个核心配置文件是 MANIFEST.MF,它以properties的形式保存了jar包的一些核心元信息。

查阅文档可知,MANIFEST.MF文件的核心配置项主要包含以下几项(一共有21项,这里只列出其中3项相对重要的):

配置项配置含义配置值示例
Manifest-Version定义MANIFEST.MF文件的版本1.0(通常)
Class-Path指定当前jar包所依赖的jar包的路径(一般是相对路径)servlet.jar、config/
Main-Class引导可独立运行jar包启动的引导类的全限定类名org.springframework.boot.loader.JarLauncher

重点关注配置项 Main-Class,它指定了一个可以在jar包的顶层结构中直接找到的、带有main方法的、引导jar包启动的引导类的全限定类名。

这里所说的顶层结构,指的是在可独立运行的jar包中,可以直接在目录中找到,不需要再解压jar包内部。换句话说,被Main-Class配置项引用的类必须同它所属的包一起放在可独立运行jar包的顶层。

14.2.2 SpringBoot的可独立运行jar包结构

对于SpringBoot通过Maven插件打包的可独立运行jar包,它的内部由3个目录构成:

可独立运行jar包结构

  • BOOT-INF:存放项目编写且编译好的字节码文件、静态资源文件、配置文件,以及依赖的jar包。
  • META-INF:存放 MANIFEST.MF 等配置元信息。
  • org.springframework.boot.loader:存放spring-boot-loader的核心引导类,这些都放在了顶层结构中

其中META-INF中的 MANIFEST.MF 文件内容如下:

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: springboot-07-webmvc
Implementation-Version: 1.0-SNAPSHOT
Start-Class: com.star.springboot.webmvc.WebMvcApp
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.11.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher

注意其中两个配置项:

  • Main-Class: org.springframework.boot.loader.JarLauncher

这个配置前面已经已经解释过,是引导可独立运行jar包启动的引导类的全限定类名。

  • Start-Class: com.star.springboot.webmvc.WebMvcApp

这个配置项中定义的WebMvcApp类,是开发者在项目中自定义的,并且这个配置项在官网的jar文件规范中并没有提及,因此Start-Class配置项本身不是MANIFEST.MF文件标准规范中的配置项,而是SpringBoot自行定义的

由此可以推测,如果直接用WebMvcApp来引导这个可独立运行jar包,是无法启动项目的。

试验一下,将MANIFEST.MF文件的Main-Class属性的值改为com.star.springboot.webmvc.WebMvcApp,并执行java -jar xxx.jar命令,发现根本无法启动项目。

无法启动项目
无法启动的原因在于,引导启动的WebMvcApp类并没有放在jar包的顶层目录下,而是放在了 BOOT-INF/classes/ 目录下,中间隔了两层包。

如果Main-Class属性使用默认指定的JarLauncher类,则可以正常启动SpringBoot项目,说明JarLauncher类是引导启动的核心类。

14.2.3 JarLauncher的设计及工作原理

JarLauncher类来自于spring-boot-loader依赖,用于引导可独立运行jar包的启动。

14.2.3.1 JarLauncher的继承结构

借助IDEA,可以生成JarLauncher类的继承关系图:

JarLauncher类的继承关系图
由上图可知,SpringBoot项目的启动器是通过两个Launcher类的落地实现类JarLauncher和WarLauncher实现的,它们分别处理jar包和war包的启动,而这两个落地实现类又同时继承自ExecutableArchiveLauncher类。

14.2.3.1.1 Launcher

Launcher是启动SpringBoot项目的顶层引导类,它的内部定义了一个非常关键的launch方法,用于启动SpringBoot项目。

14.2.3.1.2 ExecutableArchiveLauncher

从类名上可以理解为“可执行归档文件的启动器”。

所谓“归档文件”,可以简单理解为,一个SpringBoot的独立可执行jar包就是一个归档文件,可以放在外置的Web容器中运行的war包也是一个归档文件。

ExecutableArchiveLauncher的作用在于,从归档文件中检索到SpringBoot项目的主启动类,并提供给父类Launcher以完成主启动类的引导。

14.2.3.1.3 JarLauncher

JarLauncher是基于SpringBoot可独立运行jar包的启动引导器。

Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a /BOOT-INF/lib directory and that application classes are included inside a /BOOT-INF/classes directory.

基于jar包归档文件的启动器。此启动器假定项目所依赖的jar包包含在/BOOT-INF/lib目录中,项目中所定义的类包含在/BOOT-INF/classes目录中。

由JarLauncher类的javadoc可知,“/BOOT-INF/lib"和”/BOOT-INF/classes"这两个目录是项目启动的关键。

14.2.3.1.4 WarLauncher

Launcher for WAR based archives. This launcher for standard WAR archives. Supports dependencies in WEB-INF/lib as well as WEB-INF/lib-provided, classes are loaded from WEB-INF/classes.

基于war包归档文件的启动器。此启动器用于标准的war包,项目所依赖的jar包包含在WEB-INF/lib和WEB-INF/lib-provided中,项目中所定义的类包含在WEB-INF/classes目录中。

由javadoc可知,WarLauncher本身也是一个启动类引导器,可以将打包好的war包使用java -jar xxx.war命令引导启动SpringBoot项目。

WarLauncher引导启动SpringBoot项目
与jar包不同,war包对于所依赖的jar包和项目中的Class文件有一定限制。对于一个标准war包,项目中的Class文件要放在 WEB-INF/classes 目录下,所依赖的jar包要放在 WEB-INF/lib 目录下,另外所有作用范围为provided的依赖统一放在 WEB-INF/lib-provided 目录下。

如果war包独立运行,则会同时加载 WEB-INF/lib 和 WEB-INF/lib-provided 目录下的依赖,而当war包放置于外置Web容器时,由于Web容器不会读取 WEB-INF/lib-provided 目录,这部分依赖不会被加载。这样就同时兼容了两种启动方式。

14.2.3.2 JarLauncher的引导原理
源码1JarLauncher.java

public static void main(String[] args) throws Exception {
    new JarLauncher().launch(args);
}
源码2Launcher.java

protected void launch(String[] args) throws Exception {
    // 注册URL协议并清除应用缓存
    if (!isExploded()) {
        JarFile.registerUrlProtocolHandler();
    }
    // 创建类加载器
    ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
    // 获取主启动类的类名
    String jarMode = System.getProperty("jarmode");
    String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
    // 执行主启动类的main方法
    launch(args, launchClass, classLoader);
}

由 源码1 可知,JarLauncher内部定义了一个main方法,作为整个可运行jar包运行的入口。在main方法中调用了launch方法,该方法定义在其顶层父类Launcher中。

由 源码2 可知,launch方法的核心步骤可以拆分为三步:

14.2.3.2.1 创建类加载器:createClassLoader

调用createClassLoader方法创建类加载器时,其参数是getClassPathArchivesIterator方法的返回值。

源码3ExecutableArchiveLauncher.java

@Override
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
    Archive.EntryFilter searchFilter = this::isSearchCandidate;
    Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
            (entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
    if (isPostProcessingClassPathArchives()) {
        archives = applyClassPathArchivePostProcessing(archives);
    }
    return archives;
}
源码4Archive.java

default Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter)
        throws IOException {...}

由 源码3 可知,getNestedArchives方法需要传入两个EntryFilter参数,第一个是搜索范围searchFilter,第二个是过滤条件includeFilter。该方法的作用是以迭代器的形式返回在指定的搜索范围内与指定过滤器匹配的嵌套归档文件。

首先是搜索范围EntryFilter:isSearchCandidate方法。

源码5JarLauncher.java

@Override
protected boolean isSearchCandidate(Archive.Entry entry) {
    return entry.getName().startsWith("BOOT-INF/");
}

由 源码5 可知,搜索范围是所有名称以"BOOT-INF/"开头的文件。

其次是过滤条件EntryFilter,筛选出需要收集起来的文件:(entry) -> isNestedArchive(entry) && !isEntryIndexed(entry)

源码6JarLauncher.java

static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
    // 如果是文件夹,则是"BOOT-INF/classes/"文件夹
    if (entry.isDirectory()) {
        return entry.getName().equals("BOOT-INF/classes/");
    }
    // 如果是文件,则是"BOOT-INF/lib/"下的文件
    return entry.getName().startsWith("BOOT-INF/lib/");
};

@Override
protected boolean isNestedArchive(Archive.Entry entry) {
    return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
}

由 源码6 可知,过滤条件是筛选所有"BOOT-INF/lib/"目录下的文件以及"BOOT-INF/classes/"文件夹。

经过以上筛选,getClassPathArchivesIterator以迭代器形式返回了当前SpringBoot应用中依赖的嵌套jar包和字节码文件,并作为参数传入createClassLoader方法中。

源码7Launcher.java

protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
    // 将Archive对象转换为URL对象
    List<URL> urls = new ArrayList<>(50);
    while (archives.hasNext()) {
        urls.add(archives.next().getUrl());
    }
    return createClassLoader(urls.toArray(new URL[0]));
}

protected ClassLoader createClassLoader(URL[] urls) throws Exception {
    // 创建类加载器LaunchedURLClassLoader
    return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader());
}

由 源码7 可知,createClassLoader方法首先将上一步获取到的Archive对象转换为一个URL对象,每个URL对象对应一个jar包或字节码文件的路径。转换完成后,最终创建的类加载器是LaunchedURLClassLoader,传入URL对象数组。

14.2.3.2.2 获取主启动类类名:getMainClass
源码8ExecutableArchiveLauncher.java

private static final String START_CLASS_ATTRIBUTE = "Start-Class";
@Override
protected String getMainClass() throws Exception {
    Manifest manifest = this.archive.getManifest();
    String mainClass = null;
    if (manifest != null) {
        // 读取MANIFEST.MF文件中的"Start-Class"属性的值
        mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
    }
    if (mainClass == null) {
        // throw ...
    }
    return mainClass;
}

由 源码8 可知,获取主启动类的方式就是读取取 MANIFEST.MF 文件中的"Start-Class"属性的值。

在前面的【14.2.2 SpringBoot的可运行jar包结构】中就提到过,"Start-Class"属性刚好就定义了SpringBoot应用主启动类的全限定类名。

14.2.3.2.3 执行主启动类的main方法
源码9Launcher.java

protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
    Thread.currentThread().setContextClassLoader(classLoader);
    // 构造MainMethodRunner对象并执行其run方法
    createMainMethodRunner(launchClass, args, classLoader).run();
}

protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
    return new MainMethodRunner(mainClass, args);
}
源码10MainMethodRunner.java

public void run() throws Exception {
    // 利用反射机制获取主启动类
    Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
    // 获取主启动类的main方法
    Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
    mainMethod.setAccessible(true);
    // 执行主启动类的main方法
    mainMethod.invoke(null, new Object[] { this.args });
}

由 源码9-10 可知,重载的launch方法首先会构造一个MainMethodRunner对象,传入主启动类的类名及参数。

随后调用MainMethodRunner对象的run方法,该方法会利用反射机制获取主启动类的Class对象,再通过getDeclaredMethod方法获取主启动类的main方法并执行。

当SpringBoot主启动类的main方法被成功调用后,SpringBoot应用即可顺利启动,基于JarLauncher的启动引导完成。

14.2.3.3 WarLauncher的引导原理

使用WarLauncher的引导原理在本质上和JarLauncher并无太大区别,只是在定位依赖jar包和字节码文件时搜索的目录不同。

源码11WarLauncher.java

@Override
protected boolean isSearchCandidate(Entry entry) {
    return entry.getName().startsWith("WEB-INF/");
}

@Override
public boolean isNestedArchive(Archive.Entry entry) {
    if (entry.isDirectory()) {
        return entry.getName().equals("WEB-INF/classes/");
    }
    return entry.getName().startsWith("WEB-INF/lib/") || entry.getName().startsWith("WEB-INF/lib-provided/");
}

由 源码11 可知,基于WarLauncher的搜索范围是"WEB-INF/classes/"、"WEB-INF/lib/"以及"WEB-INF/lib-provided/"三个目录。

14.3 基于war包的外部Web容器运行机制

基于war包的外置容器运行需要借助Servlet 3.0规范的一个引导机制,这个机制是SpringBoot应用启动的核心。

14.3.1 Servlet 3.0规范中引导应用启动的说明

在Servlet 3.0规范文档的 8.2.4 节有对运行时插件的描述:

An instance of the ServletContainerInitializer is looked up via the jar services API by the container at container / application startup time. The framework providing an implementation of the ServletContainerInitializer MUST bundle in the META-INF/services directory of the jar file a file called javax.servlet.ServletContainerInitializer, as per the jar services API, that points to the implementation class of the ServletContainerInitializer.

在容器/应用程序启动时,通过SPI机制查找ServletContainerInitializer的示例。提供ServletContainerInitializer实现的框架必须在jar包的META-INF/services目录中定义一个名为javax.servlet.ServletContainerInitializer的文件,根据SPI机制,找到对应的ServletContainerInitializer接口的实现类。

In addition to the ServletContainerInitializer we also have an annotation - HandlesTypes. The HandlesTypes annotation on the implementation of the ServletContainerInitializer is used to express interest in classes that may have anotations (type, method or field level annotations) specified in the value of the HandlesTypes or if it extends / implements one those classes anywhere in the class’ super types. The container uses the HandlesTypes annotation to determine when to invoke the initializer’s onStartup method.

除了ServletContainerInitializer之外,我们还有一个注解——@HandlesTypes。ServletContainerInitializer实现类上的@HandlesTypes注解用于表达对一些类(或接口类型)的兴趣。容器使用@HandlesTypes注解来确定何时调用初始化器的onStartup方法。

由该段描述可知,Servlet容器启动应用时会扫描项目及依赖jar包中ServletContainerInitializer接口的实现类,方法是在jar包的META-INF/services目录中提供一个名为javax.servlet.ServletContainerInitializer的文件,文件内容要标明ServletContainerInitializer接口实现类的全限定类名。

此外,实现了ServletContainerInitializer接口的实现类可以标注**@HandlesTypes注解**,并指定一些感兴趣的类(或接口类型),Servlet容器初始化时会将这些感兴趣的类(或接口的实现类)传入onStartup方法的第一个参数中,以此完成一些更高级的处理。

源码12ServletContainerInitializer.java

public interface ServletContainerInitializer {
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

由 源码12 可知,ServletContainerInitializer本身是一个接口,它仅有一个onStartup方法,不难推测出Servlet容器启动时会回调onStartup方法以完成应用的初始化逻辑。

14.3.2 SpringBootServletInitializer

SpringBoot为了适配外置Servlet容器启动的方法,提供了一个特殊的实现类SpringBootServletInitializer。

在【14.1.2 以war包的方式】中提到,要将SpringBoot项目打包为一个war包,不仅需要在pom.xml文件中添加一些配置,还需要编写一个SpringBootServletInitializer的子类,指定SpringBoot主启动类作为启动源。

这样编写的目的在于,为当前SpringBoot项目提供一个SpringBootServletInitializer子类,从而让外置Servlet容器在启动时可以加载该子类,从而初始化和启动SpringBoot应用。

14.3.2.1 ServletContainerInitializer的加载

当外置Servlet容器启动时,默认会加载部署的war包,此时被打包成war包的SpringBoot项目被解压,Servlet容器会从当前项目及项目所依赖的jar包中搜索一个全路径名为 META-INF/services/javax.servlet.ServletContainerInitializer 的文件(基于SPI机制)。

如果成功搜索到该文件,则会加载文件中定义的全限定类名对应的类。

在spring-web依赖中,可以找到该文件,文件中定义的全限定类名是org.springframework.web.SpringServletContainerInitializer。

spring-web依赖

源码13SpringServletContainerInitializer.java

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

    @Override
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {
        // 加载、实例化WebApplicationInitializer对象 ...
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }
}

由 源码13 可知,SpringServletContainerInitializer类标注了@HandlesTypes注解,它感兴趣的类型是WebApplicationInitializer,意味着onStartup方法会获取当前项目中所有实现了WebApplicationInitializer接口的落地实现类。

14.3.2.2 SpringBootServletInitializer的加载
源码14SpringBootServletInitializer.java

public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        this.logger = LogFactory.getLog(getClass());
        WebApplicationContext rootApplicationContext = createRootApplicationContext(servletContext);
        // ......
    }
    
    protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
        SpringApplicationBuilder builder = createSpringApplicationBuilder();
        // ......
        // 此处的configure方法执行的是自定义的
        builder = configure(builder);
        // ......
        // 构建SpringApplication
        SpringApplication application = builder.build();
        // ......
        // 基于外置Servlet容器启动不需要注册回调钩子
        application.setRegisterShutdownHook(false);
        return run(application);
    }
}

protected WebApplicationContext run(SpringApplication application) {
    // 调用SpringApplication的run方法
    return (WebApplicationContext) application.run();
}

由 源码14 可知,SpringBootServletInitializer实现了WebApplicationInitializer接口,因此SpringServletContainerInitializer的onStartup方法会获取的当前项目中实现了WebApplicationInitializer接口的落地实现类就是SpringBootServletInitializer。

SpringBootServletInitializer的onStartup方法中,核心动作是创建一个SpringApplication对象并调用其run方法真正启动应用。

在构建SpringApplication对象过程中,调用的configure方法实际上就是调用了【14.1.2】节中编写的SpringBootServletInitializer的子类中的configure方法,这里指定了SpringBoot项目真正的主启动类。

SpringApplicationBuilder正是拿到了这个主启动类,才能构建对应的SpringApplication对象。

经过SpringBootServletInitializer的构建并调用SpringApplication的run方法,SpringBoot项目即可成功启动。

······

本节完,更多内容请查阅分类专栏:SpringBoot源码解读与原理分析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

维先生d

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值