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

您所在的位置:网站首页 springboot的jar包启动如何指定外部配置文件 Spring Boot Jar 包启动时如何加载外部资源

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

2023-09-19 15:24| 来源: 网络整理| 查看: 265

在项目有读取特殊配置文件的地方(不是 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 种类型的启动器:

JarLauncherWarLauncherPropertiesLauncher

当打包为 jar 或 war 时选择的前两个,JarLauncher 从 BOOT-INF/lib/ 目录加载 jars,WarLauncher 从 WEB-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 配置 org.springframework.boot spring-boot-maven-plugin ${start.class} ZIP repackage

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

JARWARZIP:使用 PropertiesLauncherNONE: 不捆绑引导加载程序

通过选择 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); }

这里调用了父类 Launcher 的 launch 方法:

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 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 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 中,这会存在同路径和名称文件的覆盖问题”。如果后续有时间再单独从这个角度再分析看看。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3