引用可执行Jar子目录内Jar类时出现ClassNotFoundException
问题描述
现有两个基于Gradle的Java项目,均需打包为.jar文件:
1. Core项目
- 类型为
java-library - 打包后的Jar包含项目自身的
.class文件,内部lib目录存放所有第三方依赖.jar文件 - Jar任务配置如下:
tasks.getByName<Jar>("jar") { enabled = true archiveClassifier.value("api") from(sourceSets.main.get().output) into("lib") { from(configurations.runtimeClasspath) } duplicatesStrategy = DuplicatesStrategy.EXCLUDE }
2. Import项目
- 用于命令行执行
- 打包后的Jar包含自身
.class文件,内部lib目录存放第三方依赖.jar文件(与core.jar://lib/*.jar一致) - 构建脚本中依赖Core的方式:
dependencies { implementation group: 'com.company.core', name: 'core', version: '0.1.0-SNAPSHOT', classifier: 'api' }
- Jar任务配置如下:
jar { zip64 = true manifest { attributes 'Main-Class': 'com.import.Runner' } from { duplicatesStrategy(DuplicatesStrategy.INCLUDE) configurations.runtimeClasspath.collect { if (it.isDirectory()) { println "Directory: $it" // 从未打印 it } else { println "Zip Tree: $it" // 会打印,显示本地maven目录中的jar路径 zipTree(it) } } } exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' }
执行异常
执行Import的Jar时,尝试以下命令:
java -cp import.jar com.import.Runner // 也曾尝试: java -cp "import.jar:lib/*" com.import.Runner
均出现如下异常:
Exception in thread "main" java.lang.NoClassDefFoundError: com/company/exception/ApplicationException at com.import.ImportClient.<init>(ImportClient.java:24) Caused by: java.lang.ClassNotFoundException: com.company.exception.ApplicationException
疑问
- JVM为何无法加载可执行Jar内部
lib目录下第三方Jar中的类? - 指定
-cp "import.jar:lib/*"为何对Jar内部的lib目录不生效?
问题解答
核心原因
JVM的类加载器默认不会识别Jar文件内部嵌套的Jar包:
- 当你把依赖Jar打包到
import.jar的lib目录下时,这些嵌套Jar只是主Jar里的普通文件,JVM不会自动扫描并加载其中的类。 - 你尝试的
-cp "import.jar:lib/*"中的lib/*,指向的是当前操作系统目录下的lib文件夹,而非import.jar内部的lib目录,因此这个参数完全不会作用到主Jar里的嵌套依赖。
解决方案
方案1:使用Gradle Shadow插件(推荐)
Shadow插件可以自动处理嵌套依赖,生成可直接运行的Fat Jar,或保留嵌套结构并配置类路径:
- 在Import项目的
build.gradle中引入插件:
plugins { id 'java' id 'com.github.johnrengelman.shadow' version '8.1.1' // 可替换为最新版本 }
- 配置ShadowJar任务:
shadowJar { zip64 = true manifest { attributes 'Main-Class': 'com.import.Runner' // 配置类路径指向内部lib目录的依赖 attributes 'Class-Path': configurations.runtimeClasspath.files.collect { "lib/${it.name}" }.join(' ') } // 将依赖复制到Jar内部的lib目录 into('lib') { from configurations.runtimeClasspath } exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' duplicatesStrategy = DuplicatesStrategy.EXCLUDE }
- 生成并运行Jar:
# 生成可执行Jar ./gradlew shadowJar # 运行Jar java -jar build/libs/import-<version>-all.jar
方案2:手动配置Manifest的Class-Path
若不想用插件,可修改Import的Jar任务,在Manifest中显式声明内部依赖路径:
jar { zip64 = true manifest { attributes 'Main-Class': 'com.import.Runner' // 生成Class-Path属性,指向Jar内部lib目录的所有依赖 attributes 'Class-Path': configurations.runtimeClasspath.files.collect { "lib/${it.name}" }.join(' ') } // 将依赖复制到Jar内部的lib目录 into('lib') { from configurations.runtimeClasspath } exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' duplicatesStrategy = DuplicatesStrategy.EXCLUDE }
注意:这种方式下,import.jar必须和内部lib目录的结构保持一致(不能单独移动Jar文件),否则相对路径会失效。
方案3:合并所有类到主Jar中
修改Import的Jar任务,仅提取依赖中的类(排除Core Jar自带的lib目录),合并到主Jar:
jar { zip64 = true manifest { attributes 'Main-Class': 'com.import.Runner' } from { duplicatesStrategy(DuplicatesStrategy.EXCLUDE) configurations.runtimeClasspath.collect { if (it.name.contains('core-api')) { // 只取Core Jar中的自身类,排除它的lib目录 zipTree(it).matching { exclude 'lib/**' } } else { zipTree(it) } } } exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' }
生成Jar后,直接用java -cp import.jar com.import.Runner即可运行。
内容的提问来源于stack exchange,提问作者bobbyrne01
相关产品推荐
相关产品推荐

