惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
博客园_首页
H
Help Net Security
T
Tailwind CSS Blog
S
SegmentFault 最新的问题
GbyAI
GbyAI
Scott Helme
Scott Helme
D
Docker
Hacker News: Ask HN
Hacker News: Ask HN
P
Privacy & Cybersecurity Law Blog
Jina AI
Jina AI
雷峰网
雷峰网
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Spread Privacy
Spread Privacy
G
GRAHAM CLULEY
C
Cisco Blogs
The Hacker News
The Hacker News
F
Full Disclosure
Y
Y Combinator Blog
Blog — PlanetScale
Blog — PlanetScale
Recent Announcements
Recent Announcements
G
Google Developers Blog
量子位
K
Kaspersky official blog
Cisco Talos Blog
Cisco Talos Blog
The Cloudflare Blog
A
About on SuperTechFans
C
Cybersecurity and Infrastructure Security Agency CISA
Last Week in AI
Last Week in AI
博客园 - 三生石上(FineUI控件)
Microsoft Security Blog
Microsoft Security Blog
Martin Fowler
Martin Fowler
T
Tenable Blog
P
Palo Alto Networks Blog
H
Heimdal Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
W
WeLiveSecurity
Schneier on Security
Schneier on Security
The Register - Security
The Register - Security
F
Fortinet All Blogs
Stack Overflow Blog
Stack Overflow Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
The Blog of Author Tim Ferriss
N
News and Events Feed by Topic
Hugging Face - Blog
Hugging Face - Blog
小众软件
小众软件
V
V2EX
爱范儿
爱范儿

czp's blog

Ubuntu 下使用 zsh · HonKit 使用 VNC 来远程管理服务器 · HonKit Linux 下无限试用 JetBrains · HonKit 使用 KMS 激活 Windows · HonKit 为 KVM 中的 Windows 虚拟机启用 VirtIO · HonKit 在 Spring Boot 中处理 MissingKotlinParameterException · HonKit 用 Spring Native 拯救微服务 · HonKit 正确调试 PHP · HonKit Ubuntu 增加最大文件打开数量限制 · HonKit 将 Minecraft 服务端编译到 native · HonKit 用凝聚力购买 P 社游戏 DLC · HonKit 模拟超星网课 Android 客户端 · HonKit 模拟 Bilibili Android 客户端 · HonKit 模拟哔咔 Android 客户端 · HonKit 异步是什么 · HonKit GCP Pub/Sub push 至 IAP 保护的 Endpoint · HonKit 部署 Spring Native 到 GCP App Engine · HonKit 多个 Gradle SubProject 同时编译导致 CI/CD 内存耗尽 · HonKit Google Cloud Build 运行 DIND · HonKit 在 Google Storage 中部署 SPA · HonKit Terraform 不自动更新 CloudRun service · HonKit Telegram Bot 收不到普通群聊消息的问题 · HonKit 加密货币交易 · HonKit 挖掘以太坊 · HonKit 挖掘门罗币 · HonKit 小明学英语 · HonKit 卖程序的小女孩 · HonKit 一个测试工程师走进一家酒吧 · HonKit 我给你们讲个 TCP 笑话吧! · HonKit 我给你们讲个 UDP 笑话吧! · HonKit 格局打开 · HonKit c 语言常见未定义行为 · HonKit 在 Kotlin 使用 SpaceEngineers Remote API · HonKit Kotlin 协程 · HonKit 在 Spring Boot 中正确注册 Jackson Module · HonKit Spring Boot Jpa 使用 Google Cloud SQL · HonKit Spring Boot 中配置单页应用 · HonKit 正确打包 Spring Boot 到 war · HonKit czp's blog · HonKit 鸣谢列表 · HonKit 友链 · HonKit
Spring Boot 无法加载 ClasspathResource 问题 · HonKit
2026-04-11 · via czp's blog

本文撰写时的 Spring 版本: Spring Boot 2.0.5.RELEASE

一般来说, 程序运行所必须的资源文件我们会一起打包到 jar. 那么接下去我们就要读取这个资源文件.

假定我们的资源文件在源码目录中为 src/main/resources/myFile.txt

Spring Boot 中读取存放在 classpath 的资源文件通常是这么做的

val myFile = ClasspathResource("/myFile.txt")

(不同于 Class.getResourceAsStream(name:String), ClasspathResource 的参数可以去除第一个 /)

然后我们读取这个文件的内容通常是先获取他的流

myFile.inputStream

这好像没有什么问题, 文件也正常的被读取了.

一些第三方库可能要求传入一个 File 类型.

myFile.file

看起来也没有问题, 并且在 IDEA 里运行的时候确实没有问题. 直到 CI 测试的时候, 就会有这么一个异常

java.io.FileNotFoundException: class path resource [myFile.txt] cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/app.jar!/BOOT-INF/classes!/myFile.txt

在集成环境和生产环境上, 我们的程序是一个 jar 包而不是 exploded 方式, 也就是说, 此时的 Resource.getFile() 将有不一样的行为.

我们在 IDEA 调试的时候, 资源文件是存在于真实文件系统里的一个文件. 而在 jar 包中, 它不是一个真实文件系统的文件.

为了能用统一的文件系统路径去表示 jar 内的文件, Java 开创了 ! 这个符号.

! 表示这个文件是一个压缩包(zip)(jar 本身就是一个 zip), 之后的路径则为压缩包内的路径(压缩包内的路径不分运行平台, 统一为 Unix 路径).

正常情况下的 getFile() 操作, 会得到一个 jar 包路径后面加上一个 ! 号然后再拼接上包内路径的一个路径.

Spring Boot 为了避免资源文件冲突(Java 的打包规范忽略了资源文件的问题, 两个库的代码文件是可以合并的, 因为包名不同. 但是资源文件都从 jar 的根目录开始编排, 如果重名将互相覆盖而导致打包后资源文件的丢失)而采用 fat-jar 的方式来打包程序.

fat-jar 就是一种 nested jar, 所有的依赖库不会合并到用户代码上, 而是以 jar 包的形式存放在 jar 包内.

一个典型的 Spring Boot 程序打包后差不多是这样的

META-INF
    MANIFEST.MF
org
    springframework
        boot
            loader
                (Launcher)
BOOT-INF
    classes
        (user code)
    lib
        dependence.jar

jar 的入口类其实是 Spring Boot Launcher, 他会为每一个依赖创建一个 ClassLoader, 这样就可以让每个依赖自己读取自己的资源文件而互不冲突.

而用户自己的类是从 /BOOT-INF/classes 开始的, 用户自己的资源文件的根目录也在这里, 所以为了让用户能够正确读到自己的资源文件. 加载用户代码的那个 ClassLoaderclasspath 从这里开始.

fat-jar 并不是 Java 官方标准, 所以 Java 认为所有 classpath 都是从 jar 的根目录开始的.

于是我们得到的文件路径, 将是 {用户代码根目录}!/{资源文件路径}

而用户代码根目录本身就是在 jar 内的, 最终我们会得到这么一个路径

jar:file:/app.jar!/BOOT-INF/classes!/myFile.txt

(注意, 有两个 ! 号)

没错, classes 文件夹被认为是一个压缩包了.

所以我们将找不到这个文件.

如果读取资源文件的操作只在自己的代码发生, 那么只要不使用 Resource.getFile() 而直接获取流就可以避免这个问题. 但是很多情况下, 并非自己要读文件, 而是第三方库要读文件.

例如第三方库可能会要求在配置文件中配置 key 文件的路径, 而这个路径支持网络读取, 所以必须是 URI. 然后第三方库的代码中就会使用 Resource.getFile() 来把这个地址转成 File 类型再去读他.

这么一读, 就抛出异常了.

那么, 怎么办呢.

我们找到了这么一个库 https://github.com/ulisesbocchio/spring-boot-jar-resources

他的功能是通过自定义的 ResourceLoader, 当 Spring Boot 需要读取文件时, 首先判断这个文件是不是存在于 classpath 中, 如果是, 则解压这个文件到临时目录(真实文件系统上), 然后返回文件系统路径.

使用非常简单, 首先加入依赖

// https://mvnrepository.com/artifact/com.github.ulisesbocchio/spring-boot-jar-resources
compile group: 'com.github.ulisesbocchio', name: 'spring-boot-jar-resources', version: '1.3'

然后把程序入口改成这样就行了

@SpringBootApplication
open class Application

fun main() {
    runApplication<Application> {
        resourceLoader = JarResourceLoader()
    }
}

我们再使用 ApplicationContext.getResource() 时, 返回的就不是 ClasspathResource 了, 而是 JarResource, 路径在一个临时目录下(Linux 下默认为 /tmp/**)

这样, 我们就可以让第三方库正常工作了.