Java模块化系统通过module-info.java在编译期和运行时施加静态边界约束,类加载器仅在该边界内工作;exports控制类加载可见性,opens管控反射访问,requires transitive解决传递依赖。

Java 模块化系统对类可见性的控制,不是靠类加载器“动态决定谁能看到谁”,而是由 module-info.java 在编译期和运行时共同施加静态边界约束,类加载器(如 ModuleLayer 下的 ModuleClassLoader)只在该边界内工作。换句话说:模块系统不改变类加载机制本身,而是给类加载过程套上一层可见性过滤器。
类加载与模块边界的协同关系
JVM 启动后,模块系统构建 ModuleLayer,每个模块被分配独立的 ModuleClassLoader(或共享的 PlatformClassLoader / AppClassLoader 的模块感知变体)。类加载仍遵循双亲委派,但关键变化在于:
-
加载前校验:当模块 A 的代码尝试通过
Class.forName("com.example.X")加载类时,JVM 不仅检查类是否存在,还会检查:-
com.example.X所在的模块是否已被requires; - 该类所在的包(如
com.example)是否被目标模块exports给模块 A; - 若未导出,直接抛
ClassNotFoundException(不是IllegalAccessError),连类都加载不到;
-
-
反射加载也受控:
Class.forName()和ClassLoader.loadClass()都遵守 exports 规则;而Class.getDeclaredField()等反射操作,则额外受opens约束——没opens,即使类已加载成功,也无法访问私有成员。
用 module-info.java 实现三层可见性控制
真正起作用的不是类加载器代码,而是你在 module-info.java 中写的三类声明,它们分别拦截不同阶段的“可见请求”:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
-
exports:拦在类加载入口
只有被exports的包,其 public 类才可能被其他模块的new、import、Class.forName()成功加载。
例如:module my.service { exports com.my.service.api; // 其他模块才能 new ServiceImpl() // 但 com.my.service.internal 包下的类,外部连名字都不知道 } -
opens:拦在反射访问入口
exports不管反射;opens是专为反射开的后门,且可精确到模块:立即学习“Java免费学习笔记(深入)”;
opens com.my.service.model to com.fasterxml.jackson.databind; // Jackson 模块能反射读写该包内私有字段;其他模块不行
-
requires transitive:拦在传递性引用入口
当你的模块导出了一个类型,而该类型继承自第三方模块的类(如extends java.sql.Timestamp),调用方必须能“看到”那个父类——这就需要依赖传递:module my.db { requires transitive java.sql; // 调用方无需再写 requires java.sql exports com.my.db.entity; }
常见误区与验证方式
- ❌ 误以为“public 类就一定可跨模块使用”
→ 错。public只解决类内部可见性;跨模块需exports+requires双向确认。 - ❌ 把
module-info.java放错位置(如放进src/main/java/com/example/)
→ 编译器找不到模块定义,整个项目退化为传统 classpath 模式,所有exports失效。 - ✅ 快速验证是否生效:
- 编译期:故意删掉
requires或exports,看是否报package is not visible; - 运行时:用
Class.forName()加载非导出包中的 public 类,观察是否ClassNotFoundException; - 反射时:用
Jackson反序列化未opens的类,看是否IllegalAccessException。
- 编译期:故意删掉
不复杂但容易忽略:模块化下的类可见性,本质是一张由 module-info.java 定义的“白名单地图”,类加载器只是严格按图索骥的执行者——画错地图,再好的加载器也无济于事。

















