一.可执行JAR结构分析
在Spring Boot应用中,使用spring-boot-maven-plugin
插件执行mvn package
命令生成的jar文件,可以通过java -jar
命令直接运行,这种jar文件称为可执行jar文件(Executable JAR)。
1.可执行jar文件的获取
可以从任意SpringBoot工程中运行mvn package
命令生成的jar文件,如没有现成的SpringBoot工程,可以参考下列步骤生成一个。
在https://start.spring.io/中创建创建SpringBoot项目,填写项目的Group、Aritact及Package信息如:
填写完信息后,点击GENERAT按钮生成SpringBoot项目压缩文件并下载到本地,通过
unzip
命令解压后,进入项目目录并执行
mvn package
命令,在项目的target目录下便生成了可执行jar文件(executable-jar-0.0.1-SNAPSHOT.jar)和原始Maven打包的jar文件(executable-jar-0.0.1-SNAPSHOT.jar.original)等文件。接下来我们打开jar文件一窥究竟吧。
2.可执行jar文件内部结构
执行unzip executable-jar-0.0.1-SNAPSHOT.jar -d temp
将jar包解压到temp目录下,在通过tree
命令查看目录结构:
william@liushipingdeMacBook-Pro target % tree temp
temp
├── BOOT-INF
│ ├── classes
│ │ ├── application.properties
│ │ └── cn
│ │ └── lsp
│ │ └── springboot
│ │ └── executablejar
│ │ └── ExecutableJarApplication.class
│ ├── classpath.idx
│ ├── layers.idx
│ └── lib
│ ├── ... ...
│ ├── spring-boot-2.6.2.jar
│ ├── ... ...
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── cn.lsp.springboot
│ └── executable-jar
│ ├── pom.properties
│ └── pom.xml
└── org
└── springframework
└── boot
└── loader
├── ClassPathIndexFile.class
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader.class
├── Launcher.class
├── ... ...
1.BOOT-INF/classes目录存放应用编译后的class文件;
2.BOOT-INF/classpath.id 可执行jar文件依赖的类路径索引文件;
3.BOOT-INF/lib目录存放应用依赖的jar包;
4.META-INF目录存放应用相关的元信息,如MANIFEST.MF文件;
5.org目录存放启动SpringBoot相关的class文件;
通过解压目录看出,和传统的jar文件相比,多了BOOT-INF目录和启动SpringBoot相关的class文件,并且将传统的class文件放置到了BOOT-INF是classes目录下,所依赖的jar均放到了BOOT-INF/lib目录。
我们知道。通过java -jar
运行的是标准的可执行jar文件,按照Java官方文档的规定,该命令引导的具体启动类必须配置在META-INF/MANIFEST.MF文件的Main-Class属性中。那我们来查看一下该文件的内容:
william@liushipingdeMacBook-Pro temp % cat META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven Jar Plugin 3.2.0
Build-Jdk-Spec: 16
Implementation-Title: executable-jar
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: cn.lsp.springboot.executablejar.ExecutableJarApplication
Spring-Boot-Version: 2.6.2
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
可以发现Main-Class属性的值为org.springframework.boot.loader.JarLauncher
,而我们自己的项目中的Main Class全路径名(cn.lsp.springboot.executablejar.ExecutableJarApplication
)则存放到了Start-Class属性中。从文件内容可以看出SpringBoot的运行都是通过org.springframework.boot.loader.JarLauncher
来引导的,该类就是可执行jar的启动器。
二.可执行JAR源码分析
由于可执行jar文件的启动类为org.springframework.boot.loader.JarLauncher
,为了方便分析源码了解其实现原理,我们将该类所在jar包引入项目的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<scope>provided</scope>
</dependency>
启动流程源码解读
public class JarLauncher extends ExecutableArchiveLauncher {
private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx";
static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
if (entry.isDirectory()) {
return entry.getName().equals("BOOT-INF/classes/");
}
return entry.getName().startsWith("BOOT-INF/lib/");
};
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
// Only needed for exploded archives, regular ones already have a defined order
if (archive instanceof ExplodedArchive) {
String location = getClassPathIndexFileLocation(archive);
return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
}
return super.getClassPathIndex(archive);
}
private String getClassPathIndexFileLocation(Archive archive) throws IOException {
Manifest manifest = archive.getManifest();
Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
return (location != null) ? location : DEFAULT_CLASSPATH_INDEX_LOCATION;
}
@Override
protected boolean isPostProcessingClassPathArchives() {
return false;
}
@Override
protected boolean isSearchCandidate(Archive.Entry entry) {
return entry.getName().startsWith("BOOT-INF/");
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
该类是一个标准的Java应用程序入口类,继承自ExecutableArchiveLauncher
,常量DEFAULT_CLASSPATH_INDEX_LOCATION
所指向的文件内容为应用依赖的jar文件类路径。isNestedArchive
方法用于判断Archive.Entry是否是Jar文件中的资源,Archive.Entry有两种实现,JarFileArchive.JarFileEntry
和ExplodedArchive.FileEntry
,前者基于jar文件,后者基于文件系统,所以JarLauncher支持Jar文件和文件系统两种启动方式。
当执行java -jar命令时,META-INF/MANIFEST.MF文件的Main-Class属性将调用main(String[])方法,实际上是调用JarLauncher#launch(args)
方法,该方法继承于基类org.springframework.boot.loader.Launcher
,他们之间的继承层次图如下:
org.springframework.boot.loader.Launcher
org.springframework.boot.loader.ExecutableArchiveLauncher
org.springframework.boot.loader.JarLauncher //用于引导jar文件
org.springframework.boot.loader.WarLauncher // 用于引导war文件
下面分析Launcher#launch(args)方法实现:
public abstract class Launcher {
... ...
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);
}
... ...
}
JarFile.registerUrlProtocolHandler()
方法将package org.springframework.boot.loader
追加到Java系统属性java.protocol.handler.pkgs
中,即org.springframework.boot.loader.jar.Handler
,其实现协议为JAR,用于覆盖JDK内建的sun.net.www.protocol.jar.Handler
。由于SpringBoot的可执行Jar文件除了包含传统的Java Jar中的资源外,还包含依赖的Jar文件,当SpringBoot的可执行jar被java -jar命令引导时,其内部的jar文件无法被JDK内建的sun.net.www.protocol.jar.Handler
当做Class Path,所以需要替换才能确保正常运行。
createClassLoader(Iterator)
方法用于创建LaunchedURLClassLoader,实现类的加载。
最后调用实际的引导类launch(args, launchClass, classLoader)
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(launchClass, args, classLoader).run();
}
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
该方法的实际执行者为MainMethodRunner#run()方法。
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = (args != null) ? args.clone() : null;
}
public void run() throws Exception {
Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.setAccessible(true);
mainMethod.invoke(null, new Object[] { this.args });
}
}
MainMethodRunner对象需要关联mainClass及main方法参数args,通过反射来调用项目中真正的入口类的main方法,即META-INF/MANIFEST.MF文件中指定的Start-Class: cn.lsp.springboot.executablejar.ExecutableJarApplication。至此,应用程序的class path等环境在启动前已准备完毕,真正进入应用的启动阶段。
三.总结
1.SpringBoot的Launcher有JarLauncher和WarLauncher,前者引导jar文件启动,后者引导war文件启动;
2.SpringBoot的Launcher有两种引导模式,基于Jar和文件系统;
3.由于SpringBoot生成的可执行jar文件与传统jar文件不同,因此需要实现自己的org.springframework.boot.loader.jar.Handler来覆盖JDK内建的sun.net.www.protocol.jar.Handler,从而按照SpringBoot自己的方式来初始化classpath等环境并引导jar运行;
网友评论