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

Posted isea533

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 包启动时如何加载外部资源的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot 2 启动时加载properties文件

spring boot jar包启动JNI相对路径加载dll方案二

spring boot jar包启动JNI相对路径加载dll方案二

spring boot 加载原理

关于springboot启动所需所有jar包详解

spring boot打jar包运行,加载jar包外部的页面以及静态资源文件