Pre
大家开发的基于Spring Boot 的应用 ,jar形式, 发布的时候,绝大部分都是使用java -jar 启动。 得益于Spring Boot 的封装 , 再也不用操心搭建tomcat等相关web容器le , 一切变得非常美好, 那SpringBoot是怎么做到的呢?

引导
新建工程 打包 启动
我们新创建一个Spring Boot的工程

其中打包的配置为
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
先打包一下

查看target目录

然后启动


java -jar 干啥的
我们先看看 java -jar 干了啥 ?
在oracle官网找到了该命令的描述:
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 【spring-0.0.1-SNAPSHOT.jar】,该jar文件中包含的是class和资源文件; 在manifest文件中有Main-Class的定义;Main-Class的源码中指定了整个应用的启动类;
简单来说: java -jar会去找jar中的manifest文件,去找到Main-Class对应的真正的启动类;
那看看去吧

咦 ,这个Main-Class 是Spring Boot 的。
我们还看到有个Start Class

官方文档中,只提到过Main-Class ,并没有提到Start-Class;
Start-Class的值是com.artisan.spring.Application
,这是我们的java代码中的唯一类,包含main方法, 是能够真正的应用启动类

所以问题就来了:理论上看,执行java -jar命令时JarLauncher类会被执行,但实际上是com.artisan.spring.Application
被执行了,这其中发生了什么呢?why?

打包插件
事实上,Java没有提供任何标准的方式来加载嵌套的jar文件 (jar中包含jar ,即Spring Boot 中的fat jar)
Spring Boot 默认的打包插件如下:
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
执行maven clean package之后,会生成两个文件,刚才我们也看到了

spring-boot-maven-plugin简介
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方法。
- 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());
- }
执行以上命令之后,便生成了打包结果对应的两个文件。

包结构
下面针对文件的内容和结构进行一探究竟。
- spring-0.0.1-SNAPSHOT.jar
- ├── META-INF
- │ └── maven(主要是pom文件)
- │ └── 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: spring
- Implementation-Version: 0.0.1-SNAPSHOT
- Spring-Boot-Layers-Index: BOOT-INF/layers.idx
- Start-Class: com.artisan.spring.Application
- Spring-Boot-Classes: BOOT-INF/classes/
- Spring-Boot-Lib: BOOT-INF/lib/
- Build-Jdk-Spec: 1.8
- Spring-Boot-Version: 2.4.1
- 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: com.artisan.spring.Application,这个是我们应用自己的Main函数
Archive的概念
在继续了解底层概念和原理之前,我们先来了解一下Archive的概念:
- archive即归档文件,这个概念在linux下比较常见
- 通常就是一个tar/zip格式的压缩包
- jar是zip格式
SpringBoot抽象了Archive的概念,一个Archive可以是jar(JarFileArchive),可以是一个文件目录(ExplodedArchive),可以抽象为统一访问资源的逻辑层
关于Spring Boot中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
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类的继承结构是:
- class JarLauncher extends ExecutableArchiveLauncher
- class ExecutableArchiveLauncher extends Launcher
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.
什么意思呢?
按照定义,JarLauncher可以加载内部/BOOT-INF/lib下的jar及/BOOT-INF/classes下的应用class。
- public class JarLauncher extends ExecutableArchiveLauncher {
- public JarLauncher() {}
- public static void main(String[] args) throws Exception {
- new JarLauncher().launch(args);
- }
- }
其主入口新建了JarLauncher并调用父类Launcher中的launch方法启动程序。在创建JarLauncher时,父类ExecutableArchiveLauncher找到自己所在的jar,并创建archive。
JarLauncher继承于org.springframework.boot.loader.ExecutableArchiveLauncher。该类的无参构造方法最主要的功能就是构建了当前main方法所在的FatJar的JarFileArchive对象。
下面来看launch方法。该方法主要是做了2个事情:
(1)以FatJar为file作为入参,构造JarFileArchive对象。获取其中所有的资源目标,取得其Url,将这些URL作为参数,构建了一个URLClassLoader。
(2)以第一步构建的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目录所对应的archive,通过这些archives的url生成LaunchedURLClassLoader
,并将其设置为线程上下文类加载器,启动应用。
至此,才执行我们应用程序主入口类的main方法,所有应用程序类文件均可通过/BOOT-INF/classes加载,所有依赖的第三方jar均可通过/BOOT-INF/lib加载。

小结
- 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容器中的启动。
通过spring-boot-plugin 生成了MANIFEST.MF , main-class 指定运行java -jar的主程序把依赖的jar文件 打包在fat jar.
到此这篇关于Spring Boot如何通过java -jar启动的文章就介绍到这了,更多相关SpringBoot java -jar启动内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!