模块化安全需精准控制exports、requires transitive和opens:仅导出API包(如com.example.api),禁止导出internal/impl子包;requires transitive仅用于API依赖,避免基础设施穿透;反射访问非导出类触发IllegalAccessError,必须用--validate-modules启动验证。

模块化不是加个 module-info.java 就自动安全了;真正的强隔离防护,取决于你是否精准控制了包导出、依赖传递和反射开口。
exports 只导出真正需要的 API 包,其余一律隐藏
未在 exports 中声明的包,连同其所有 public 类,在模块边界外完全不可见——反射调用也会触发 IllegalAccessError。这是 JPMS 最硬的安全栅栏。
- 错误做法:
exports com.example.myservice(导出整个包,哪怕里面混着internal、impl子包) - 正确做法:只导出契约层,例如
exports com.example.myservice.api,而com.example.myservice.internal不导出、不声明、不暴露 - 若必须让某类被测试模块访问,用
opens替代exports,且限定目标模块:opens com.example.myservice.config to com.example.myservice.test
requires transitive 仅用于“API 依赖”,切勿滥用
requires transitive 的本质是把某个依赖“提升为本模块公共契约的一部分”。一旦标记,下游模块就能直接使用该依赖的类型——这既是便利,也是风险敞口。
- 典型误用:对日志、监控等基础设施模块加
transitive,导致业务模块意外获得java.util.logging.Logger或io.micrometer.core.instrument.MeterRegistry的直接构造权限 - 应仅对真正参与 API 签名的依赖启用,例如:你的
com.example.rpc.api模块返回了com.fasterxml.jackson.databind.JsonNode,才需requirestransitive com.fasterxml.jackson.core - 内部工具类、配置解析器、序列化适配器等实现细节依赖,一律用普通
requires,确保它们不会穿透到消费者模块
禁止跨模块直接访问非导出类,包括通过 ClassLoader 或 ServiceLoader 绕过
模块系统默认拦截所有非法跨模块访问,但两类常见路径容易被忽略:
-
ServiceLoader.load(X.class)要求X所在包必须被提供方模块exports,否则加载失败;若需支持 SPI,提供方必须exports接口包,并用provides X with Y显式注册 - 自定义
ClassLoader若未正确设置ModuleLayer,会绕过模块检查——务必继承SecureClassLoader并在defineClass前校验getModule().isExported() - 反射访问非导出类(如
Class.forName("com.example.internal.Helper"))在运行时抛NoClassDefFoundError或IllegalAccessError,不能靠 try-catch 容错,必须从设计上杜绝
编译期与启动期双重验证,别跳过 --validate-modules
JVM 默认只做最小兼容检查。要真正捕获越界访问和封装泄露,必须主动开启验证:
- 编译时加
--module-source-path+--enable-preview(如用新语法),但关键在运行时 - 启动参数加上
--validate-modules --illegal-access=deny,任何隐式反射或非法包访问都会立即失败,而不是静默降级 - CI 流水线中用
jdeps --module-path mods --check com.example.myservice扫描模块间实际引用,确认无未声明的跨包调用
最容易被忽略的是:模块化安全不保护运行时动态生成的字节码(如 CGLIB、Javassist),也不约束 native 方法或 JNI 调用。如果你的二方库含这些能力,必须配合 SecurityManager 或 JVM Sandbox 做第二层围栏。

















