从传统Jar到Java模块:手把手教你用Gradle Java Library插件构建真正的模块化库
从传统Jar到Java模块:手把手教你用Gradle Java Library插件构建真正的模块化库
当Java 9引入模块系统(JPMS)时,许多开发者将其视为解决"Jar地狱"的终极方案。五年过去了,模块化开发正从可选变成必选——特别是对于需要被广泛复用的库开发者。本文将带你完整走过一个传统Java库的模块化改造之旅,从module-info.java的创建到处理棘手的非模块化依赖,最终发布符合JPMS规范的模块化Jar。
1. 模块化改造的起点:项目基础配置
在开始模块化改造前,我们需要确保项目基础配置正确。使用Gradle 7.x或更高版本,在build.gradle中应用java-library插件:
plugins { id 'java-library' } group = 'com.example' version = '1.0.0' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }关键配置说明:
- java-library插件:提供API/实现分离等库开发专属功能
- Java 17工具链:推荐使用LTS版本作为模块化开发的基础
- 明确group/version:这对后续模块命名和发布至关重要
提示:在改造前确保所有单元测试通过,这能帮助快速发现模块化引入的问题
2. 创建模块描述符:module-info.java实战
模块化的核心是module-info.java文件,它需要放置在src/main/java目录下。假设我们的库提供HTTP客户端功能,模块声明可能如下:
module com.example.httpclient { // 导出公共API包 exports com.example.httpclient.core; exports com.example.httpclient.spi; // 服务提供接口 provides com.example.httpclient.spi.HttpAdapter with com.example.httpclient.core.DefaultHttpAdapter; // 依赖声明 requires transitive org.apache.httpcomponents.httpclient; requires java.logging; requires static com.google.code.findbugs; }模块声明要点解析:
| 指令类型 | 作用 | Gradle对应配置 |
|---|---|---|
requires | 必需依赖 | implementation |
requires transitive | 传递性API依赖 | api |
requires static | 编译时可选依赖 | compileOnly |
exports | 公开API包 | 无直接对应 |
provides...with | 服务提供声明 | 需手动配置 |
常见陷阱:
- 未导出的包对模块外完全不可见(包括反射)
- 模块名称应该与
build.gradle中的group+主包名保持一致 requires static依赖在运行时可能引发NoClassDefFoundError
3. 处理非模块化依赖的三种策略
现实项目中,我们常遇到没有模块信息的传统Jar。以下是处理这类依赖的系统性方案:
3.1 自动模块(Automatic Module)
当依赖的Jar包含Automatic-Module-Name清单属性时,Gradle会自动将其视为模块。我们可以通过修改构建脚本确保兼容:
dependencies { // 标准模块化依赖 api 'org.apache.httpcomponents:httpclient:4.5.13' // 自动模块依赖 implementation 'org.apache.commons:commons-lang3:3.12.0' // 传统Jar(无模块信息) implementation 'commons-cli:commons-cli:1.5.0' }对应的module-info.java需要区别声明:
module com.example.httpclient { requires org.apache.httpcomponents.httpclient; // 标准模块 requires org.apache.commons.lang3; // 自动模块 // commons-cli无法直接require }3.2 传统Jar的模块化包装
对于完全没有模块信息的Jar,可以创建适配器模块:
- 新建子项目
commons-cli-wrapper - 添加Jar清单属性:
jar { manifest { attributes('Automatic-Module-Name': 'com.example.commons.cli') } }- 主项目通过
requires com.example.commons.cli引用
3.3 模块路径与类路径混合模式
当无法修改依赖时,可通过Gradle配置控制路径分配:
tasks.named('compileJava') { modularity.inferModulePath = false // 禁用自动模块推断 options.compilerArgs += [ '--module-path', classpath.filter { it.name.contains('httpclient') }.asPath, '--class-path', classpath.filter { !it.name.contains('httpclient') }.asPath ] }4. 构建配置的深度调优
模块化构建需要特别注意以下配置项:
4.1 多项目构建的模块关系
对于多模块项目,settings.gradle中应明确模块结构:
rootProject.name = 'http-suite' include 'http-client-core' include 'http-client-extension'子项目间依赖需使用api或implementation:
// http-client-extension/build.gradle dependencies { api project(':http-client-core') // 暴露给消费者 }对应的模块声明需体现这种关系:
// http-client-extension的module-info.java module com.example.httpclient.extension { requires transitive com.example.httpclient.core; exports com.example.httpclient.extension.features; }4.2 测试环境的特殊处理
测试代码通常需要访问内部实现,可通过opens指令开放访问:
module com.example.httpclient { // 对测试模块开放内部实现 opens com.example.httpclient.internal to org.junit.platform.commons; }Gradle测试配置也需要相应调整:
tasks.named('test') { useJUnitPlatform() modularity.inferModulePath = false // 测试使用类路径 }4.3 发布模块化Jar的注意事项
确保发布的Jar包含模块信息:
publishing { publications { mavenJava(MavenPublication) { from components.java pom { // 显式声明模块特性 properties.put('module.name', project.javaModule.name) } } } }关键发布检查点:
- 使用
./gradlew publishToMavenLocal本地验证 - 检查生成的
module-info.class是否包含在Jar中 - 确认POM文件中的依赖范围正确(api→compile,implementation→runtime)
5. 模块化开发的进阶技巧
5.1 条件化模块描述符
对于需要同时支持模块化和传统模式的项目,可以使用资源过滤:
processResources { filesMatching('module-info.java') { filter { line -> project.hasProperty('disableModules') ? '//' + line : line } } }5.2 模块化与OSGi的兼容方案
如果需要同时支持JPMS和OSGi,可以配置bnd工具:
plugins { id 'biz.aQute.bnd.builder' version '6.4.0' } jar { bnd('Bundle-SymbolicName': project.javaModule.name, 'Bundle-Version': project.version, 'Export-Package': 'com.example.httpclient.*;-noimport:=true') }5.3 模块化库的性能优化
模块化可以带来启动性能提升,通过jlink创建自定义运行时:
tasks.register('customRuntime', Exec) { commandLine 'jlink', '--module-path', "${java.modularity.inferModulePath.get() ? configurations.runtimeClasspath.asPath : ''}", '--add-modules', 'com.example.httpclient', '--output', 'build/runtime' }实际项目中,模块化改造最大的挑战往往不是技术实现,而是依赖生态的碎片化。我曾在一个金融项目中遇到需要同时使用5个不同状态的JSON库(全模块化、自动模块、传统Jar),最终通过创建适配层解决了兼容性问题。
