Java模块系统中存在命名模块、自动模块和未命名模块三类:命名模块含module-info.java并位于模块路径;自动模块是无module-info的JAR,置于模块路径时由JVM自动命名;未命名模块承载所有-classpath加载的类,不参与模块验证且无法被requires声明依赖。
自动模块(automatic module)和未命名模块(unnamed module)是 java 9+ 模块系统中两类特殊模块,它们不带 module-info.java,但被 jvm 在运行时“降级”为模块参与类加载。它们之间、以及与显式模块之间的交互容易引发变量不可见、服务加载失败、noclassdeffounderror 或 illegalaccesserror 等问题。处理关键在于理解其行为边界,并主动约束交互方式。
识别自动模块与未命名模块的来源
自动模块由传统 JAR 包(无 module-info.class)生成,模块名默认来自 JAR 文件名(如 guava-31.1-jre.jar → 模块名 guava);未命名模块则承载所有通过 -classpath 加载的类(包括启动类、测试类、动态加载类等),它不参与模块图验证,且对所有模块“可见”,但自身无法导出任何包。
常见触发场景:
- Spring Boot 应用中,主启动类在未命名模块,而依赖的第三方 JAR(如 Jackson、Logback)成为自动模块
- Maven 多模块项目里,某子模块未写
module-info.java,编译后 JAR 被其他模块当作自动模块引用 - 使用
java -cp lib/* MyApp启动时,整个 classpath 归入未命名模块
变量/类交互受限的根本原因
Java 模块系统要求:一个类能被另一个模块访问,必须同时满足两个条件——目标类所在包已被导出(exports),且调用方模块已声明依赖(requires)。而自动模块默认导出所有包,但未命名模块**无法声明 requires**,因此它只能访问自动模块的公开类型,却无法被自动模块反向“requires”——这导致双向反射、服务加载(ServiceLoader)、甚至某些代理机制失效。
典型报错示例:
-
java.lang.IllegalAccessError: class com.example.MyProcessor cannot access a member of class com.google.common.base.Preconditions—— 因未命名模块中的类试图访问自动模块中非 public 成员,而模块边界阻止了这种跨模块私有访问 -
java.util.ServiceConfigurationError—— 服务提供者(SPI)定义在自动模块中,但实现类放在未命名模块(如测试类路径),JVM 无法从后者加载前者要求的服务
实战解决策略:分层隔离 + 显式桥接
不强行消除未命名模块(通常不可能),而是将其影响控制在最小范围,并为必要交互提供安全通道:
-
将核心逻辑移入显式模块:为业务代码编写
module-info.java,明确requires所需的自动模块(如requires guava;),并只通过导出的 API 包与外部交互。避免在未命名模块中直接操作自动模块内部类 -
用系统属性或配置文件替代跨模块变量共享:不要让未命名模块(如 main 方法)直接读写自动模块内某个 static 变量。改用
System.setProperty("key", "value")或ConfigurableBeanFactory(Spring 场景)统一注入 -
SPI 服务桥接需双端适配:若自动模块定义了
java.util.spi.LocaleProviderAdapter接口,而实现类在未命名模块(如测试模拟类),应在启动时添加--add-opens:java --add-opens java.base/java.util.spi=ALL-UNNAMED -cp ... MyApp -
避免在未命名模块中使用反射访问自动模块私有成员:这类调用在 JDK 17+ 默认被拒绝。如确需(如单元测试),加 JVM 参数:
--add-opens guava/com.google.common.base=ALL-UNNAMED
构建与运行时检查建议
提前暴露问题比线上崩溃更可控:
- 用
java --list-modules查看实际加载的模块列表,确认自动模块名是否冲突(如两个 JAR 都叫utils) - 运行时加
--illegal-access=deny(JDK 16+ 默认),强制暴露非法反射调用 - Maven 构建时启用
maven-compiler-plugin的模块检查:<compilerargs><arg>--module-path</arg><arg>${project.build.outputDirectory}</arg></compilerargs> - 对 Spring Boot 项目,优先使用
spring-boot-maven-plugin的repackage目标生成可执行 JAR,它会把依赖 JAR 打包进BOOT-INF/lib/,避免 classpath 混乱,间接减少未命名模块干扰










