文章目录
- 前言
- 第14章 运行SpringBoot应用
前言
在一个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文件规范的文档 :
文档中指出,可独立运行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个目录构成:
- 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类的继承关系图:
由上图可知,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包的启动引导器。
由JarLauncher类的javadoc可知,“/BOOT-INF/lib"和”/BOOT-INF/classes"这两个目录是项目启动的关键。
14.2.3.1.4 WarLauncher
由javadoc可知,WarLauncher本身也是一个启动类引导器,可以将打包好的war包使用java -jar xxx.war
命令引导启动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的引导原理
源码1:JarLauncher.java
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
源码2:Launcher.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
方法的返回值。
源码3:ExecutableArchiveLauncher.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;
}
源码4:Archive.java
default Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter)
throws IOException {...}
由 源码3 可知,getNestedArchives
方法需要传入两个EntryFilter参数,第一个是搜索范围searchFilter,第二个是过滤条件includeFilter。该方法的作用是以迭代器的形式返回在指定的搜索范围内与指定过滤器匹配的嵌套归档文件。
首先是搜索范围EntryFilter:isSearchCandidate
方法。
源码5:JarLauncher.java
@Override
protected boolean isSearchCandidate(Archive.Entry entry) {
return entry.getName().startsWith("BOOT-INF/");
}
由 源码5 可知,搜索范围是所有名称以"BOOT-INF/"开头的文件。
其次是过滤条件EntryFilter,筛选出需要收集起来的文件:(entry) -> isNestedArchive(entry) && !isEntryIndexed(entry)
。
源码6:JarLauncher.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
方法中。
源码7:Launcher.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
源码8:ExecutableArchiveLauncher.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
方法
源码9:Launcher.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);
}
源码10:MainMethodRunner.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包和字节码文件时搜索的目录不同。
源码11:WarLauncher.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 节有对运行时插件的描述:
由该段描述可知,Servlet容器启动应用时会扫描项目及依赖jar包中ServletContainerInitializer接口的实现类,方法是在jar包的META-INF/services目录中提供一个名为javax.servlet.ServletContainerInitializer
的文件,文件内容要标明ServletContainerInitializer接口实现类的全限定类名。
此外,实现了ServletContainerInitializer接口的实现类可以标注**@HandlesTypes注解**,并指定一些感兴趣的类(或接口类型),Servlet容器初始化时会将这些感兴趣的类(或接口的实现类)传入onStartup
方法的第一个参数中,以此完成一些更高级的处理。
源码12:ServletContainerInitializer.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。
源码13:SpringServletContainerInitializer.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的加载
源码14:SpringBootServletInitializer.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源码解读与原理分析