springboot项目maven打包插件原理
使用java -jar命令运行一个jar文件时,jvm会读取jar文件中的MANIFEST.MF文件,其中的Main-Class属性用来确定应用程序的入口类。maven打包插件的主要作用就是将Main-Class属性写入MANIFEST.MF文件(以及Start-Class属性),并将程序的所有依赖库都一块放到BOOT-INF/lib/目录下,默认JarLauncher为主类,启动时JarLaun
目的:
以jar包为例(war包的原理基本上一致),为了能让一个springboot项目打成的jar包直接以java -jar
命令启动,需要引入spring-boot-maven-plugin
插件,springboot和maven插件使用版本:2.3.12.RELEASE
。
大致原理:
使用java -jar
命令运行一个jar文件时,jvm会读取jar文件中的MANIFEST.MF
文件,它包含了jar文件的元数据信息,其中的Main-Class
属性用来确定应用程序的入口类,如果不使用插件,很有可能不会有Main-Class
属性。插件的主要作用就是将Main-Class
属性写入MANIFEST.MF
文件(以及Start-Class
属性),并将程序的所有依赖库都一块放到BOOT-INF/lib/
目录下(JarLauncher
依赖spring-boot-loader.jar
的相关代码在jar包根目录下,注意:并不是将spring-boot-loader.jar
打入jar包,而是将spring-boot-loader.jar
中的class文件及目录结构,也就是org/*
直接写入jar包根目录)。
<!-- maven依赖 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.12.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
package com.personal;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 启动类
* @author lyj
**/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
打包完成后,打开jar包会发现MANIFEST.MF
文件中的Main-Class
属性并非我们期望中自己的启动类,而是org.springframework.boot.loader.JarLauncher
,我们自己的启动类则配置在Start-Class
属性。
这个时候会有三个疑问:
1.这样的配置是如何形成的(插件在打包过程中做了哪些操作)
查看spring-boot-maven-plugin
源码,有一个RepackageMojo
类,会介入maven的package
动作。这是因为其使用了@org.apache.maven.plugins.annotations.Mojo
注解进行修饰,该注解的作用就是标记某个类,使其对应一个maven声明周期中某个阶段的操作。
在maven进行package
时会执行execute()
方法,并执行同类中的repackage()
方法
重点在repackage()
方法中
private void repackage() throws MojoExecutionException {
//获取使用maven-jar-plugin生成的jar
Artifact source = getSourceArtifact(this.classifier);
// 最终文件,即Fat jar
File target = getTargetFile(this.finalName, this.classifier, this.outputDirectory);
//获取重新打包器,此时repackager中的layout为null,真正获取这个属性的是在try块中的repackager.repackage()中
Repackager repackager = getRepackager(source.getFile());
//查找并过滤项目运行时依赖的jar
Libraries libraries = getLibraries(this.requiresUnpack);
try {
// 获取启动脚本
LaunchScript launchScript = getLaunchScript();
// 执行重新打包逻辑,生成fat jar
repackager.repackage(target, libraries, launchScript, parseOutputTimestamp());
}
catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
updateArtifact(source, target, repackager.getBackupFile());
}
Repackager.repackage(File,Libraries,LaunchScript,FileTime)
方法
// Repackager.repackage()
public void repackage(File destination, Libraries libraries, LaunchScript launchScript, FileTime lastModifiedTime)
throws IOException {
Assert.isTrue(destination != null && !destination.isDirectory(), "Invalid destination");
// layout在这里获取,其获取的大致逻辑是根据文件后缀名进行匹配,jar文件会获取到org.springframework.boot.loader.tools.Layouts.Jar
getLayout(); // get layout early
if (lastModifiedTime != null && getLayout() instanceof War) {
throw new IllegalStateException("Reproducible repackaging is not supported with war packaging");
}
destination = destination.getAbsoluteFile();
File source = getSource();
// 判断是否重复打包,如果在没有改动的情况下重新执行打包操作,就会直接return了
if (isAlreadyPackaged() && source.equals(destination)) {
return;
}
File workingSource = source;
if (source.equals(destination)) {
workingSource = getBackupFile();
workingSource.delete();
renameFile(source, workingSource);
}
destination.delete();
try {
try (JarFile sourceJar = new JarFile(workingSource)) {
// 执行重新打包操作,填充Main-Class和Start-Class属性,写入依赖库等
repackage(sourceJar, destination, libraries, launchScript, lastModifiedTime);
}
}
finally {
if (!this.backupSource && !source.equals(workingSource)) {
deleteFile(workingSource);
}
}
}
getLayout()
方法
protected final Layout getLayout() {
if (this.layout == null) {
// getLayoutFactory()会拿到org.springframework.boot.loader.tools.DefaultLayoutFactory
Layout createdLayout = getLayoutFactory().getLayout(this.source);
Assert.state(createdLayout != null, "Unable to detect layout");
this.layout = createdLayout;
}
return this.layout;
}
// DefaultLayoutFactory.getLayout()
public Layout getLayout(File source) {
return Layouts.forFile(source);
}
// Layouts.forFile
public static Layout forFile(File file) {
if (file == null) {
throw new IllegalArgumentException("File must not be null");
}
String lowerCaseFileName = file.getName().toLowerCase(Locale.ENGLISH);
if (lowerCaseFileName.endsWith(".jar")) {
return new Jar();
}
if (lowerCaseFileName.endsWith(".war")) {
return new War();
}
if (file.isDirectory() || lowerCaseFileName.endsWith(".zip")) {
return new Expanded();
}
throw new IllegalStateException("Unable to deduce layout for '" + file + "'");
}
// Layouts.Jar,这里可以看到JarLauncher和其他jar包内的结构信息
public static class Jar implements RepackagingLayout {
@Override
public String getLauncherClassName() {
return "org.springframework.boot.loader.JarLauncher";
}
@Override
public String getLibraryLocation(String libraryName, LibraryScope scope) {
return "BOOT-INF/lib/";
}
@Deprecated
@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 String getClasspathIndexFileLocation() {
return "BOOT-INF/classpath.idx";
}
@Override
public String getLayersIndexFileLocation() {
return "BOOT-INF/layers.idx";
}
@Override
public boolean isExecutable() {
return true;
}
}
同类下的重载方法repackage(JarFile,File,Libraries,LaunchScript,FileTime)
,在上面repackage(File,Libraries,LaunchScript,FileTime)
方法中的try
块中调用
private void repackage(JarFile sourceJar, File destination, Libraries libraries, LaunchScript launchScript,
FileTime lastModifiedTime) throws IOException {
try (JarWriter writer = new JarWriter(destination, launchScript, lastModifiedTime)) {
// 获取各种信息,如Main-Class、Start-Class、lib库等,并写入文件
write(sourceJar, libraries, writer);
}
// 设置文件的修改时间
if (lastModifiedTime != null) {
destination.setLastModified(lastModifiedTime.toMillis());
}
}
protected final void write(JarFile sourceJar, Libraries libraries, AbstractJarWriter writer) throws IOException {
Assert.notNull(libraries, "Libraries must not be null");
WritableLibraries writeableLibraries = new WritableLibraries(libraries);
writer.useLayers(this.layers, this.layersIndex);
// buildManifest()方法中拿到并组装了springboot项目中文件的属性,如Main-Class等
writer.writeManifest(buildManifest(sourceJar));
// 将spring-boot-loader.jar的class文件写入jar包根路径,也就是JarLauncher的所在的依赖
writeLoaderClasses(writer);
// 写入class文件到BOOT-INF/classes/目录
writer.writeEntries(sourceJar, getEntityTransformer(), writeableLibraries);
//写入lib库
writeableLibraries.write(writer);
if (this.layers != null) {
writeLayerIndex(writer);
}
}
private Manifest buildManifest(JarFile source) throws IOException {
// 读取之前打包的source包中的MANIFEST.MF的文件属性
Manifest manifest = createInitialManifest(source);
// 设置Main-Class和Start-Class
addMainAndStartAttributes(source, manifest);
// 设置Spring-Boot-Version、Spring-Boot-Classes、Spring-Boot-Lib、Spring-Boot-Classpath-Index属性和Spring-Boot-Layers-Index属性(如果有Layers)
addBootAttributes(manifest.getMainAttributes());
return manifest;
}
private void addMainAndStartAttributes(JarFile source, Manifest manifest) throws IOException {
/*
* 如果在pom.xml中配置了mainClass,则直接使用配置好的mainClass
* 如果没有配置,则会自动推断实际的springboot启动类,大致是先找到含有main方法的类,
* 再通过@SpringBootApplication注解进行匹配
* 注意:这里如果使用自动推断,当项目中有多个匹配的类(有多个启动类)会抛出异常,
* 而在pom.xml中直接指定启动类就可以避免该异常抛出,只要别配置错就行。。。
*/
String mainClass = getMainClass(source, manifest);
// 获取LauncherClassName,即org.springframework.boot.loader.JarLauncher
String launcherClass = getLayout().getLauncherClassName();
// 设置Main-Class和Start-Class
if (launcherClass != null) {
Assert.state(mainClass != null, "Unable to find main class");
// main-class:JarLauncher
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, launcherClass);
// start-class:项目中实际的启动类
manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, mainClass);
}
else if (mainClass != null) {
manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, mainClass);
}
}
2.这样配置如何能找到并运行我们自己的启动类
由上面的repackage()->write()
方法可知,最终打成的jar包中的MANIFEST.MF
文件中,Main-Class
属性为spring-boot-loader.jar
包中的org.springframework.boot.loader.JarLauncher
类,项目实际配置的启动类在配置在Start-Class
属性,且在打包过程中将JarLauncher
的相关依赖代码均已打入jar文件中,因此可以根据Main-Class
属性直接找到JarLauncher
类,并启动其中的main
方法。launch(String[])
方法
// org.springframework.boot.loader.Launcher.launch()
protected void launch(String[] args) throws Exception {
if (!isExploded()) {
JarFile.registerUrlProtocolHandler();
}
// 创建 URLClassLoader
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
// 获取需要执行的MainClass,这里实际上取的是MANIFEST.MF文件中的Start-Class属性
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
launch(args, launchClass, classLoader);
}
// ExecutableArchiveLauncher.getMainClass()
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
// START_CLASS_ATTRIBUTE 值为 "Start-Class"
mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
//Launcher.launch()
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
// 将URLClassLoader设置为线程上下文类加载器
Thread.currentThread().setContextClassLoader(classLoader);
// 执行启动类main方法的操作在run()方法中
createMainMethodRunner(launchClass, args, classLoader).run();
}
//MainMethodRunner.run()
public void run() throws Exception {
// 下面的逻辑可以很清晰的看到使用了反射获取并执行main方法
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 });
}
3.这样有什么好处
- 提供一个统一的启动机制,无论是jar包还是war包,都能以一致的方式启动。无论是
JarLauncher
还是WarLauncher
,其main()
方法中执行的launch()
方法逻辑都是走的公共父类org.springframework.boot.loader.Launcher
类。 JarLauncher
通过URLClassLoader
从指定的URL 位置加载类及其依赖和资源,确保应用程序的所有依赖项都能被正确加载和使用,传统的类加载器只能加载jar包中直接的类,而不能加载jar包中jar包的类。

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐
所有评论(0)