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

Java模块化系统(JPMS)全指南:从核心原理到SpringBoot3生产适配避坑实战

前言

最近很多读者私信我,升级SpringBoot3和JDK17的时候遇到一大堆java.lang.reflect.InaccessibleObjectException报错,查资料都是说要加--add-opens参数,但不知道为什么要加,也不知道有没有更优雅的解决方案。其实这些问题的根源都是Java 9引入的模块化系统(JPMS,Java Platform Module System),很多开发者对这个特性了解甚少,导致升级的时候踩了无数坑。

JPMS是JDK历史上最大的架构变更之一,彻底解决了困扰Java开发者二十多年的Jar Hell问题,同时大幅提升了Java的封装性、安全性和部署灵活性,是JDK17+和SpringBoot3生态的基础特性。今天这篇文章就从核心原理、基础实操、生产避坑、SpringBoot3适配四个维度,带你彻底搞懂JPMS,让你升级JDK17+再也不慌。

一、为什么需要JPMS?传统ClassPath的痛点

在Java 9之前,Java的类加载完全依赖ClassPath机制,这个机制设计得非常简单,但在复杂项目中会暴露出很多无法解决的痛点:

1. Jar Hell问题

ClassPath的类加载遵循“先到先得”的原则,如果项目中引入了两个不同版本的同名Jar包(比如commons-lang3 3.12和3.8),类加载器只会加载第一个找到的类,运行期会出现各种诡异的NoSuchMethodError、ClassCastException,排查成本极高。而且没有任何机制能在启动前就检测到这种冲突,只能靠开发者人肉排查依赖树。

2. 封装性完全失效

JDK内部的很多API比如sun.misc.Unsafejdk.internal.misc.Unsafe本来是JDK内部使用的,但是因为ClassPath没有访问控制,开发者可以随意调用这些内部API,导致JDK升级的时候兼容性极差,很多老项目只能停留在JDK8不敢升级。

3. 运行时冗余严重

传统的JRE包含了所有Java标准模块的实现,哪怕你的项目只用到了基础的集合和IO能力,也要带上几百M的完整JRE,容器化部署的时候镜像体积非常大,浪费存储和带宽资源。

JPMS的出现就是为了彻底解决这些痛点,它把Java的代码组织粒度从Jar包提升到了“模块”,给Java增加了类似OSGi的模块化能力,但比OSGi更轻量、更简单,是JDK原生支持的特性。

二、JPMS核心概念详解

1. 什么是模块

模块是JPMS中代码组织的最小单元,一个模块就是一组包含了module-info.java描述文件的包集合,编译后会生成module-info.class文件,放在Jar包的根目录下。和传统Jar包相比,模块明确声明了自己的依赖、对外暴露的包、运行时开放的包等元信息,JVM在启动的时候就会校验这些元信息,提前发现问题。

2. 核心指令详解

module-info.java是模块的描述文件,里面通过几个核心指令定义模块的行为: | 指令 | 作用 | 访问范围 | | --- | --- | --- | |exports <包名>| 导出指定包给其他模块,其他模块可以在编译期访问这个包下的public类和成员 | 编译期可见,运行期反射访问非public成员会报错 | |exports <包名> to <模块1>,<模块2>| 定向导出包,只有指定的模块可以访问这个包 | 编译期定向可见 | |opens <包名>| 开放指定包给其他模块,其他模块可以在运行期反射访问这个包下的所有成员(包括private) | 运行期反射可见 | |opens <包名> to <模块1>,<模块2>| 定向开放包,只有指定的模块可以反射访问这个包 | 运行期定向反射可见 | |requires <模块名>| 声明当前模块依赖的其他模块 | 依赖的模块必须存在,否则启动失败 | |requires transitive <模块名>| 声明传递依赖,其他模块依赖当前模块时,会自动继承这个依赖 | 传递给上层模块 | |uses <接口全类名>| 声明当前模块使用的服务接口,配合ServiceLoader使用 | 服务发现 | |provides <接口全类名> with <实现类全类名>| 声明当前模块提供的服务实现 | 服务注册 |

三、JPMS基础实操:从零搭建模块化项目

我们通过一个简单的多模块项目来演示JPMS的基础用法,项目包含两个模块:工具模块com.example.util和应用模块com.example.app

1. 项目结构

jpms-demo ├── pom.xml # 父pom ├── com.example.util # 工具模块 │ ├── pom.xml │ └── src │ └── main │ └── java │ ├── com │ │ └── example │ │ └── util │ │ └── StringUtils.java │ └── module-info.java └── com.example.app # 应用模块 ├── pom.xml └── src └── main └── java ├── com │ └── example │ └── app │ └── Main.java └── module-info.java

2. 父pom配置

父pom统一管理JDK版本和插件版本,需要使用支持JPMS的maven-compiler-plugin 3.8.0以上版本:

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>jpms-demo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>com.example.util</module> <module>com.example.app</module> </modules> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <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> </plugins> </build> </project>

3. 工具模块实现

首先编写com.example.util模块的module-info.java,导出工具类所在的包:

module com.example.util { // 导出util包给其他模块编译期访问 exports com.example.util; }

然后编写工具类StringUtils:

package com.example.util; public class StringUtils { public static boolean isEmpty(String str) { return str == null || str.trim().isEmpty(); } // 内部私有方法,默认模块外不可访问 private static void internalCheck() { System.out.println("执行内部校验逻辑"); } }

4. 应用模块实现

编写com.example.app模块的module-info.java,声明对util模块的依赖:

module com.example.app { requires com.example.util; }

编写启动类Main,调用StringUtils的方法:

package com.example.app; import com.example.util.StringUtils; import java.lang.reflect.Method; public class Main { public static void main(String[] args) throws Exception { String testStr = "hello JPMS"; System.out.println("字符串是否为空:" + StringUtils.isEmpty(testStr)); // 尝试反射调用私有方法,没有开放包的话会报错 Method method = StringUtils.class.getDeclaredMethod("internalCheck"); method.setAccessible(true); method.invoke(null); } }

5. 运行测试

此时直接运行Main类会报错:java.lang.IllegalAccessException: class com.example.app.Main (in module com.example.app) cannot access class com.example.util.StringUtils (in module com.example.util) because module com.example.util does not open com.example.util to module com.example.app

这是因为我们只exports了包,没有opens包给app模块反射访问,修改util模块的module-info.java

module com.example.util { exports com.example.util; // 定向开放util包给app模块反射访问 opens com.example.util to com.example.app; }

重新运行就可以看到正常输出:

字符串是否为空:false 执行内部校验逻辑

6. 传递依赖演示

如果util模块依赖了Guava,我们可以用requires transitive让app模块自动继承这个依赖,修改util模块的module-info.java

module com.example.util { exports com.example.util; opens com.example.util to com.example.app; // 传递依赖Guava,上层模块不需要再显式依赖 requires transitive com.google.common; }

此时app模块不需要再声明对Guava的依赖,就可以直接使用Guava的类:

// Main类中直接使用Guava的工具类 System.out.println(com.google.common.base.Strings.isNullOrEmpty(testStr));

四、生产环境JPMS常见踩坑指南

1. JDK内部类反射访问报错

这是升级JDK17+最常见的报错,比如:

java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @12345678

产生原因:你的代码或者依赖的框架反射访问了JDK内部模块的类,JDK的模块默认没有开放这些包给外部访问。解决方案

  • 优先升级依赖框架到最新适配JDK17的版本,比如MyBatis 3.5.10+、Jackson 2.15+都已经原生适配JPMS,不需要额外配置。
  • 如果框架暂时没有适配,可以加JVM参数开放对应的包:--add-opens <模块名>/<包名>=<目标模块名>,如果是未命名模块(没有module-info的项目),目标模块名填ALL-UNNAMED,比如:--add-opens java.base/java.lang=ALL-UNNAMED

2. 未命名模块问题

很多老旧的Jar包没有module-info.class文件,会被JVM自动放入未命名模块,未命名模块的特性:

  • 可以访问所有已命名模块的导出包
  • 已命名模块不能访问未命名模块的任何类解决方案:如果你的项目是模块化项目,依赖了老旧的非模块化Jar包,可以加JVM参数--add-modules=ALL-MODULE-PATH把所有Jar包当做模块加载,或者暂时不把自己的项目改为模块化,留在未命名模块中。

3. jlink裁剪Runtime实战

JPMS带来的最大收益之一就是可以用jlink工具自定义裁剪JRE,大幅减小部署包体积。比如我们的项目只用到了java.basejava.net.http两个模块,可以用如下命令生成自定义JRE:

jlink --module-path $JAVA_HOME/jmods \ --add-modules java.base,java.net.http \ --output custom-jre \ --compress 2 \ --no-header-files \ --no-man-pages

生成的custom-jre大小只有30M左右,比传统JRE的200M+小了85%,非常适合容器化部署。配合Docker多阶段构建,可以把SpringBoot镜像的体积控制在100M以内。

五、SpringBoot3适配JPMS完整实战

SpringBoot3已经原生支持JPMS,我们可以快速搭建一个模块化的SpringBoot3项目:

1. 模块描述文件编写

新建SpringBoot3项目,在src/main/java下创建module-info.java

module com.example.springbootjpms { // 依赖Spring核心模块 requires spring.core; requires spring.context; requires spring.boot; requires spring.boot.autoconfigure; requires spring.web; requires com.fasterxml.jackson.databind; // 开放项目包给Spring反射扫描(必须配置,否则Spring扫描不到Bean) opens com.example.springbootjpms to spring.core, spring.context, spring.beans, spring.boot; opens com.example.springbootjpms.controller to spring.web; // 导出Controller包给Spring Web访问 exports com.example.springbootjpms.controller; }

2. 业务代码编写

启动类:

package com.example.springbootjpms; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class JpmsSpringBootApplication { public static void main(String[] args) { SpringApplication.run(JpmsSpringBootApplication.class, args); } }

测试Controller:

package com.example.springbootjpms.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello JPMS + SpringBoot3!"; } }

3. 打包运行

pom.xml中使用spring-boot-maven-plugin 3.0+版本打包:

<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>3.1.5</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>

运行mvn package生成Jar包,直接用java -jar命令运行即可,访问http://localhost:8080/hello可以正常得到返回结果。

六、总结

JPMS是Java生态未来的基础特性,随着JDK17成为LTS版本和SpringBoot3的普及,JPMS会越来越多地出现在我们的项目中。对于开发者来说:

  1. 新的JDK17+项目优先考虑适配JPMS,从一开始就做好代码封装,避免Jar Hell问题。
  2. 老项目升级JDK17+可以先不使用模块化,通过JVM参数解决反射报错,逐步适配。
  3. 容器化部署的项目一定要尝试用jlink裁剪Runtime,大幅降低镜像体积。

如果你在适配JPMS的过程中遇到任何问题,欢迎在评论区留言交流。

http://www.zskr.cn/news/1355300.html

相关文章:

  • 终极APK编辑指南:APK Editor Studio完整使用教程
  • 如何在Windows系统上使用Btrfs文件系统:WinBtrfs完整实用指南
  • FastGithub:5分钟彻底解决GitHub访问慢的智能DNS加速神器
  • TV Bro:用遥控器征服大屏幕,重新定义智能电视上网体验
  • 终极指南:如何用PHP轻松实现网页截图与PDF生成
  • 通过Taotoken模型广场对比不同模型在代码生成任务上的效果与成本
  • CVE-2022-30525:Zyxel防火墙ZTP未授权RCE漏洞深度解析
  • 手把手教你用闲置安卓手机搭建个人收款系统(蓝鲸支付私有化部署实战)
  • Java NIO核心组件与使用
  • 3大音乐平台逐字歌词解析:ESLyric-LyricsSource完整使用指南
  • Blender新手别怕!跟着这篇保姆级教程,用细分建模搞定你的第一辆卡通小车
  • 城通网盘解析器终极指南:3步获取高速直连下载地址
  • M3U8视频下载神器:3分钟搞定分段视频合并
  • eNSP实验笔记:从攻击到防御,一次搞懂交换机如何应对MAC地址泛洪(含静态绑定与动态限制)
  • 3分钟掌握Illustrator批量替换:ReplaceItems.jsx让你的设计效率提升10倍
  • 赴德国参展展台设计规划:从品牌形象到空间动线怎么落地? - 资讯焦点
  • 解决SolidWorks转URDF三大典型问题:坐标系错乱、模型散架与参数丢失
  • 终极指南:如何免费快速解决国内GitHub访问难题,提升下载速度100倍
  • 为自动化脚本选择taotoken多模型api提升任务兼容性
  • 深度解析开源GPS自行车码表:构建专业级离线导航与轨迹记录系统
  • Arm安全架构中的SPM与FF-A规范解析
  • 初次体验Taotoken模型广场一站式选型与测试
  • AMD Ryzen处理器终极调试指南:如何通过SMUDebugTool实现精准性能调优
  • 苏州市吴江区星汇耀再生资源:吴江电线电缆回收哪家靠谱 - LYL仔仔
  • 手写神经网络:从NumPy实现前向传播与反向传播
  • 终极指南:30秒解决JetBrains IDE试用期到期问题
  • 聚类实战指南:从业务问题出发的无监督学习落地方法
  • Windows网络带宽测试终极指南:iperf3完整安装与使用教程
  • k6+Grafana 实时性能测试工作流:构建SLO驱动的可观测闭环
  • 告别ChatGPT频繁掉线!手把手教你用油猴脚本KeepChatGPT实现稳定对话(附详细配置与安全建议)