当前位置: 首页 > news >正文

Java 9 + 模块化系统实战:从 Jar 地狱到模块解耦的架构升级​

在 Java 9 之前,Java 应用的依赖管理一直面临 “Jar 地狱”(Jar Hell)的困扰 —— 不同 Jar 包的版本冲突、依赖传递混乱、无用依赖冗余等问题,常常导致应用启动失败或运行时异常。为解决这一痛点,Java 9 正式引入模块化系统(Java Platform Module System,JPMS),通过 “显式依赖声明”“访问权限控制”“模块边界定义”,从语言层面实现了代码的模块化拆分与解耦。本文将从传统 Jar 依赖的痛点出发,详解模块化系统的核心概念、配置语法、模块间交互规则及实战落地案例,帮你掌握 Java 模块化开发的完整流程。​

一、为什么需要模块化系统?—— 传统 Jar 依赖的 4 大痛点​

在理解模块化系统之前,我们首先要明确:传统 Java 应用的依赖管理基于 “类路径(ClassPath)”,所有 Jar 包和类文件都被放入同一个 “全局空间”,这种方式在应用规模扩大时会暴露诸多问题。​

1.1 痛点 1:Jar 版本冲突(最常见)​

当应用依赖的两个 Jar 包引用了同一个第三方库的不同版本时,ClassPath 会优先加载 “先出现” 的 Jar 包,导致后续版本的类无法被加载,引发NoClassDefFoundError或MethodNotFoundError。​

例如:​

  • 应用依赖spring-core-5.2.0.jar,该 Jar 包依赖commons-logging-1.2.jar;​
  • 同时应用还依赖mybatis-3.5.0.jar,该 Jar 包依赖commons-logging-1.1.jar;​
  • 若 ClassPath 中commons-logging-1.1.jar在前,spring-core需要的1.2版本类会缺失,导致启动失败。​

这种问题排查难度极大,往往需要通过mvn dependency:tree分析依赖树,手动排除冲突版本,效率低下。​

1.2 痛点 2:依赖传递混乱(隐式依赖)​

传统依赖管理中,依赖是 “隐式传递” 的 —— 引入一个 Jar 包会自动引入其依赖的所有 Jar 包,开发者无法清晰感知应用实际依赖的组件。例如,引入spring-boot-starter-web会间接引入tomcat-embed-core、spring-web、jackson-databind等数十个 Jar 包,其中很多组件可能并非应用所需,导致应用体积臃肿。​

1.3 痛点 3:访问权限失控(无模块边界)​

传统 Java 中,只要类的访问修饰符是public,任何其他类都可以访问,即使这些类属于不同的功能模块。例如,应用的 “支付模块” 中的PaymentUtils工具类(设计为内部使用),若被标记为public,则 “订单模块”“用户模块” 都能直接调用,导致模块间耦合度极高,后续修改PaymentUtils时可能影响多个模块。​

1.4 痛点 4:JRE 体积庞大(无用 API 冗余)​

Java 9 之前的 JRE 包含rt.jar(约 60MB)和tools.jar等巨型 Jar 包,其中包含大量应用可能永远不会用到的 API(如javax.swing、java.applet)。对于嵌入式设备或轻量级应用(如微服务),这种 “全量 JRE” 会导致部署包体积过大,资源浪费严重。​

1.2 模块化系统的核心价值​

Java 模块化系统通过以下 4 点设计,从根本上解决了传统依赖的痛点:​

  1. 显式依赖声明:每个模块需在module-info.java中明确声明依赖的其他模块,避免隐式传递和版本冲突;​
  1. 访问权限控制:模块可精确控制 “对外暴露哪些包”,非暴露的包即使包含public类,其他模块也无法访问,实现模块边界隔离;​
  1. 依赖解耦:模块间通过 “接口依赖” 而非 “实现依赖”,降低耦合度,便于后续升级和替换;​
  1. JRE 瘦身:JRE 本身被拆分为多个模块(如java.base、java.sql、java.xml),应用可仅依赖所需模块,减少部署体积。​

二、模块化系统的核心概念:从模块到 module-info​

要掌握模块化开发,首先需要理解 3 个核心概念:模块(Module)、模块描述符(module-info.java)、模块路径(ModulePath),它们共同构成了模块化系统的基础。​

2.1 1. 模块(Module):代码的最小功能单元​

模块是 Java 9 + 中代码组织的最小单元,一个模块包含:​

  • 包(Package):一组相关的类和接口,模块通过包实现代码的逻辑分组;​
  • 模块描述符:module-info.java文件,定义模块的基本信息、依赖和访问规则;​
  • 资源文件:模块所需的配置文件、静态资源等(如application.properties)。​

每个模块都有一个唯一的模块名(类似包名,需全局唯一),例如com.example.payment(支付模块)、com.example.order(订单模块)。​

2.2 2. 模块描述符(module-info.java):模块的 “身份证”​

module-info.java是模块的核心配置文件,必须放在模块的根目录下(与包的层级平级),用于定义模块的以下信息:​

  • 模块名称;​
  • 依赖的其他模块;​
  • 对外暴露的包(exports);​
  • 提供的服务和使用的服务(ServiceLoader 相关);​
  • 允许反射访问的包(opens)。​

示例:一个简单的module-info.java​

j取消自动换行复制

// 模块名称:com.example.payment(必须全局唯一)​

module com.example.payment {​

// 1. 依赖的模块:依赖JDK的基础模块java.base(默认隐式依赖,可省略)​

requires java.base;​

// 依赖自定义的订单模块​

requires com.example.order;​

// 2. 对外暴露的包:其他模块可访问该包下的public类​

exports com.example.payment.api; // 暴露支付接口包​

exports com.example.payment.dto; // 暴露支付数据传输对象包​

// 3. 不对外暴露的包:仅模块内部可访问​

// (如com.example.payment.impl包,包含支付接口的实现,不对外暴露)​

// 4. 允许其他模块反射访问的包(如框架需要反射注入)​

opens com.example.payment.config to spring.core;​

}​

2.3 3. 模块路径(ModulePath):模块的 “类路径”​

Java 9 + 引入 “模块路径”(ModulePath)替代传统的 “类路径”(ClassPath),用于存放模块(可是模块化 Jar 包或未打包的模块目录)。与 ClassPath 的区别:​

  • ClassPath:所有类和 Jar 包处于同一全局空间,无模块边界;​
  • ModulePath:每个模块独立存在,模块间的访问受module-info约束。​

在编译和运行时,需通过--module-path(或简写-p)指定模块路径,例如:​

ba取消自动换行复制

# 编译模块:将模块路径设为modules目录,编译com.example.payment模块​

javac --module-path modules -d modules/com.example.payment src/com.example.payment/module-info.java src/com.example.payment/com/example/payment/**/*.java​

# 运行模块:指定主模块和主类​

java --module-path modules -m com.example.payment/com.example.payment.Main​

三、module-info 核心配置详解:依赖、暴露与访问控制​

module-info.java的配置语法虽简洁,但包含丰富的功能,掌握这些配置是模块化开发的关键。以下是最常用的 5 类配置:​

3.1 1. 依赖声明:requires与requires transitive​

requires用于声明模块依赖的其他模块,分为 “直接依赖” 和 “传递依赖”。​

(1)直接依赖:requires <模块名>​

声明当前模块直接依赖的模块,其他模块若依赖当前模块,不会自动依赖该模块(即 “非传递依赖”)。​

示例:​

j取消自动换行复制

module com.example.order {​

// 直接依赖用户模块,仅当前模块可使用用户模块的功能​

requires com.example.user;​

}​

(2)传递依赖:requires transitive <模块名>​

声明当前模块依赖的模块,且其他模块若依赖当前模块,会自动依赖该模块(即 “传递依赖”),用于 “API 依赖传递” 场景。​

示例:​

java取消自动换行复制

module com.example.payment {​

// 传递依赖订单模块:若其他模块依赖payment,会自动依赖order​

requires transitive com.example.order;​

}​

// 其他模块依赖payment时,无需显式依赖order​

module com.example.app {​

requires com.example.payment; // 自动依赖com.example.order​

}​

适用场景:​

  • 若当前模块的 API(对外暴露的包)依赖了其他模块的类(如PaymentApi使用OrderDTO),需用requires transitive,避免下游模块手动依赖;​
  • 若当前模块的内部实现依赖其他模块,用requires即可,避免不必要的依赖传递。​

3.2 2. 包暴露:exports与exports ... to​

exports用于控制模块对外暴露的包,只有被暴露的包,其他模块才能访问其中的public类和接口。​

(1)公开暴露:exports <包名>​

将指定包公开暴露给所有依赖当前模块的模块,任何模块都可访问该包下的public成员。​

示例:​

java取消自动换行复制

module com.example.payment {​

// 公开暴露api包,所有依赖payment的模块都可访问​

exports com.example.payment.api;​

}​

(2)定向暴露:exports <包名> to <模块名1>, <模块名2>​

将指定包仅暴露给特定模块,其他模块无法访问,用于 “模块间安全通信” 场景。​

示例:​

java取消自动换行复制

module com.example.payment {​

// 仅将internal包暴露给order模块,其他模块无法访问​

exports com.example.payment.internal to com.example.order;​

}​

关键规则:​

  • 未被exports的包,即使包含public类,其他模块也无法访问(模块边界隔离的核心);​
  • 子包不会被自动暴露,例如exports com.example.payment不会暴露com.example.payment.api,需单独声明。​

3.3 3. 反射访问:opens与opens ... to​

传统 Java 中,反射可访问任何类(即使是private成员),但模块化系统默认禁止跨模块反射访问非暴露的包。opens用于允许其他模块反射访问当前模块的包。​

(1)公开反射:opens <包名>​

允许所有模块反射访问指定包下的所有类(包括private成员)。​

示例:​

java取消自动换行复制

module com.example.payment {​

// 允许所有模块反射访问config包(如框架读取配置类)​

opens com.example.payment.config;​

}​

(2)定向反射:opens <包名> to <模块名>​

仅允许特定模块反射访问指定包,用于 “框架反射注入” 场景(如 Spring、Hibernate)。​

示例:​

java取消自动换行复制

module com.example.payment {​

// 仅允许spring.core模块反射访问service包(Spring依赖注入)​

opens com.example.payment.service to spring.core;​

}​

常见使用场景:​

  • Spring 的@Autowired依赖注入:需opens服务类所在包给spring.core;​
  • Jackson 的 JSON 序列化:需opensDTO 类所在包给com.fasterxml.jackson.databind;​
  • MyBatis 的 Mapper 接口代理:需opensmapper 包给org.mybatis。​

3.4 4. 服务提供与使用:provides ... with与uses​

模块化系统支持 “服务发现” 机制,通过provides和uses实现模块间的 “接口依赖”,降低耦合度。​

(1)服务接口定义(公共模块)​

首先在公共模块中定义服务接口(如支付服务接口):​

java取消自动换行复制

// 公共模块:com.example.common​

module com.example.common {​

exports com.example.common.service; // 暴露服务接口包​

}​

// 服务接口​

package com.example.common.service;​

public interface PaymentService {​

boolean pay(String orderId, BigDecimal amount);​

}​

(2)服务实现(提供方模块)​

在支付模块中实现服务接口,并通过provides ... with声明服务实现:​

java取消自动换行复制

(3)服务使用(消费方模块)​

在订单模块中通过uses声明使用服务,并通过ServiceLoader获取服务实现:​

java取消自动换行复制

核心优势:​

  • 消费方模块仅依赖 “服务接口”(com.example.common),不依赖 “服务实现”(com.example.payment),实现 “接口与实现分离”;​
  • 更换服务实现(如从支付宝改为微信支付)时,只需修改提供方模块,消费方无需任何改动,灵活性极高。​

3.5 5. 自动模块:Automatic-Module-Name​

对于未模块化的传统 Jar 包(如第三方库),模块化系统会将其视为 “自动模块”(Automatic Module),并自动生成模块名。若传统 Jar 包的META-INF/MANIFEST.MF中包含Automatic-Module-Name,则模块名为此值;否则模块名由 Jar 包名推导(如commons-logging-1.2.jar的模块名为commons.logging)。​

示例:为传统 Jar 包添加自动模块名​

在META-INF/MANIFEST.MF中添加:​

plaintext取消自动换行复制

Automatic-Module-Name: com.example.commons.logging​

这样,其他模块化 Jar 包可通过requires com.example.commons.logging依赖该传统 Jar 包,实现模块化与非模块化代码的兼容。​

四、模块化实战:搭建一个多模块 Java 应用​

本节将通过一个 “电商应用” 案例,展示模块化应用的完整搭建流程,包含 “用户模块”“订单模块”“支付模块” 和 “主应用模块”,实现模块间的依赖与解耦。​

4.1 1. 项目结构设计​

模块化应用的项目结构需按 “模块” 划分,每个模块独立为一个目录,包含src(源代码)和module-info.java:​

plaintext取消自动换行复制

4.2 2. 各模块module-info.java配置​

(1)用户模块(com.example.user)​

java取消自动换行复制

// src/com.example.user/module-info.java​

module com.example.user {​

// 依赖JDK基础模块(默认隐式依赖,可省略)​

requires java.base;​

// 对外暴露api和dto包​

exports com.example.user.api;​

exports com.example.user.dto;​

}​

(2)订单模块(com.example.order)​

java取消自动换行复制

(3)支付模块(com.example.payment)​

java取消自动换行复制

(4)主应用模块(com.example.app)​

java取消自动换行复制

// src/com.example.app/module-info.java​

module com.example.app {​

// 依赖订单和支付模块​

requires com.example.order;​

requires com.example.payment;​

// 使用支付服务​

uses com.example.common.service.PaymentService;​

}​

4.3 3. 核心代码实现​

(1)用户模块:UserService 接口与 UserDTO​

java取消自动换行复制

// com.example.user.api.UserService​

package com.example.user.api;​

import com.example.user.dto.UserDTO;​

public interface UserService {​

UserDTO getUserById(Long userId);​

}​

// com.example.user.dto.UserDTO​

package com.example.user.dto;​

public class UserDTO {​

private Long id;​

private String name;​

private String phone;​

// getter/setter​

}​

(2)订单模块:OrderService 接口​

java取消自动换行复制

// com.example.order.api.OrderService​

package com.example.order.api;​

import com.example.order.dto.OrderDTO;​

public interface OrderService {​

OrderDTO createOrder(Long userId, BigDecimal amount);​

}​

(3)支付模块:PaymentService 实现​

java取消自动换行复制

(4)主应用:Main 类(启动入口)​

java取消自动换行复制

4.4 4. 编译与运行​

(1)编译模块​

在项目根目录执行以下命令,将所有模块编译到modules目录:​

bash取消自动换行复制

(2)运行主应用​

通过java命令指定模块路径和主模块,启动应用:​

bash取消自动换行复制

java --module-path modules -m com.example.app/com.example.app.Main​

(3)运行结果​

plaintext取消自动换行复制

当前用户:张三​

创建订单:ORDER_20241110001​

支付宝支付:订单ID=ORDER_20241110001,金额=99.00​

订单支付成功!​

五、模块化系统的常见误区与避坑指南​

虽然模块化系统优势显著,但在实际开发中,若配置不当,可能导致 “模块找不到”“类访问权限不足” 等问题。以下是 6 个常见误区及避坑建议:​

5.1 误区 1:模块名与包名混淆​

错误示例:模块名与包名不一致,导致依赖引用错误:​

java取消自动换行复制

// 错误:模块名使用包名的子路径,不符合规范​

module com.example.payment.api {​

requires com.example.order;​

}​

避坑建议:​

  • 模块名应遵循 “反向域名” 规则,与包名保持一致或包含包名的核心部分,例如包名为com.example.payment,模块名也应为com.example.payment;​
  • 模块名应简洁且全局唯一,避免包含版本号(如com.example.payment-1.0)。​

5.2 误区 2:过度暴露包​

错误示例:将模块的所有包都对外暴露,导致内部实现被外部依赖:​

java取消自动换行复制

// 错误:暴露impl包(包含内部实现),导致耦合度升高​

module com.example.payment {​

exports com.example.payment.api;​

exports com.example.payment.impl; // 不应暴露实现包​

}​

避坑建议:​

  • 仅暴露 “对外提供的 API 和 DTO 包”,内部实现包(如impl、util、config)不暴露;​
  • 若其他模块需访问内部包,优先使用 “定向暴露”(exports ... to),而非公开暴露。​

5.3 误区 3:忽略反射访问配置​

错误示例:使用 Spring 等框架时,未配置opens,导致反射注入失败:​

java取消自动换行复制

// 错误:未opens service包,Spring无法反射创建Service实例​

module com.example.payment {​

requires spring.core;​

exports com.example.payment.api;​

}​

避坑建议:​

  • 若使用依赖注入、JSON 序列化等需要反射的框架,需通过opens允许框架模块反射访问相关包;​
  • 常用框架的模块名:Spring 核心模块为spring.core,Jackson 为com.fasterxml.jackson.databind,MyBatis 为org.mybatis。​

5.4 误区 4:依赖传递配置错误​

错误示例:API 依赖的模块未用requires transitive,导致下游模块手动依赖:​

java取消自动换行复制

// 支付模块的API使用OrderDTO,但未用transitive​

module com.example.payment {​

requires com.example.order; // 应为requires transitive​

exports com.example.payment.api;​

}​

// 下游应用模块依赖payment时,需手动依赖order(冗余)​

module com.example.app {​

requires com.example.payment;​

requires com.example.order; // 本可通过transitive自动依赖​

}​

避坑建议:​

  • 若当前模块的exports包中引用了其他模块的类,必须用requires transitive声明该模块依赖;​
  • 通过jdeps工具分析模块依赖:jdeps --module-path modules -s com.example.payment,检查是否存在未传递的 API 依赖。​

5.5 误区 5:传统 Jar 包与模块冲突​

错误示例:同时在 ClassPath 和 ModulePath 中放置同一 Jar 包,导致类重复加载:​

bash取消自动换行复制

# 错误:同时在ClassPath和ModulePath中包含commons-logging​

java --class-path lib/commons-logging-1.2.jar --module-path modules -m com.example.app/com.example.app.Main​

避坑建议:​

  • 模块化应用应优先使用 ModulePath,避免同时使用 ClassPath 和 ModulePath;​
  • 对于未模块化的传统 Jar 包,通过Automatic-Module-Name将其转为自动模块,放入 ModulePath。​

5.6 误区 6:JDK 内部模块依赖错误​

错误示例:依赖 JDK 的非基础模块但未声明requires:​

java取消自动换行复制

// 错误:使用java.sql包但未声明依赖java.sql模块​

module com.example.order {​

// 缺少requires java.sql;​

exports com.example.order.api;​

}​

避坑建议:​

  • JDK 的基础模块java.base是默认隐式依赖,无需声明;​
  • 其他 JDK 模块(如java.sql、java.xml、java.net.http)需显式requires声明;​
  • 通过javac -Xlint:module编译时开启模块依赖检查,及时发现未声明的 JDK 模块依赖。​

六、总结与未来展望​

Java 模块化系统是 Java 语言从 “面向类” 到 “面向模块” 的重要升级,它通过 “显式依赖”“边界隔离”“服务发现” 等特性,彻底解决了传统 Jar 依赖的痛点,为大型 Java 应用的架构解耦提供了语言层面的支持。​

6.1 核心价值回顾​

  1. 解耦与隔离:模块间通过显式依赖和包暴露实现边界隔离,降低耦合度;​
  1. 依赖清晰:module-info明确声明依赖,避免隐式传递和版本冲突;​
  1. 安全可控:非暴露包和定向访问控制,防止内部实现被滥用;​
  1. 生态兼容:支持自动模块,兼容传统 Jar 包,平滑过渡到模块化开发。​

6.2 实战建议​

  • 小型应用:若应用规模较小(代码量 < 1 万行),可暂不使用模块化,避免过度设计;​
  • 中型应用:按业务功能拆分模块(如用户、订单、支付),实现核心功能解耦;​
  • 大型应用:结合 DDD 领域驱动设计,按领域边界拆分模块,通过服务接口实现跨模块通信;​
  • 框架选型:优先选择支持模块化的框架(如 Spring Boot 2.3+、MyBatis 3.5+),避免反射访问问题。​

6.3 未来展望​

随着 Java 11、17 等长期支持版本的普及,模块化系统已成为企业级 Java 应用的标配。未来,模块化将进一步与微服务、容器化结合 —— 通过模块拆分实现 “微服务的原子化设计”,通过模块路径优化容器镜像体积,为 Java 应用的云原生转型提供更强的架构支撑。​

掌握 Java 模块化系统,不仅能解决当前依赖管理的痛点,更能帮助你建立 “模块化思维”,为后续复杂应用的架构设计打下坚实基础。

http://www.dtcms.com/a/585208.html

相关文章:

  • 及时通讯桌面端应用基vue+GO
  • 三个常听到的消息/中间件MQTT RabbitMQ Kafka
  • QML学习笔记(五十四)QML与C++交互:数据转换——QVariantList与QVariantMap
  • Linux的基础IO流
  • RabbitMQ死信交换机与延迟队列:原理、实现与最佳实践
  • 网站建设人员叫什么科目wordpress站长地图
  • Kafka安装搭建
  • 深度血虚:Django水果检测识别系统 CNN卷积神经网络算法 python语言 计算机 大数据✅
  • 郑州h5网站建设信息流推广
  • Git-新建分支并推送远程仓
  • 团关系转接网站建设免费psd模板素材
  • 永磁同步电机MTPA控制详解:从理论到实践的全方位指南
  • 【GORM(3)】Go的跨时代ORM框架!—— 数据库连接、配置参数;本文从0开始教会如何配置GORM的数据库
  • AIStarter 服务器版 PanelAI 开源+早鸟票 抢商业永久授权
  • 【项目】pyqt5基于python的照片整蛊项目
  • 深入理解Java堆栈:从原理到面试实战
  • MySQL快速入门——基本查询(下)
  • PyTorch深度学习进阶(二)(批量归一化)
  • 基于字符串的专项实验
  • CPO-SVM回归 基于冠豪猪优化算法支持向量机的多变量回归预测 (多输入单输出)Matlab
  • 飞凌嵌入式ElfBoard-标准IO接口之关闭文件
  • Rust 练习册 :Prime Factors与质因数分解
  • 12380网站开发apache wordpress rewrite
  • CSS - transition 过渡属性及使用方法(示例代码)
  • web网页开发,在线%考试管理%系统,基于Idea,vscode,html,css,vue,java,maven,springboot,mysql
  • 2025年北京海淀区中小学生信息学竞赛第一赛段试题(附答案)
  • Linux 基础开发工具入门:软件包管理器的全方位实操指南
  • 金仓数据库用户权限隔离:从功能兼容到安全增强的技术演进
  • shell(4)--shell脚本中的循环:(if循环,for,while,until)和退出循环(continue,break, exit)
  • IDEA 软件下载 + 安装 | 操作步骤