核心在于封装机制与显式导出策略协同:exports仅开放编译期类型引用,不暴露变量值;敏感字段必须private,通过受控getter返回脱敏或计算结果,禁止opens配置包以防反射窃取。

直接控制模块内部配置变量的可见性,核心在于不让敏感值被外部无意读取或篡改。exports 不是万能开关,它只决定“编译期类型引用”是否可达;真正起作用的是语言层的封装机制 + 模块系统的显式导出策略。
明确 exports 的作用边界:它不导出变量值,只导出可引用的声明
在 Java 9+ 的模块系统中,exports com.example.config 表示允许其他模块 import 这个包里的 public 类——但不会让 config.properties 文件内容、静态字段值或私有常量自动暴露。如果某个配置类长这样:
public class DbConfig {
public static final String URL = "jdbc:postgresql://prod-db/";
private static final String PASSWORD = "s3cr3t!";
}
即使你 exports com.example.config,PASSWORD 字段仍不可见(private),URL 字段虽 public,但只有通过 DbConfig.URL 引用时才生效——这正是可控起点。
实战四步法:从定义到加固
-
第一步:把配置变量封装进不可实例化的工具类或记录类
避免裸露 public static final 字符串。改用 sealed record 或 private 构造器类,强制走 getter 控制逻辑:
public sealed class AppConfig permits AppConfigImpl {}
final class AppConfigImpl extends AppConfig {
private final String dbUrl;
AppConfigImpl(String url) { this.dbUrl = url; }
public String getDbUrl() { return isProduction() ? maskUrl(dbUrl) : dbUrl; }
}
-
第二步:仅导出抽象接口,不导出实现类
在 module-info.java 中只 exports 接口所在包,而非实现包:
module com.example.api {
exports com.example.api.config;
// 不 exports com.example.impl
}
-
第三步:敏感字段绝不 public,用方法代理访问
密码、密钥、内部端口等必须设为 private,并通过受控方法返回脱敏值或运行时计算结果(如加解密后返回)。 -
第四步:禁止 opens 配置包(除非框架强依赖反射)
opens com.example.config; 是高危操作——它允许任意模块通过反射读取 private 字段。若 Jackson 需序列化,优先用 @JsonCreator + 构造器,而非开放整个包。
对比常见错误与安全写法
❌ 错误示范:
module com.example.core {
opens com.example.config; // 允许反射读取所有字段
exports com.example.config; // 同时开放编译引用
}
→ 攻击者可直接反射获取 PASSWORD 字段值。
✅ 安全写法:
module com.example.core {
exports com.example.api.config; // 只导出 ConfigReader 接口
requires com.example.spi; // 依赖 SPI,不暴露实现
}
→ 外部只能调用 ConfigReader.getSafeDbUrl(),无法触达原始字段。
延伸加固建议
- 构建阶段加入 linter 规则,拦截对 *Config.class 中 private static final 字段的跨模块直接引用
- CI 流程中扫描 module-info.java,告警未加限制的 opens 指令
- 运行时使用 SecurityManager(或模块层沙箱)限制 ClassLoader 对敏感包的 getResource 调用










