JPMS 强封装:--add-opens 与快速失败
Java 9 引入模块系统(JPMS)后,跨模块访问 class 有了更强的封装约束。动态代理因为要在目标类所在的包内生成字节码,首当其冲。
APS 对这块的态度是:不硬来,快速失败并给出可操作的提示。
类代理怎么定义类
类代理需要在目标类同一个包 里生成子类(这样才能访问包私有成员、合法 extends)。APS 用的是 MethodHandles.privateLookupIn 拿到目标类的私有 Lookup,再定义隐藏类。
当目标类在一个强封装模块 里时------任何没有 open 的包,包括 java.base 里的 java.util 这种------这个 private lookup 会被 JVM 拒绝。于是 proxy() 立刻失败,并抛出一条带 --add-opens 建议的异常。
一个真实报错
下面这段代码尝试代理 java.util.ArrayList:
java
try {
AcceleratedProxy.proxy(ArrayList.class,
(o, m, a) -> AcceleratedProxy.invokeSuper(o, m, a));
} catch (RuntimeException e) {
System.out.println(e.getCause().getMessage());
}
输出:
text
Cannot access java.util.ArrayList in module java.base (package java.util):
the package is not open to the unnamed module. Add --add-opens
java.base/java.util=ALL-UNNAMED to the JVM arguments, or declare
'opens java.util;' in the module's module-info.java.
这条消息把两件事都告诉你了:为什么失败 (包没 open),以及怎么修。
两种修复方式
方式一:加 JVM 参数 (对 java.base 这种第三方模块常用):
bash
java --add-opens java.base/java.util=ALL-UNNAMED ...
方式二:在目标模块声明 open(你自己的模块):
java
module my.module {
opens com.example.internal; // 把包 open 出来
}
接口代理为什么不受影响
和类代理不同,接口代理用的是 public Lookup ,所以它只支持 public 接口------这和 java.lang.reflect.Proxy 的约束完全一致。非 public 接口代理则走 LookupManager,把生成类定义在接口自己的包里(上一篇讲过)。
所以「JPMS 强封装」这条限制,只针对类代理,接口代理不碰 private lookup。
设计上的取舍
值得点出来的是「快速失败」这个选择。很多库在这里会静默降级(比如偷偷换一个更慢但能工作的路径),结果是用户半年后才发现性能不对。APS 选择当场抛异常 + 明确提示 ,把决策权交给调用方------要么加 --add-opens,要么换接口代理。
这也呼应了本系列第一篇的核心观点:APS 的性能优势来自「直接 INVOKESPECIAL 调用父类方法」,而这要求代理类定义在目标类的包里。JPMS 的强封装直接堵死了这条路,所以必须显式 open,没有中间态。
小结
JPMS 强封装是类代理绕不开的一道坎。APS 的处理是:接口代理不受影响、类代理遇到强封装模块快速失败并给出 --add-opens 提示。理解这一点,你在模块化项目里就能提前规划好哪些包需要 open。
下一篇正式进入原理,回答那个贯穿全系列的问题:invokeSuper 到底是怎么变成 INVOKESPECIAL 的。
本文示例代码见 aps-blog/examples,框架源码与完整文档见 APS 仓库。