模块化概述(jdk 9 jep 261)
什么是jpms
java platform module system(jpms),又称project jigsaw,是jdk 9引入的模块化系统(jep 261)。它为java平台提供了模块化的代码组织结构,实现了强封装和可靠的依赖管理。
// 模块化前:所有代码在一个巨大的classpath中
// 模块化后:代码被组织为独立的模块
// module-info.java - 模块描述符
module com.example.myapp {
requires java.sql;
requires com.example.common;
exports com.example.myapp.api;
opens com.example.myapp.entity to org.hibernate.orm.core;
}
模块化的核心目标
| 目标 | 说明 |
|---|---|
| 可靠配置 | 编译期检测依赖缺失,替代运行时classnotfoundexception |
| 强封装 | 包级别访问控制,未导出的包外部完全不可见 |
| 安全平台 | 限制反射访问,保护jdk内部api |
| 可裁剪 | 通过jlink构建仅包含所需模块的运行时 |
| 性能提升 | 更小的运行时镜像,更快的类加载 |
jdk模块化演进时间线
2008年 - project jigsaw立项 2014年 - jdk 8(原计划引入,推迟) 2017年 - jdk 9 正式发布jpms(jep 261) 2018年 - jdk 11 移除java ee和corba模块 2020年 - jdk 16 默认强封装(--illegal-access=deny) 2022年 - jdk 17 移除--illegal-access选项 2023年 - jdk 21 虚拟线程与模块化协同
为什么需要模块化(classpath hell)
classpath hell问题
在模块化之前,java应用面临严重的类路径问题:
// 问题1:类冲突 - 两个jar包含同名类 // lib/guava-20.jar → com.google.common.collect.immutablelist // lib/guava-31.jar → com.google.common.collect.immutablelist // 运行时加载哪个?取决于classpath顺序! // 问题2:隐式依赖 - 编译通过但运行时失败 import com.fasterxml.jackson.databind.objectmapper; // 编译时存在 // 部署时忘记包含jackson-databind.jar → noclassdeffounderror // 问题3:无封装 - 所有public类全局可见 // 即使标注了@internalapi,任何代码都能访问 import sun.misc.unsafe; // jdk内部api,不应被使用但无法阻止
巨型jdk问题
jdk 8的rt.jar: ~66mb,包含所有核心类 - 一个"hello world"也需要完整jre(~180mb) - 无法按需裁剪 - iot/容器场景部署体积过大 模块化后(jdk 9+): - jdk被拆分为~70个模块 - 可以只选择需要的模块 - jlink定制jre可小至~30mb
安全性问题
// jdk 8: 反射可以突破任何封装
field field = string.class.getdeclaredfield("value");
field.setaccessible(true); // 总是成功
byte[] value = (byte[]) field.get("hello");
// jdk 9+: 模块化限制反射访问
// 对未开放的包,setaccessible(true) 抛出 inaccessibleobjectexception
// 除非使用 --add-opens 显式开放
模块化解决方案对比
| 问题 | 模块化前 | 模块化后 |
|---|---|---|
| 依赖管理 | 运行时发现缺失 | 编译期/启动期检测 |
| 封装 | public即全局可见 | exports精确控制 |
| 反射安全 | 任意反射 | 仅opens的包可反射 |
| 部署体积 | 完整jre 180mb+ | jlink定制 30-50mb |
| 版本冲突 | classpath顺序决定 | 模块系统检测冲突 |
module-info.java语法
基本结构
// 文件位置: src/main/java/module-info.java
// 每个模块有且仅有一个module-info.java
module com.example.order {
// 依赖声明
requires java.sql;
requires transitive com.example.common;
requires static com.example.annotation;
// 导出声明
exports com.example.order.api;
exports com.example.order.event to com.example.notification;
// 开放声明(运行时反射)
opens com.example.order.entity to org.hibernate.orm.core;
// 服务声明
uses com.example.order.spi.ordervalidator;
provides com.example.order.spi.orderprocessor
with com.example.order.impl.defaultorderprocessor;
}
模块命名规范
// 推荐:反向域名 + 项目名
module com.company.project.module { }
// 示例
module org.apache.commons.lang3 { }
module com.google.guava { }
module io.netty.transport { }
// 避免:
module myapp { } // 太简单,可能冲突
module java.custom { } // 不要以java.开头(jdk保留)
module javax.custom { } // 不要以javax.开头(jdk保留)
模块声明的完整语法
// 普通模块
module com.example.app {
// 模块体
}
// 开放模块(所有包默认开放反射)
open module com.example.legacy {
// 所有包自动opens,无需逐个声明
// 适用于需要大量反射的遗留代码
}
// 模块注释(javadoc风格)
/**
* 订单处理模块
*
* @since 2.0
*/
module com.example.order {
// ...
}
requires/exports/opens/uses/provides详解
requires(依赖声明)
module com.example.app {
// 基本依赖:编译期+运行期都需要
requires java.sql;
// 传递性依赖:依赖此模块的模块也能读取java.logging
requires transitive java.logging;
// 可选依赖:仅编译期需要,运行期可不存在
requires static com.example.optional.plugin;
// 传递+可选(较少使用)
requires transitive static com.example.compile.only;
}
requires的语义规则:
// 可读性(readability):
// 模块a requires 模块b → a能读取b导出的包
// 这是编译和运行的前提
// 隐式依赖:所有模块自动requires java.base
module com.example.app {
// 无需写 requires java.base;(自动拥有)
// java.lang.string, java.util.list等直接可用
}
// 循环依赖检测:
// module a requires b; module b requires a; → 编译错误!
// 必须重构消除循环
exports(导出包)
module com.example.library {
// 导出给所有模块
exports com.example.library.api;
exports com.example.library.model;
// 限定导出:仅对指定模块可见
exports com.example.library.internal to com.example.app;
exports com.example.library.spi to com.example.plugin.a, com.example.plugin.b;
// 未导出的包 → 外部完全不可见(编译错误+运行时错误)
// com.example.library.impl 不导出 → 外部无法import
// com.example.library.util 不导出 → 外部无法import
}
exports vs public的区别:
// 包 com.example.library.api(已导出)
package com.example.library.api;
public class orderservice { } // 外部可见 ✓
class internalhelper { } // 外部不可见(包私有)✗
// 包 com.example.library.impl(未导出)
package com.example.library.impl;
public class orderserviceimpl { } // 外部不可见!即使是public ✗
// 模块化后:public + exported 才真正可见
opens(开放包)
module com.example.app {
// 开放给所有模块(运行时深度反射可访问)
opens com.example.app.entity;
// 限定开放:仅对指定模块允许反射
opens com.example.app.dto to com.fasterxml.jackson.databind;
opens com.example.app.entity to org.hibernate.orm.core;
}
// 整个模块开放
open module com.example.legacy {
// 所有包自动开放反射
// 等同于对每个包写 opens xxx;
}
exports vs opens对比:
| 特性 | exports | opens |
|---|---|---|
| 生效时期 | 编译期+运行期 | 仅运行期 |
| 访问方式 | 正常代码引用(import) | 反射(setaccessible) |
| 编译期可见 | 是 | 否 |
| 典型用途 | 公开api | 框架反射(orm/序列化) |
// 实际场景:hibernate需要反射访问实体字段
module com.example.app {
requires org.hibernate.orm.core;
// exports让hibernate编译期能看到实体类
exports com.example.app.entity;
// opens让hibernate运行时能反射访问私有字段
opens com.example.app.entity to org.hibernate.orm.core;
}
uses(服务消费)
// 声明本模块使用某个服务接口
module com.example.app {
// 声明使用ordervalidator服务
uses com.example.order.spi.ordervalidator;
// 运行时通过serviceloader获取实现
// serviceloader.load(ordervalidator.class)
}
provides(服务提供)
// 声明本模块提供某个服务的实现
module com.example.validation {
requires com.example.order; // 需要访问接口定义
// 提供ordervalidator的实现
provides com.example.order.spi.ordervalidator
with com.example.validation.impl.amountvalidator,
com.example.validation.impl.stockvalidator;
}
完整服务化示例
// === 模块1: 服务接口定义 ===
// module-info.java
module com.example.order.spi {
exports com.example.order.spi;
}
// 接口定义
package com.example.order.spi;
public interface ordervalidator {
boolean validate(order order);
string name();
}
// === 模块2: 服务实现 ===
// module-info.java
module com.example.order.validation {
requires com.example.order.spi;
provides com.example.order.spi.ordervalidator
with com.example.order.validation.amountvalidator;
exports com.example.order.validation;
}
// 实现类
package com.example.order.validation;
import com.example.order.spi.ordervalidator;
public class amountvalidator implements ordervalidator {
@override
public boolean validate(order order) {
return order.getamount() > 0;
}
@override
public string name() { return "amountvalidator"; }
}
// === 模块3: 服务消费 ===
// module-info.java
module com.example.order.app {
requires com.example.order.spi;
uses com.example.order.spi.ordervalidator;
}
// 消费代码
package com.example.order.app;
import com.example.order.spi.ordervalidator;
import java.util.serviceloader;
public class orderapplication {
public static void main(string[] args) {
serviceloader<ordervalidator> loader =
serviceloader.load(ordervalidator.class);
for (ordervalidator validator : loader) {
system.out.println("found validator: " + validator.name());
}
}
}
模块层次(java.base等系统模块)
java.base - 基础模块
// java.base 是所有模块的隐式依赖
// 包含的核心包:
// java.lang - object, string, thread, exception
// java.util - list, map, set, collections
// java.io - inputstream, outputstream, file
// java.nio - buffer, channel, path
// java.net - url, socket, inetaddress
// java.math - biginteger, bigdecimal
// java.time - localdate, instant, duration
// java.concurrent - executorservice, completablefuture
// java.security - 安全框架基础
// java.util.function - function, predicate, consumer
// 无需声明,自动拥有:
module com.example.app {
// 不需要 requires java.base;
// string, list, map 等直接可用
}
主要平台模块
# 查看jdk所有模块 java --list-modules # 常见平台模块: java.sql # jdbc, datasource, connection java.xml # dom, sax, stax, xslt java.logging # java.util.logging java.net.http # httpclient (jdk 11+) java.desktop # swing, awt, java2d java.compiler # javax.annotation.processing java.instrument # java agent java.management # jmx java.naming # jndi java.rmi # rmi远程调用 java.scripting # scriptengine java.se # java se聚合模块(包含所有se模块)
模块依赖关系示例
# 查看模块依赖 java --describe-module java.sql # 输出示例: # java.sql@17.0.1 # requires java.base mandated # requires java.logging transitive # requires java.transaction.xa transitive # requires java.xml transitive # exports java.sql # exports javax.sql # uses java.sql.driver
模块层次图
java.base (基础,无依赖) ├── java.logging ├── java.xml │ └── java.sql (依赖 java.base + java.logging + java.xml + java.transaction.xa) ├── java.net.http (依赖 java.base) ├── java.desktop (依赖 java.base + java.logging + java.xml + ...) ├── java.compiler (依赖 java.base) └── java.se (聚合模块,requires transitive 所有se模块)
模块路径 vs 类路径
类路径(classpath)
# 传统方式:所有jar在类路径上 java -cp "lib/*:classes" com.example.main # 特点: # - 扁平结构,无层次 # - 所有public类全局可见 # - 无封装,无依赖检查 # - 向后兼容,所有旧代码可运行
模块路径(module path)
# 模块化方式:模块在模块路径上 java --module-path mods -m com.example.app/com.example.app.main # 或简写 java -p mods -m com.example.app/com.example.app.main # 特点: # - 模块有明确的依赖关系 # - 强封装生效 # - 启动时验证模块图完整性 # - 支持自动模块和未命名模块共存
两者共存规则
# 混合使用:部分模块化,部分传统
java --module-path mods \
--class-path "legacy-libs/*" \
--add-modules com.example.app \
-m com.example.app/com.example.app.main
# 解析优先级:
# 1. 模块路径上的显式模块
# 2. 模块路径上的自动模块
# 3. 类路径上的代码 → 归入未命名模块
对比总结
| 特性 | 类路径 (-cp) | 模块路径 (-p) |
|---|---|---|
| 封装 | 无(public即可见) | 强(需exports) |
| 依赖检查 | 无(运行时发现) | 有(启动时验证) |
| jar要求 | 任意jar | 模块化jar或自动模块 |
| 反射限制 | 无 | 需opens |
| 版本冲突 | 静默覆盖 | 报错 |
| 适用场景 | 遗留代码/兼容 | 新项目/迁移后 |
自动模块(automatic module)
什么是自动模块
// 当一个普通jar(无module-info.class)被放在模块路径上时, // 它自动成为一个"自动模块" // 自动模块的特性: // 1. 导出所有包 // 2. 开放所有包(允许反射) // 3. 可读所有其他模块 // 4. 可被其他模块requires
模块名确定规则
// 规则1(优先):manifest.mf中声明 // meta-inf/manifest.mf: // automatic-module-name: com.google.guava // 规则2:从jar文件名推导 // guava-31.1-jre.jar → 模块名: guava // commons-lang3-3.12.0.jar → 模块名: commons.lang3 // spring-core-5.3.20.jar → 模块名: spring.core // 推导算法: // 1. 去除.jar后缀 // 2. 去除版本号(末尾的数字.数字...部分) // 3. 非字母数字字符替换为点号 // 4. 连续点号合并为一个 // 5. 去除首尾点号
自动模块示例
# 将guava.jar放在模块路径上
java --module-path "mods:lib/guava-31.1-jre.jar" \
-m com.example.app/com.example.app.main
# 在module-info.java中引用自动模块
module com.example.app {
requires com.google.guava; // 使用automatic-module-name
// 或 requires guava; // 使用推导名(无manifest声明时)
}
自动模块的风险
// 风险1:模块名不稳定 // 库升级后文件名变化 → 模块名变化 → requires失败 // 解决:库作者应在manifest.mf声明automatic-module-name // 风险2:拆分包(split package) // 两个自动模块包含相同包名 → 运行时错误 // 例:spring-core和spring-context都包含org.springframework.util // 风险3:过度可见 // 自动模块导出所有包,无法实现最小权限原则
未命名模块(unnamed module)
定义与特性
// 类路径上的所有代码归入"未命名模块" // 每个类加载器有自己的未命名模块 // 特性: // 1. 无名称(不可被requires引用) // 2. 可读所有命名模块 // 3. 导出所有包(对命名模块可见) // 4. 开放所有包(允许反射) // 5. 向后兼容:所有旧代码无需修改即可运行
与命名模块的交互
# 命名模块不可直接requires未命名模块
# 以下写法无效(未命名模块无名称):
# module com.example.app {
# requires ???; // 无法引用未命名模块
# }
# 解决方案:使用 --add-reads
java --module-path mods \
--class-path "legacy.jar" \
--add-reads com.example.app=all-unnamed \
-m com.example.app/com.example.app.main
实际应用场景
// 场景1:测试代码通常在未命名模块中 // 测试框架需要访问所有代码 → 未命名模块的开放性正好满足 // 场景2:动态加载的类 // 通过urlclassloader加载的类归入未命名模块 // 场景3:尚未迁移的第三方库 // 放在classpath上,作为未命名模块运行 // 应用模块通过 --add-reads 访问
服务加载(uses/provides替代spi)
传统spi机制
// 传统方式:meta-inf/services
// 文件: meta-inf/services/com.example.spi.paymentprocessor
// 内容(每行一个实现类全限定名):
// com.example.alipay.alipayprocessor
// com.example.wechat.wechatprocessor
// 加载方式:
serviceloader<paymentprocessor> loader =
serviceloader.load(paymentprocessor.class);
for (paymentprocessor processor : loader) {
processor.pay(amount);
}
// 缺点:
// 1. 无编译期检查(实现类不存在到运行时才发现)
// 2. 配置文件容易遗漏或拼写错误
// 3. 无法限定哪些模块可以提供服务
模块化spi
// === 接口模块 ===
module com.example.payment.spi {
exports com.example.payment.spi;
}
package com.example.payment.spi;
public interface paymentprocessor {
void pay(double amount);
string channel();
}
// === 实现模块 ===
module com.example.payment.alipay {
requires com.example.payment.spi;
provides com.example.payment.spi.paymentprocessor
with com.example.payment.alipay.alipayprocessor;
}
package com.example.payment.alipay;
import com.example.payment.spi.paymentprocessor;
public class alipayprocessor implements paymentprocessor {
@override
public void pay(double amount) {
system.out.println("alipay paying: " + amount);
}
@override
public string channel() { return "alipay"; }
}
// === 消费模块 ===
module com.example.shop {
requires com.example.payment.spi;
uses com.example.payment.spi.paymentprocessor;
}
package com.example.shop;
import com.example.payment.spi.paymentprocessor;
import java.util.serviceloader;
public class checkoutservice {
public void checkout(double amount) {
serviceloader<paymentprocessor> processors =
serviceloader.load(paymentprocessor.class);
processors.stream()
.map(serviceloader.provider::get)
.filter(p -> p.channel().equals("alipay"))
.findfirst()
.ifpresent(p -> p.pay(amount));
}
}
模块化spi的优势
// 1. 编译期验证:provides的实现类必须存在且实现接口 // 2. 模块图验证:启动时检查服务提供者模块是否在模块图中 // 3. 无需meta-inf/services文件(module-info.java替代) // 4. 可限定服务可见范围 // 注意:为兼容非模块化消费者,仍可同时保留meta-inf/services
jmod工具
概述
# jmod用于创建和操作.jmod文件 # .jmod文件包含:类文件 + 本地库 + 配置文件 + 法律文件 + 头文件 # 用途:jlink构建定制运行时的输入 # .jmod vs .jar: # - .jar: 运行时分发,可放在classpath/module-path # - .jmod: 仅用于jlink构建,不可直接运行 # - .jmod可包含native库(.so/.dll)和头文件(.h)
常用命令
# 创建jmod文件
jmod create \
--class-dir classes \
--lib-dir native-libs \
--conf-dir config \
--legal-notices legal \
--target-platform linux/amd64 \
mods/com.example.native.jmod
# 查看模块描述
jmod describe mods/com.example.native.jmod
# 列出所有文件
jmod list mods/com.example.native.jmod
# 提取内容
jmod extract --dir output-dir mods/com.example.native.jmod
# 记录模块哈希(用于完整性验证)
jmod hash \
--hash-modules "com.example.*" \
--module-path mods \
mods/com.example.app.jmod
jdk自带的jmod文件
# 位置:$java_home/jmods/ # 例如: # java.base.jmod # java.sql.jmod # java.xml.jmod # ... # 这些是jlink构建定制jre的输入
jlink定制运行时镜像
基本用法
# 构建最小运行时(仅包含java.base)
jlink \
--module-path $java_home/jmods \
--add-modules java.base \
--output custom-jre \
--strip-debug \
--compress zip-9 \
--no-header-files \
--no-man-pages
# 输出目录结构:
# custom-jre/
# ├── bin/
# │ └── java (启动器)
# ├── conf/
# ├── legal/
# └── lib/
# └── modules (所有模块数据)
构建应用运行时
# 包含应用模块的完整运行时
jlink \
--module-path "$java_home/jmods:mods" \
--add-modules com.example.app \
--output app-runtime \
--launcher start=com.example.app/com.example.app.main \
--strip-debug \
--compress zip-9 \
--no-header-files \
--no-man-pages
# 运行:
./app-runtime/bin/start
# 或
./app-runtime/bin/java -m com.example.app
常用选项详解
# --add-modules: 指定要包含的模块(会递归包含依赖) --add-modules java.base,java.sql,java.logging # --limit-modules: 限制可解析的模块范围 --limit-modules java.se # --launcher: 创建自定义启动脚本 --launcher myapp=com.example.app/com.example.app.main # --compress: 压缩级别 --compress zip-0 # 不压缩 --compress zip-6 # 恒定压缩(默认) --compress zip-9 # 最大压缩 # --strip-debug: 去除调试信息 # --no-header-files: 去除c头文件 # --no-man-pages: 去除man手册 # --bind-services: 绑定serviceloader服务 --bind-services # --exclude-files: 排除特定文件 --exclude-files glob:**.jcov # --list-plugins: 查看可用插件 jlink --list-plugins
体积对比
# 完整jdk 17: ~300mb # 完整jre (java.se): ~180mb # 最小jre (java.base): ~40mb # hello world应用: ~42mb # 含java.sql的应用: ~55mb # spring boot应用(模块化后): ~80-100mb # docker镜像对比: # 传统: from openjdk:17 → 470mb+ # jlink: from debian:slim + custom-jre → 80-120mb
多平台构建
# 交叉编译(需要目标平台的jmods)
jlink \
--module-path $java_home/jmods \
--add-modules java.base \
--output linux-jre \
--target-platform linux/amd64
# 支持的target-platform:
# linux/amd64, linux/aarch64
# macos/amd64, macos/aarch64
# windows/amd64
迁移策略(自底向上/自顶向下)
自底向上迁移
适用场景:内部库、工具类、基础框架
步骤:
1. 找到依赖图最底层的模块(无外部依赖)
2. 为其添加module-info.java
3. 编译验证
4. 逐步向上层模块添加module-info
5. 最终所有模块都是显式模块
示例依赖图:
com.example.util (无依赖) ← 先迁移
↑
com.example.domain (依赖util) ← 其次
↑
com.example.service (依赖domain) ← 然后
↑
com.example.web (依赖service) ← 最后
// step 1: 最底层模块
module com.example.util {
exports com.example.util.string;
exports com.example.util.collection;
// 内部工具包不导出
}
// step 2: 中间层模块
module com.example.domain {
requires transitive com.example.util;
exports com.example.domain.model;
exports com.example.domain.event;
}
// step 3: 上层模块
module com.example.service {
requires com.example.domain;
requires java.sql;
exports com.example.service.api;
opens com.example.domain.model to org.hibernate.orm.core;
}
自顶向下迁移
适用场景:应用项目、依赖大量第三方库 步骤: 1. 为应用主模块添加module-info.java 2. 第三方库暂放模块路径(作为自动模块) 3. 逐步将第三方库替换为模块化版本 4. 最终所有依赖都是显式模块 优点:快速开始,无需等待所有库模块化 缺点:自动模块阶段封装性有限
// 应用主模块(自顶向下第一步)
module com.example.myapp {
requires java.sql;
requires java.net.http;
// 第三方库作为自动模块引用
requires org.apache.commons.lang3; // 自动模块
requires com.google.guava; // 自动模块
requires spring.core; // 自动模块
exports com.example.myapp.api;
opens com.example.myapp.entity; // 框架反射需要
}
迁移工具:jdeps
# 分析jar的依赖 jdeps --list-deps myapp.jar # 生成module-info.java(jdk 11+) jdeps --generate-module-info output-dir myapp.jar # 输出模块依赖(用于jlink) jdeps --print-module-deps myapp.jar # 输出: java.base,java.sql,java.logging,java.xml # 检查模块依赖完整性 jdeps --check com.example.app --module-path mods # 分析多版本jar jdeps --multi-release 17 myapp.jar
常见迁移问题及解决
// 问题1:拆分包(split package) // 两个模块包含相同包名 → 模块系统不允许 // 解决:合并包、重命名包、或使用shade插件 // 问题2:反射访问被拒绝 // java.lang.reflect.inaccessibleobjectexception // 解决:在module-info.java中添加opens opens com.example.entity to org.hibernate.orm.core; // 或命令行:--add-opens com.example.app/com.example.entity=org.hibernate.orm.core // 问题3:资源文件访问 // 模块化后,getresource()可能找不到资源 // 解决:资源放在导出的包中,或使用opens // 注意:非class文件(.xml, .properties)需要特殊处理 // 问题4:循环依赖 // a requires b, b requires a → 编译错误 // 解决:提取公共接口到第三个模块c // a requires c, b requires c, a requires b(单向)
模块化与spring boot兼容
spring boot模块化现状
// spring boot 3.x 对jpms的支持: // - spring framework 6.x 的jar包含automatic-module-name // - 但spring boot应用通常不完全模块化 // - 推荐策略:open module + 必要的exports // spring modulith(逻辑模块化,非jpms): // - 基于包结构的逻辑模块划分 // - 不依赖jpms,使用注解和约定 // - 适合spring boot应用的内部模块化
spring boot应用的module-info.java
// 方案1:open module(最简单,兼容性最好)
open module com.example.springbootapp {
requires java.sql;
requires java.management;
requires java.naming;
requires spring.core;
requires spring.context;
requires spring.beans;
requires spring.web;
requires spring.boot;
requires spring.boot.autoconfigure;
requires com.fasterxml.jackson.databind;
requires org.hibernate.orm.core;
// open module自动开放所有包的反射
// 无需逐个opens
}
// 方案2:精确控制(更安全,但配置复杂)
module com.example.springbootapp {
requires spring.core;
requires spring.context;
requires spring.boot;
requires spring.boot.autoconfigure;
requires com.fasterxml.jackson.databind;
requires org.hibernate.orm.core;
exports com.example.app.api;
// spring需要反射访问的包
opens com.example.app.controller to spring.core, spring.web;
opens com.example.app.entity to org.hibernate.orm.core, com.fasterxml.jackson.databind;
opens com.example.app.config to spring.core, spring.context;
opens com.example.app.dto to com.fasterxml.jackson.databind;
}
maven配置
<!-- pom.xml 模块化配置 -->
<build>
<plugins>
<plugin>
<groupid>org.apache.maven.plugins</groupid>
<artifactid>maven-compiler-plugin</artifactid>
<version>3.11.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
<!-- spring boot maven plugin需要额外配置 -->
<plugin>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-maven-plugin</artifactid>
<configuration>
<!-- 模块化启动需要指定主模块 -->
<mainclass>com.example.app.application</mainclass>
</configuration>
</plugin>
<!-- surefire测试插件需要add-opens -->
<plugin>
<groupid>org.apache.maven.plugins</groupid>
<artifactid>maven-surefire-plugin</artifactid>
<configuration>
<argline>
--add-opens com.example.app/com.example.app.entity=all-unnamed
--add-opens com.example.app/com.example.app.controller=all-unnamed
</argline>
</configuration>
</plugin>
</plugins>
</build>
运行时jvm参数
# spring boot模块化运行时常需要的参数
java \
--module-path mods \
-m com.example.app/com.example.app.application \
--add-opens com.example.app/com.example.app.entity=org.hibernate.orm.core \
--add-opens com.example.app/com.example.app.dto=com.fasterxml.jackson.databind \
--add-opens java.base/java.lang=all-unnamed \
--add-opens java.base/java.util=all-unnamed
# spring boot 3.x 推荐的application.properties配置
# spring.main.allow-circular-references=false
# 使用spring modulith进行逻辑模块化
spring modulith(替代方案)
// spring modulith: 不依赖jpms的逻辑模块化
// maven依赖:
// org.springframework.modulith:spring-modulith-starter-core
// 包结构约定:
// com.example.app
// ├── order/ ← 逻辑模块
// │ ├── order.java
// │ ├── orderservice.java
// │ └── internal/ ← 模块内部(其他模块不可访问)
// │ └── orderrepository.java
// ├── payment/ ← 逻辑模块
// │ ├── payment.java
// │ └── paymentservice.java
// └── application.java
// 模块间通信通过事件:
package com.example.app.order;
import org.springframework.modulith.events.applicationmodulelistener;
@applicationmodulelistener
public class paymenteventlistener {
@applicationmodulelistener
void on(orderplacedevent event) {
// 处理订单事件
}
}
// 验证模块结构:
@test
void verifymodularstructure() {
applicationmodules.of(application.class).verify();
}
最佳实践
模块设计原则
// 原则1:最小导出 - 只导出必要的api包
module com.example.library {
// 好:只导出api
exports com.example.library.api;
// 坏:导出所有包
// exports com.example.library.impl;
// exports com.example.library.util;
// exports com.example.library.internal;
}
// 原则2:api与实现分离
// 模块 com.example.order.api → 接口和dto
// 模块 com.example.order.impl → 实现(不导出)
// 模块 com.example.order.spring → spring集成
// 原则3:稳定依赖方向
// 不稳定模块 → 依赖 → 稳定模块
// web层 → service层 → domain层 → util层
// 永远不要反向依赖
// 原则4:避免循环依赖
// 如果a和b互相需要 → 提取公共接口到c
// a → c ← b(而非 a ↔ b)
封装策略
// 策略1:默认封闭,按需开放
module com.example.app {
// 仅导出公共api
exports com.example.app.api;
// 仅对需要的框架开放反射
opens com.example.app.entity to org.hibernate.orm.core;
opens com.example.app.dto to com.fasterxml.jackson.databind;
// 内部包完全不导出、不开放
// com.example.app.internal.*
// com.example.app.util.*
}
// 策略2:限定导出(给特定模块)
module com.example.sdk {
// 仅对插件模块导出扩展点
exports com.example.sdk.spi to com.example.plugin.a, com.example.plugin.b;
// 公共api对所有模块导出
exports com.example.sdk.api;
}
// 策略3:open module用于遗留代码(过渡方案)
open module com.example.legacy {
// 所有包自动开放
// 逐步收紧:将open module改为module,逐个添加exports/opens
}
构建配置最佳实践
<!-- maven多模块项目结构 -->
<!-- parent/pom.xml -->
<modules>
<module>app-api</module> <!-- 接口模块 -->
<module>app-domain</module> <!-- 领域模块 -->
<module>app-service</module> <!-- 服务模块 -->
<module>app-web</module> <!-- web模块 -->
<module>app-bootstrap</module> <!-- 启动模块 -->
</modules>
<!-- 每个子模块的module-info.java对应其职责 -->
// gradle模块化配置
// build.gradle
plugins {
id 'java'
}
java {
modularity.infermodulepath = true // 自动推断模块路径
toolchain {
languageversion = javalanguageversion.of(17)
}
}
// settings.gradle
rootproject.name = 'my-modular-app'
include 'app-api', 'app-domain', 'app-service', 'app-web'
运行时调试
# 显示模块解析过程(调试模块路径问题) java --show-module-resolution -m com.example.app/com.example.app.main # 查看模块描述 java --describe-module com.example.app # 查看所有已解析模块 java --list-modules # 添加额外的可读性(调试用) java --add-reads com.example.app=all-unnamed -m com.example.app/main # 添加额外的导出(调试用) java --add-exports java.base/sun.nio.ch=all-unnamed -m com.example.app/main # 添加额外的开放(调试用) java --add-opens java.base/java.lang=all-unnamed -m com.example.app/main
模块化检查清单
□ 每个模块有且仅有一个module-info.java □ 模块名使用反向域名命名 □ 仅导出必要的api包(最小导出原则) □ opens仅给需要的框架模块(限定开放) □ 无循环依赖 □ 依赖方向稳定(上层依赖下层) □ 服务使用uses/provides声明 □ 第三方库确认automatic-module-name稳定 □ 无拆分包问题 □ 测试覆盖模块化边界 □ jlink构建验证通过 □ ci/cd流水线支持模块路径
版本兼容建议
// jdk 9-15: 模块化可选,--illegal-access=permit(默认允许反射) // jdk 16: --illegal-access=deny(默认拒绝) // jdk 17+: --illegal-access选项移除(强封装不可逆) // 建议: // - 新项目直接使用jdk 17+,从开始就模块化 // - 旧项目渐进迁移,先classpath运行,逐步添加module-info // - 库项目尽早添加automatic-module-name(即使不完全模块化) // - 关注依赖库的模块化进度(module-info.java或automatic-module-name)
附录:常用命令速查
# === 编译 === javac --module-source-path src -d out $(find src -name "*.java") javac -p mods -d out src/com.example.app/module-info.java src/com.example.app/com/example/app/main.java # === 运行 === java -p mods -m com.example.app/com.example.app.main java --module-path mods --module com.example.app/com.example.app.main # === 分析 === jdeps --list-deps app.jar jdeps --print-module-deps app.jar jdeps --generate-module-info output app.jar java --describe-module java.sql java --list-modules java --show-module-resolution -m com.example.app/main # === 打包 === jar --create --file mods/com.example.app.jar --main-class com.example.app.main -c out/com.example.app . jar --create --file mods/com.example.app.jar -c out/com.example.app . --module-version 1.0 # === jmod === jmod create --class-dir classes mods/com.example.jmod jmod describe mods/com.example.jmod jmod list mods/com.example.jmod jmod extract --dir output mods/com.example.jmod # === jlink === jlink --module-path "$java_home/jmods:mods" --add-modules com.example.app --output runtime --launcher start=com.example.app/com.example.app.main --strip-debug --compress zip-9 --no-header-files --no-man-pages # === 调试参数 === --add-reads 模块=目标模块 --add-exports 模块/包=目标模块 --add-opens 模块/包=目标模块 --add-modules 模块列表 --limit-modules 模块列表
总结
到此这篇关于java深入解析篇之模块化详解的文章就介绍到这了,更多相关java模块化内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论