目的:

以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属性。
MANIFEST.MF
这个时候会有三个疑问:

1.这样的配置是如何形成的(插件在打包过程中做了哪些操作)

查看spring-boot-maven-plugin源码,有一个RepackageMojo类,会介入maven的package动作。这是因为其使用了@org.apache.maven.plugins.annotations.Mojo注解进行修饰,该注解的作用就是标记某个类,使其对应一个maven声明周期中某个阶段的操作。
RepackageMojo
在maven进行package时会执行execute()方法,并执行同类中的repackage()方法
RepackageMojo.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方法。
JarLauncher
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.这样有什么好处
  1. 提供一个统一的启动机制,无论是jar包还是war包,都能以一致的方式启动。无论是JarLauncher还是WarLauncher,其main()方法中执行的launch()方法逻辑都是走的公共父类org.springframework.boot.loader.Launcher类。
  2. JarLauncher通过URLClassLoader从指定的URL 位置加载类及其依赖和资源,确保应用程序的所有依赖项都能被正确加载和使用,传统的类加载器只能加载jar包中直接的类,而不能加载jar包中jar包的类。
Logo

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

更多推荐