bigywater.tech
Java · 383

模块边界测试:防止内部 API 越界使用

“内部 API 不要用”这句话靠什么落实?这篇用 JPMS 与 ArchUnit 实测:编译期、运行期反射、测试位置和 classpath 四种场景下边界各自是否还在,以及哪种写法能绕过它。

javac
直接引用未导出包时的拒绝点:package impl is not visible
exports vs opens
--add-exports 只让公有成员的反射调用成功,私有字段要 --add-opens
1 vs 0
ArchUnit 对直接引用与 Class.forName 字符串引用报告的违反数

编译期边界

实测 · 本机执行输出
  $ 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

classpath

实测 · 本机执行输出
  $ 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

ArchUnit 规则

实测 · 本机执行输出
  $ 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

分步演示

模块边界测试
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12 的 javac / java;ArchUnit 1.3.0;模块与类都是自写的最小例子
    • 没有验证:Maven / Gradle 的 module-info 测试配置(Surefire 的 patch-module 设置)、jlink / jmod、ServiceLoader 在模块里的 provides / uses、自动模块(Automatic-Module-Name)、–illegal-access 在 JDK 17+ 的变化、Spring 等框架对 opens 的需求
    京ICP备2026067502号-1京公网安备11010502063487号