SpringBoot的 jar为什么 可以直接运行?
先打包个JAR看下:
SpringBoot提供了一个插件spring-boot-maven-plugin用于把程序打包成一个可执行的jar包。在pom文件里加入这个插件即可:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
打包完生成的 example.jar 内部的结构如下:
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── com.test
│ └── mytest
│ ├── pom.properties
│ └── pom.xml
├── org
│ └── springframework
│ └── boot
│ └── loader
│ ├── archive
│ ├── data
│ ├── jar
│ ├── ...
│
├── BOOT-INF
│ ├── class
│ ├── com
│ └── test
│ ├── XX.java
│ ├── XX.calss
│ ├── application.properties
│ ├── log4j.properties
│
│ ├── lib
│ ├── spring-boot-1.3.5.RELEASE.jar
│ ├── spring-boot-autoconfigure-1.3.5.RELEASE.jar
│ ├── ...
然后可以直接执行jar包就能启动程序了:
java -jar example.jar
打包出来fat jar内部有4种文件类型:
-
META-INF:程序入口,其中MANIFEST.MF用于描述jar包的信息
-
BOOT-INF/lib:放置第三方依赖的jar包,比如springboot的一些jar包
-
BOOT-INF/classes:存放应用编译后的 class 文件。
-
org: spring-boot-loader本身需要的class放置处
MANIFEST.MF文件的内容:
Manifest-Version: 1.0
Created-By: Maven Jar Plugin 3.2.0
Build-Jdk-Spec: 11
Implementation-Title: davinqi-test-pdfcli
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Version: 2.4.0
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
Start-Class: com.shebao.pdfcli.PdfCliApp
它的Main-Class是org.springframework.boot.loader.JarLauncher,当我们使用java -jar执行jar包的时候会调用JarLauncher的main方法,
而不是我们编写的SelfApplication( XXXAppApplication )
也就是说想要知道 fat jar
是如何生成的,就必须知道spring-boot-maven-plugin
工作机制,而spring-boot-maven-plugin
属于自定义插件,因此我们又必须知道,Maven的自定义插件是如何工作的。
The Spring Boot Maven Plugin provides Spring Boot support in Maven, letting you package executable jar or war archives and run an application “in-place”. To use it, you must use Maven 3.2 (or later).
翻译: Spring Boot Maven Plugin提供了Spring Boot在Maven中的支持,让你打包可执行的jar或war档案,并“就地”运行应用程序。要使用它,必须使用Maven 3.2(或更高版本)。
由上图我们可以看到,Spring Boot Maven plugin提供几种常用操作,maven标准说法是5种goal。
- spring-boot:repackage,默认goal。在mvn package之后,再次打包可执行的jar/war,同时保留mvn package生成的jar/war为.origin
- spring-boot:run,运行Spring Boot应用
- spring-boot:start,在mvn integration-test阶段,进行Spring Boot应用生命周期的管理
- spring-boot:stop,在mvn integration-test阶段,进行Spring Boot应用生命周期的管理
- spring-boot:build-info,生成Actuator使用的构建信息文件build-info.properties
那么我们就从这个 repackage开始看:
可以看到: 在项目中可以找到对应 上边说的(maven标准说法是5种goal)的执行实体类:
由于上面的XML 标签<mojo> 以及类关系UML图, 他们都实现了 org.apache.maven.plugin.Mojo#execute.
红框处为maven类的接口类。mojo这个东西就是入口。
Springboot 的 RepackageMojo 类实现了mojo接口就相当于实现 maven 的执行入口。我们打开mojo这个接口。那么我们主要看 RepackageMojo 这个类的执行过程:
org.springframework.boot.maven.RepackageMojo#execute
调用
org.springframework.boot.maven.RepackageMojo#repackage
private void repackage() throws MojoExecutionException {
// Artifact mvn的接口 通过他获取mvn的依赖信息
Artifact source = getSourceArtifact();
// 最终文件,即Fat jar
File target = getTargetFile();
// 重新打包器,将重新打包成可执行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 {
// 启动脚本
LaunchScript launchScript = getLaunchScript();
// 重新打包生成最后fat jar
repackager.repackage(target, libraries, launchScript);
}
catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
// 将source更新成 xxx.jar.orignal文件
updateArtifact(source, target, repackager.getBackupFile());
}
由上代码可知,该方法具体步骤:
1.获取源文件的Artifact
2.创建并获取打包文件,这里仅仅是生成,并没有写入任何东西。
3.通过源文件构建一个打包对象Repackager。
4.获取项目的所有依赖Artifact,并且过滤一些不需要依赖对象
5.构建依赖对象
6.获取启动脚本
7.由步骤3构建的打包对象进行重新打包
8.更新Artifact
private Repackager getRepackager(File source) {
Repackager repackager = new Repackager(source, this.layoutFactory);
repackager.addMainClassTimeoutWarningListener(
new LoggingMainClassTimeoutWarningListener());
// 不指定的,会查找第一个包含main方法的类, 最后将会设置org.springframework.boot.loader.JarLauncher
repackager.setMainClass(this.mainClass);
if (this.layout != null) {
getLog().info("Layout: " + this.layout);
// 视图布局 layout 判断Jar 还是war
repackager.setLayout(this.layout.layout());
}
return repackager;
}
/**
* Executable JAR layout.
*/
public static class Jar implements RepackagingLayout {
@Override
public String getLauncherClassName() {
return "org.springframework.boot.loader.JarLauncher";
}
@Override
public String getLibraryDestination(String libraryName, LibraryScope scope) {
return "BOOT-INF/lib/";
}
@Override
public String getClassesLocation() {
return "";
}
@Override
public String getRepackagedClassesLocation() {
return "BOOT-INF/classes/";
}
@Override
public boolean isExecutable() {
return true;
}
}
这样就生成了我们想要的文章最初的文件结构 ( layout
布局)
启动类 JarLauncher
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
public static void main(String[] args) throws Exception { //项目入口,重点在launch这个方法中
new JarLauncher().launch(args); }
}
protected void launch(String[] args) throws Exception {
if (!this.isExploded()) {
JarFile.registerUrlProtocolHandler();
}
// 创建LaunchedURLClassLoader。如果根类加载器和扩展类加载器没有加载到某个类的话,
// 就会通过LaunchedURLClassLoader这个加载器来加载类。
// 这个加载器会从Boot-INF下面的class目录和lib目录下加载类。
ClassLoader classLoader = this.createClassLoader(this.getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
String launchClass = jarMode != null && !jarMode.isEmpty() ? "org.springframework.boot.loader.jarmode.JarModeLauncher" : this.getMainClass();
// 这个方法会读取jar描述文件中的Start-Class属性,然后通过反射调用到这个类的main方法。
this.launch(args, launchClass, classLoader);
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(this.isExploded(), this.getArchive(), urls, this.getClass().getClassLoader());
}
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
} else {
return mainClass;
}
}
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
this.createMainMethodRunner(launchClass, args, classLoader).run();
}
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((Object)null, this.args);
}
寻找这个类: Start-Class: com.shebao.pdfcli.PdfCliApp
- Spring Boot 执行入口是 JarLauncher 的 main 方法;
- 逻辑是先创建一个 LaunchedURLClassLoader:先判断根类加载器和扩展类加载器能否加载到某个类,如果都加载不到就从 Boot-INF 下面的 class 和 lib 目录下去加载;
- 读取
Start-Class
属性,通过反射,调用启动类的 main(),然后就是执行spring容器的类的加载初始化等操作。