Skip to content

📦 模块化与包管理(JPMS、Maven、Gradle 深入)

本章覆盖 Java 9 引入的模块系统(JPMS)的动机、核心概念、实践模式与迁移策略,并系统梳理 Maven/Gradle 的依赖管理、版本对齐与冲突消解方法,帮助你构建稳定、可维护的大中型 Java 项目。

1. 为什么需要模块化(JPMS)

  • 明确的边界与封装:只导出需要被使用的 API 包,隐藏内部实现。
  • 减少类路径地狱(Class-Path Hell):可声明依赖关系、检测循环依赖与缺失。
  • 更好的启动与运行时图谱:支持 jlink 定制最小运行时镜像。
  • 安全与可维护:opens/exports 区分反射可见性,限制随意访问。

2. 模块基础:module-info.java

2.1 基本语法

java
module com.example.app {
    requires java.base;       // 隐式依赖,可省略
    requires java.sql;        // 声明依赖

    exports com.example.app.api;        // 导出 API 包供外部使用
    exports com.example.app.util;       // 可导出多个包

    // 仅对特定模块开放反射(JPA/Hibernate 等常见)
    opens com.example.app.entity to org.hibernate.orm.core;

    // 服务提供与消费(SPI)
    uses com.example.spi.PaymentService;
    provides com.example.spi.PaymentService
        with com.example.impl.WechatPaymentService;
}

2.2 重要关键字

  • exports:编译期与运行期均对外可见(编译依赖 & 访问)。
  • opens:仅在运行期对反射开放(不提供编译期可见性)。
  • requires transitive:传递依赖,A→B(transitive),则依赖 A 的模块自动读取 B。
  • requires static:仅编译期依赖,运行时可选(常用于可选功能)。

2.3 可读性与可访问性

  • 模块图决定“可读性”(readability),exports/opens 决定“可访问性”(accessibility)。
  • 通过 java --describe-module com.example.app 查看模块信息。

3. 模块化常见结构与陷阱

3.1 自动模块与未命名模块

  • 自动模块:将类路径上的 JAR 放到模块路径(--module-path),其名称来源于 JAR 文件名。
  • 未命名模块:仍在类路径(-classpath)上的代码,视为未命名模块,能读取所有命名模块,但被强封装模块读取受限。

建议尽量迁移到“全模块路径”,减少自动模块的不可预测性。

3.2 Split Package(包拆分)问题

  • JPMS 不允许同名包来自多个模块(与 OSGi 类似),须合并或重命名。
  • 典型于多模块项目复用 com.example.common 包时需小心。

3.3 反射框架兼容

  • JPA/Hibernate、Jackson、Spring 在运行期需要反射访问非公开成员,需在 module-info.java 使用 opensopen module(对所有包开放反射,极不推荐)。

4. 模块工具链:编译、运行、打包

4.1 编译与运行

bash
# 编译(模块路径)
javac --module-path libs -d out $(find src -name "*.java")

# 运行
java  --module-path out:libs --module com.example.app/com.example.app.Main
bash
jlink \
  --module-path $JAVA_HOME/jmods:out \
  --add-modules com.example.app \
  --output runtime-image \
  --strip-debug --compress=2 --no-header-files --no-man-pages

生成仅包含所需模块的精简运行时,显著降低镜像大小与启动时间。

4.3 jmod 与多模块布局

  • jmod 可封装本地库与配置;多模块项目建议统一 src/<module>/ 目录结构。

5. 与主流框架协作(Spring、Hibernate、Jackson)

5.1 Spring

  • Spring 6+/Boot 3+ 基于 Jakarta EE 命名空间,模块兼容更好。
  • 对反射访问 Bean 需要:
java
module com.example.app {
    opens com.example.app.config to spring.core; // 仅对 Spring 反射开放
    opens com.example.app.bean   to spring.beans, spring.core;
}

5.2 JPA/Hibernate

java
module com.example.app {
    opens com.example.app.entity to org.hibernate.orm.core;
}

5.3 Jackson

java
module com.example.app {
    opens com.example.app.dto to com.fasterxml.jackson.databind;
}

6. 迁移策略:从类路径到模块路径

  1. 先清理 Split Package,归并共享包到独立模块。
  2. 为每个模块补充 module-info.java,明确 exportsrequires
  3. 检查反射需求,按需 opens 到具体模块,避免 open module
  4. 逐步将依赖从类路径迁至模块路径,减少自动模块。
  5. 最后使用 jlink 优化分发镜像与启动。

7. Maven 依赖管理(深入)

7.1 依赖范围与传递

  • compile(默认)、providedruntimetestsystemimport
  • 传递依赖通过最近优先与最短路径原则解析。

7.2 版本对齐与 BOM(Bill of Materials)

xml
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>3.3.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
  • 上述方式统一管理版本,子依赖无需显式 version,避免冲突。

7.3 依赖冲突与排除

xml
<dependencies>
  <dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
    <exclusions>
      <exclusion>
        <groupId>org.reactivestreams</groupId>
        <artifactId>reactive-streams</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
</dependencies>

7.4 可重复构建与锁定

  • 使用 Maven Resolver、maven-dependency-plugin 生成树,结合私有仓库锁定版本。

7.5 多模块 Maven 与 JPMS 协同

  • 每个 Maven 模块对标一个 Java 模块;使用 maven-compiler-plugin 设置 --release--module-path
xml
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>3.11.0</version>
  <configuration>
    <release>17</release>
    <compilerArgs>
      <arg>--module-path</arg>
      <arg>${project.build.outputDirectory}</arg>
    </compilerArgs>
  </configuration>
</plugin>

8. Gradle 依赖管理(深入)

8.1 配置与依赖范围

  • 常见配置:implementationapicompileOnlyruntimeOnlytestImplementation
  • api 会将依赖暴露给下游模块(类似 requires transitive),implementation 不会。

8.2 版本对齐与平台(platform)

gradle
dependencies {
    implementation platform('org.springframework.boot:spring-boot-dependencies:3.3.0')
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

8.3 依赖解析策略与锁定

gradle
configurations.all {
    resolutionStrategy {
        failOnVersionConflict()
        force 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
    }
}

dependencyLocking {
    lockAllConfigurations()
}

8.4 JPMS 与 Gradle

  • 使用 modularity 插件或在 tasks.compileJava 中添加 --module-path;多模块工程可借助 java-library 插件区分 apiimplementation

9. 打包与分发

9.1 可执行 JAR(fat/uber JAR)与模块化

  • spring-boot-maven-plugin/shadowJar 打包 fat JAR 便捷,但与 JPMS 双亲委派与包结构可能冲突。
  • 模块化项目生产推荐:常规分层镜像 + jlink 精简 JRE,或使用容器镜像多阶段构建。

9.2 容器与镜像体积优化

  • JDK 17 alpine 基础镜像 + jlink 输出的自定义 runtime,可显著减小镜像体积与冷启动时间。

10. 实战:从传统多模块到 JPMS 的最小改造

  1. commondomainapp 三个 Maven 模块添加 module-info.java
java
// common 模块
module com.example.common {
    exports com.example.common.util;
}

// domain 模块
module com.example.domain {
    requires transitive com.example.common; // 下游自动可读 common
    exports com.example.domain.model;
    opens com.example.domain.entity to org.hibernate.orm.core;
}

// app 模块
module com.example.app {
    requires com.example.domain; // 通过 domain 间接获得 common
    requires spring.context;
    opens com.example.app.config to spring.core, spring.beans;
}
  1. 修复 Split Package,将重复包合并到 common
  2. 对反射需求最小化 opens 范围到具体包与模块。
  3. 使用 mvn -DskipTests package 验证,再切换 --module-path 运行。
  4. 最后引入 jlink 生成运行时镜像供生产分发。

11. 常见问题与排查

  • java.lang.LayerInstantiationException:模块层加载冲突,检查模块路径与版本。
  • java.lang.IllegalAccessError:未导出/未开放的包被访问或反射。
  • 服务加载失败(SPI):忘记在 module-info.javauses/provides
  • Split Package:合并或重命名,避免同包多模块。

12. 最佳实践清单

  • 内聚:一个 Maven 模块对应一个 Java 模块;公共包单独模块化。
  • 最小暴露原则:exports 只导出 API 包;运行期反射用 opens 精准授权。
  • 统一版本:Maven BOM 或 Gradle platform 对齐;锁定依赖避免漂移。
  • 构建可复制:本地/CI 使用相同仓库与锁定策略;产物可追溯。
  • 分发优化:结合 jlink 与精简镜像,缩短冷启动、提升可观测性。

13. 参考配置模板

13.1 Maven(片段)

xml
<properties>
  <maven.compiler.release>17</maven.compiler.release>
</properties>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.11.0</version>
      <configuration>
        <release>${maven.compiler.release}</release>
      </configuration>
    </plugin>
  </plugins>
  <pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
        <version>3.6.1</version>
      </plugin>
    </plugins>
  </pluginManagement>
</build>

13.2 Gradle(片段)

gradle
plugins {
    id 'java-library'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

dependencies {
    implementation platform('org.springframework.boot:spring-boot-dependencies:3.3.0')
    api 'com.fasterxml.jackson.core:jackson-annotations'
    implementation 'com.fasterxml.jackson.core:jackson-databind'
}

掌握 JPMS 能让大型项目的边界更清晰、依赖更可控;熟练使用 Maven/Gradle 的版本平台与冲突策略,能显著降低“依赖地震”的风险。建议与“JVM 深入与性能调优”、“并发编程高级”等章节组合练习与实践。