必须用模块边界实现敏感变量物理隔离:ESM/Java/Go均需通过exports、module-info或命名规则使变量编译期不可见;禁用default导出,强制命名导出与封装访问;设api.ts为唯一出口并启用noUnusedExports;高阶场景需ClassLoader级隔离。

要实现敏感业务逻辑变量的物理隔离,不能只靠“把变量改成 private”或加注释提醒,必须借助模块化系统本身的边界机制——exports 不是导出功能的开关,而是划定可见边界的刻刀。核心在于:让变量在编译/加载阶段就“不可见”,而非运行时靠约定不访问。
用模块边界锁死变量访问路径
变量本身没有独立生命周期,它依附于所在模块(文件或包)。只要该模块未通过 exports 显式暴露含该变量的类、对象或命名空间,外部就无法触达。
- JavaScript(ESM)中,一个文件内声明的
const apiKey = 'xxx',若未写export { apiKey }或任何包含它的对象,其他模块import时根本无法获取它——不是报错,是压根不存在这个引用入口 - Java JPMS 中,即使某个类是
public class ConfigHolder,只要它所在的包com.example.internal没在module-info.java中exports com.example.internal,其他模块连编译都过不去,更别说读取其中的静态变量 - Go 语言中,小写字母开头的变量(如
dbConn)天然不导出,哪怕同包其他文件能用,跨包 import 后也完全不可见
禁止默认导出,强制命名约束
默认导出(export default 或 module.exports = ...)容易掩盖真实结构,给误用留缝隙。敏感变量一旦被裹进默认导出的对象里,就可能被解构、扩散、意外缓存。
- 避免:
export default { token, encrypt, decrypt }—— 外部可随意import { token } from './auth',等于直接暴露 - 推荐:
export const getAuthToken = () => secureTokenStore.get();—— 变量不导出,只导出受控的读取函数,内部可做校验、过期判断、动态生成 - 对必须导出的配置项,用带前缀的命名和只读封装:
export const INTERNAL_API_BASE_URL = Object.freeze('https://internal.api/');
分层导出 + 顶层门禁(api.ts / index.ts)
不许底层模块直接对外“开门”。所有导出必须经过统一入口文件二次筛选,形成“导出防火墙”。
- 业务模块目录结构示例:
src/order/<br>├── core/<br>│ └── paymentState.ts // 含 private state: let pendingOrders = new Map()<br>├── adapters/<br>│ └── stripeClient.ts // 含 secretKey 变量<br>└── api.ts // 唯一合法出口
-
api.ts内容:export { validateOrderInput } from './core/validator';<br>export { submitOrderFlow } from './core/flow';<br>// ❌ 不导出 ./adapters/stripeClient 里的任何东西<br>// ✅ 如需调用支付,只导出封装后的 service 方法 - 构建时启用
noUnusedExports,自动剔除未被api.ts引用的导出项,防止遗漏清理
结合加载器级隔离(Java/高级场景)
当变量属于类(如 static CacheManager instance),且需彻底杜绝跨模块共享,仅靠源码级 exports 不够,得升级到类加载器层面。
- 为每个业务模块分配独立
ClassLoader,确保同名类(如com.example.auth.TokenHolder)被加载为 JVM 中互不兼容的类型 - 此时,模块 A 中的
TokenHolder.token和模块 B 中的同名变量,内存地址不同、初始化独立、无法强制转换或传递实例 - 关键前提:关闭双亲委派(设父加载器为
null),并严格隔离各模块的 classpath,否则仍会退化到共用同一个类实例










