使用Spring Boot Maven Plugin无法访问Jar包内资源的问题
我使用的某库将模板文件存储在resources/vc-templates/*.json路径下,通过以下代码访问模板:
checkNotNull(resource) { "Cannot find resource path: $resourcePath" } log.debug { "Loading templates from: $resource" } when { File(resource.file).isDirectory -> File(resource.file).walk().filter { it.isFile }.map { (it.relativeTo(File(resource.file)).parent?.let { "$it/" } ?: "") + it.nameWithoutExtension }.toList() else -> { FileSystems.newFileSystem(resource.toURI(), emptyMap<String, String>()).use { fs -> Files.walk(fs.getPath(resourcePath)) .filter { it.isRegularFile() } .map { (Path.of(it.toString()).relativeTo(Path.of(resourcePath)).parent?.let { "$it/" } ?: "") + it.nameWithoutExtension }.toList() } } }
本地执行./mvnw spring-boot:run运行一切正常,但执行./mvnw clean package打包成Jar部署时,出现找不到/vc-templates文件夹的错误,报错信息如下:
Caused by: java.nio.file.NoSuchFileException: /vc-templates at jdk.zipfs/jdk.nio.zipfs.ZipPath.readAttributes(ZipPath.java:769) ~[jdk.zipfs:na] at jdk.zipfs/jdk.nio.zipfs.ZipPath.readAttributes(ZipPath.java:777) ~[jdk.zipfs:na] at jdk.zipfs/jdk.nio.zipfs.ZipFileSystemProvider.readAttributes(ZipFileSystemProvider.java:276) ~[jdk.zipfs:na] at java.base/java.nio.file.Files.readAttributes(Files.java:1851) ~[na:na] at java.base/java.nio.file.FileTreeWalker.getAttributes(FileTreeWalker.java:220) ~[na:na] at java.base/java.nio.file.FileTreeWalker.visit(FileTreeWalker.java:277) ~[na:na] at java.base/java.nio.file.FileTreeWalker.walk(FileTreeWalker.java:323) ~[na:na] at java.base/java.nio.file.FileTreeIterator.<init>(FileTreeIterator.java:71) ~[na:na] at java.base/java.nio.file.Files.walk(Files.java:3918) ~[na:na] at java.base/java.nio.file.Files.walk(Files.java:3973) ~[na:na] at id.walt.credentials.w3c.templates.VcTemplateService$resourceWalk$1.invoke(VcTemplateService.kt:124) ~[waltid-ssikit-1.2306281832.0.jar!/:na] at id.walt.credentials.w3c.templates.VcTemplateService$resourceWalk$1.invoke(VcTemplateService.kt:109) ~[waltid-ssikit-1.2306281832.0.jar!/:na] at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74) ~[kotlin-stdlib-1.8.22.jar!/:1.8.22-release-407(1.8.22)] at id.walt.credentials.w3c.templates.VcTemplateService.listResources(VcTemplateService.kt:134) ~[waltid-ssikit-1.2306281832.0.jar!/:na] at id.walt.credentials.w3c.templates.VcTemplateService.listTemplates(VcTemplateService.kt:149) ~[waltid-ssikit-1.2306281832.0.jar!/:na]
代码日志输出:Loading templates from: jar:file:/Path/to/*.jar!/BOOT-INF/classes!/vc-templates,我疑惑classes后的感叹号是否导致文件夹无法被找到?有没有解决办法?另外想了解javaClass.getResource()在Jar文件中的工作机制。此外,打包生成的*.jar.original执行时提示:no main manifest attribute, in *.jar.original。
1. classes后的感叹号不是问题根源
感叹号是Jar URL的标准格式,用于分隔Jar文件路径和内部资源路径。Spring Boot打包的Jar是嵌套结构,BOOT-INF/classes是应用代码和资源的存放目录,因此出现两个!是正常的,不会导致资源找不到。
2. 找不到vc-templates的解决办法
问题出在Jar模式下的路径处理:代码中直接用fs.getPath(resourcePath),但Spring Boot打包后资源实际位于BOOT-INF/classes/vc-templates,而不是Jar根目录的/vc-templates,导致路径匹配失败。
方案一:用ClassLoader枚举Jar内资源
checkNotNull(resource) { "Cannot find resource path: $resourcePath" } log.debug { "Loading templates from: $resource" } val templateNames = mutableListOf<String>() when { File(resource.file).isDirectory -> { // 保留本地运行时的原有逻辑 File(resource.file).walk().filter { it.isFile }.map { (it.relativeTo(File(resource.file)).parent?.let { "$it/" } ?: "") + it.nameWithoutExtension }.toList() } else -> { val classLoader = javaClass.classLoader val targetPath = resourcePath.removePrefix("/") val jarUri = resource.toURI() if (jarUri.scheme == "jar") { // 提取Jar文件路径 val jarPath = jarUri.path.split("!")[0].substring(5) JarFile(jarPath).use { jarFile -> jarFile.entries().asSequence() .filter { !it.isDirectory && it.name.startsWith(targetPath) } .forEach { entry -> // 计算相对路径与模板名称 val relativeEntryPath = entry.name.substring(targetPath.length + 1) val name = if (relativeEntryPath.contains("/")) { relativeEntryPath.substringBeforeLast("/") + "/" + entry.name.substringAfterLast("/").substringBeforeLast(".") } else { entry.name.substringAfterLast("/").substringBeforeLast(".") } templateNames.add(name) } } } templateNames } }
方案二:用Spring内置工具简化处理(推荐)
如果是Spring Boot项目,直接用PathMatchingResourcePatternResolver自动处理本地/Jar资源:
val resolver = PathMatchingResourcePatternResolver() val resources = resolver.getResources("classpath:vc-templates/**/*.json") val templateNames = resources.map { resource -> val path = resource.url.path val relativePath = path.substring(path.indexOf("vc-templates/") + "vc-templates/".length) relativePath.substringBeforeLast(".json") }
3. javaClass.getResource()在Jar中的工作机制
- 本地运行时:资源是文件系统中的真实文件/目录,
getResource()返回file:协议URL,可直接用File类访问。 - Jar运行时:资源是Jar内部的条目,返回
jar:协议URL(格式为jar:file:/xxx.jar!/内部路径),无法直接用File访问,必须通过JarFile或ZipFileSystem读取。 - 加载逻辑:
getResource()通过当前类的ClassLoader从类路径查找资源,Spring Boot打包后类路径包含BOOT-INF/classes和BOOT-INF/lib下的依赖Jar,资源会从这些位置检索。
4. *.jar.original无法运行的原因
*.jar.original是Maven打包时生成的原始Jar文件,仅包含项目编译后的class和资源,没有Spring Boot的启动配置(如MANIFEST.MF中的Main-Class属性)和依赖包,因此无法直接运行。正常运行应使用不带.original后缀的Spring Boot可执行Jar,该Jar包含所有依赖和启动逻辑。
内容的提问来源于stack exchange,提问作者cheekz

