Spring Boot Jar 包启动时如何加载外部资源

在项目有读取特殊配置文件的地方(不是 Spring 的 application 配置),项目打包为 jar 后,无法从外部替换默认的配置文件。

我自己尝试了 java -cp 的方式,发现没法启动(Spring Boot 打的包很特殊)。

通过谷歌搜索查到:Spring Boot Executable Jar with Classpath

其中 Peter Tarlos 的答案是完整的,本文的内容也是以这里为起点,通过查找官方文档来说明如何实现。

1. 关键的 PropertiesLauncher

Executable Jars Spring Boot’s executable jars, their launchers, and their format.

在 Spring Boot 中,存在 3 种类型的启动器:

  • JarLauncher
  • WarLauncher
  • PropertiesLauncher

当打包为 jar 或 war 时选择的前两个,JarLauncherBOOT-INF/lib/ 目录加载 jars,WarLauncherWEB-INF/lib/WEB-INF/lib-provided/ 加载 jars,如果想添加额外的 jars 就需要往这些目录添加。

第三个 PropertiesLauncher 默认BOOT-INF/lib/ 目录加载 jars,你还可以通过 LOADER_PATH 或者 loader.properties 中的 loader.path 配置额外的位置(多个位置逗号隔开),所以这个是我们需要的启动类。

启动类最终是在打包文件中的 MANIFEST.MF 中配置的,例如 jar 方式:

Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.mycompany.project.MyApplication

想要使用 PropertiesLauncher,可以通过官方的配置来启用。

2. 如何配置使用 PropertiesLauncher

Build Tool Plugins Maven Plugin, Gradle Plugin, Antlib, and more.

在官方打包工具中,有 Maven 和 Gradle 的两种方式。

Maven 配置

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <mainClass>${start.class}</mainClass>
                <layout>ZIP</layout>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

这里的 <layout>ZIP</layout> 配置可以选择使用哪个启动器,默认根据 <packing> 打包类型( jar 或 war)确定,可以配置下面可选值:

  • JAR
  • WAR
  • ZIP:使用 PropertiesLauncher
  • NONE: 不捆绑引导加载程序

通过选择 ZIP 即可使用 PropertiesLauncher

Gradle 配置

配置起来更直接,配置如下:

tasks.named("bootWar") {
	manifest {
		attributes 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher'
	}
}

3. 指定其他类加载路径

详细的配置可以参考官方文档: PropertiesLauncher Features,这里就简单举例用用:

java -Dloader.path=file:/config -jar spring-boot-app.jar

通过 -Dloader.path=file:/config 指定路径后,就能通过这种方式覆盖 jar 包中的文件了。

使用 -Dloader.dubug=true 会通过 System.out.println 输出日志信息。

4. 类路径的加载顺序(优先级)

为了确保替换配置文件的方式有效,最后还要确认一下类路径的加载顺序,只有当我提供的配置先加载时,才能确保替换默认的配置文件,官方没有明确说明加载顺序,因此只能通过代码来确认。

PropertiesLauncher 中存在 main 方法:

public static void main(String[] args) throws Exception {
	PropertiesLauncher launcher = new PropertiesLauncher();
	args = launcher.getArgs(args);
	launcher.launch(args);
}

这里调用了父类 Launcherlaunch 方法:

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);
}

在创建 ClassLoader 时,调用了 getClassPathArchivesIterator() 方法,这个方法会获取所有类路径下面的资源文件和jar包,这个方法就是我们要重点关注的方法:

@Override
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
	ClassPathArchives classPathArchives = this.classPathArchives;
	if (classPathArchives == null) {
		classPathArchives = new ClassPathArchives();
		this.classPathArchives = classPathArchives;
	}
	return classPathArchives.iterator();
}

这里创建了 ClassPathArchives 的单例,在构造方法中:

ClassPathArchives() throws Exception {
	this.classPathArchives = new ArrayList<>();
	for (String path : PropertiesLauncher.this.paths) {
		for (Archive archive : getClassPathArchives(path)) {
			debug("paths: " + archive.getUrl());
			addClassPathArchive(archive);
		}
	}
	addNestedEntries();
}

这里的 PropertiesLauncher.this.paths 就是通过 loader.path 配置的所有路径,这部分内容首先添加进去了,从这儿已经可以看出 loader.path 的优先级更高,因此通过这种方式设置的外部配置文件会优先使用。

上面代码后面的 addNestedEntries 在 jar 包启动时,就是加载 jar 包中的内容:

private void addNestedEntries() {
	// The parent archive might have "BOOT-INF/lib/" and "BOOT-INF/classes/"
	// directories, meaning we are running from an executable JAR. We add nested
	// entries from there with low priority (i.e. at end).
	try {
		Iterator<Archive> archives = PropertiesLauncher.this.parent.getNestedArchives(null,
				JarLauncher.NESTED_ARCHIVE_ENTRY_FILTER);
		while (archives.hasNext()) {
			this.classPathArchives.add(archives.next());
		}
	}
	catch (IOException ex) {
		// Ignore
	}
}

在这里的 PropertiesLauncher.this.parent 对应的就是启动的 jar,这里调用获取嵌套的包,使用 JarLauncher.NESTED_ARCHIVE_ENTRY_FILTER 作为过滤条件,过滤条件定义:

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/");
};

可以看到当遍历当前 jar 包时,只会匹配 BOOT-INF/classes/ 目录和 BOOT-INF/lib/ 下面的所有文件。

5. 不在深入一点吗?

到这里本文关注的内容就结束了,但是如果看源码只看到这种程度就够了吗?

看源码最好是有目的的看,看到感兴趣的地方时再深入看,看上面代码时你最有兴趣的地方在哪里?

我最感兴趣的是 ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); 这里,以及后续对该 classLoader 的使用,这部分的内容应该是 Spring Boot 能打成一个特殊 fat jar 启动的核心,Spring Boot 包和 Apache Maven Shade Plugin 插件的区别在于 “Spring Boot 中依赖的 jar 包仍然是独立的 jar,存在于 BOOT-INF/lib 中,Shade 插件打的是一个真正的大 jar 包,把所有依赖的 jar 都抽取到了大的 jar 中,这会存在同路径和名称文件的覆盖问题”。如果后续有时间再单独从这个角度再分析看看。

Spring Boot中实现热加载外部jar,可以通过以下步骤进行操作: 1. 首先在pom.xml文件中添加spring-boot-devtools依赖,如下所示: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <scope>runtime</scope> </dependency> ``` 该依赖主要用于开发环境下的热加载功能。 2. 然后,在application.properties文件中添加以下配置: ``` spring.devtools.restart.enabled=true ``` 该配置表示启用热加载功能。 3. 接下来,在IDE中启动Spring Boot应用程序。当修改了外部jar时,IDE会自动检测到变化并重新加载应用程序。可以通过快捷键Ctrl + F9手动触发重新加载。 4. 此外,还可以在IDE的设置中进行配置,以确保外部jar的修改能够被自动检测到。例如,在IntelliJ IDEA中,可以通过以下步骤进行设置: - 进入Preferences/Settings窗口,找到Build, Execution, Deployment - Compiler - 在右侧的Additional build process VM options中添加以下参数: ``` -Dspring.devtools.restart.additional-paths=/path/to/external/jar ``` 这样,IDE会监视指定路径下的外部jar的变化,并自动触发热加载。 总结起来,实现Spring Boot的热加载外部jar的步骤主要括添加相关依赖、配置启用热加载功能,并在IDE中进行相应的设置。这样,当外部jar发生变化时,应用程序会自动重新加载,从而提高开发效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

isea533

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值