Java深入解析篇七之模块化详解

Java深入解析篇七之模块化详解

模块化概述(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对比:

特性exportsopens
生效时期编译期+运行期仅运行期
访问方式正常代码引用(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 模块列表

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

萧瑟余晖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值