“内部 API 不要用”这句话靠什么落实?这篇用 JPMS 与 ArchUnit 实测:编译期、运行期反射、测试位置和 classpath 四种场景下边界各自是否还在,以及哪种写法能绕过它。
$ javac provider → 退出码 0
$ javac consumer(Good + Reflect) → 退出码 0
$ javac consumer(Bad,引用 impl.Secret) → 退出码 1
consumer/app/Bad.java:2: error: package impl is not visible
public class Bad { public static void main(String[] a) { System.out.println("取到:" + impl.Secret.token()); } }
^
(package impl is declared in module provider, which does not export it)
1 error
$ java --module consumer/app.Good → 退出码 0
hello world $ javac(全部 classpath) → 退出码 0
$ java -cp out/cp app.Bad → 退出码 0
取到:s3cret $ 默认 → 退出码 0
反射调用公有静态方法 token() 失败:IllegalAccessException: class app.Reflect (in module consumer) cannot access class impl.Secret (in module provider) because module provider does not export impl to module consumer
setAccessible 读私有字段 hidden 失败:InaccessibleObjectException: Unable to make field private java.lang.String impl.Secret.hidden accessible: module provider does not "opens impl" to module consumer
$ --add-exports provider/impl=consumer → 退出码 0
反射调用公有静态方法 token():s3cret
setAccessible 读私有字段 hidden 失败:InaccessibleObjectException: Unable to make field private java.lang.String impl.Secret.hidden accessible: module provider does not "opens impl" to module consumer
$ --add-opens provider/impl=consumer → 退出码 0
反射调用公有静态方法 token():s3cret
setAccessible 读私有字段 hidden:h-42
$ classpath 上运行(不加参数) → 退出码 0
反射调用公有静态方法 token():s3cret
setAccessible 读私有字段 hidden:h-42 $ javac --patch-module provider=test → 退出码 0
$ java --patch-module provider=out/test_patch ... provider/impl.SecretTest → 退出码 0
同模块测试直接访问 impl.Secret:s3cret
$ javac 独立测试模块 testmod(requires provider) → 退出码 1
testmod/t/ExtTest.java:2: error: package impl is not visible
public class ExtTest { public static void main(String[] a) { System.out.println("独立模块测试访问 impl.Secret:" + impl.Secret.token()); } }
^
(package impl is declared in module provider, which does not export it)
1 error $ javac ArchCheck → 退出码 0
$ ArchUnit 检查 out/cp(含 Bad 的直接引用与 Reflect 的 Class.forName) → 退出码 0
导入类数 5;规则违反数 1
违反:Method <app.Bad.main()> calls method <impl.Secret.token()> in ()
对照:只检查 Good 与 Reflect(没有直接引用 impl,只有字符串 "impl.Secret")
$ ArchUnit 检查 out/cp2 → 退出码 0
导入类数 4;规则违反数 0