Java原生javac在JDK17+已强制校验密封类闭合性,自动检查permits列表、子类修饰符及sealed子类的permits声明;插件仅用于查漏补缺如非final子类警告;模块系统和运行时校验是必要补充。

Java 原生编译器(javac)已内置对密封类闭合性的强制校验,无需额外插件即可拦截非法子类定义。所谓“配置编译期插件”,本质是强化或补充 javac 的默认行为,而非替代它。
javac 默认就做三件事
只要使用标准 JDK 17+(推荐 JDK 21 或更高),以下检查在编译时自动生效,不依赖任何插件:
- 未出现在 permits 列表中的类型实现密封接口或继承密封类 → 直接报错
- permits 中列出的子类未声明 final、sealed 或 non-sealed → 编译失败
- 被声明为 sealed 的子类,自身又没提供 permits 列表 → 编译失败
真正需要插件的场景:查漏补缺
原生 javac 不检查“本该 final 却没写 final”的设计意图偏差。比如你希望所有 permits 子类都不可再继承,但开发者误写了 non-sealed class X —— 这合法,但违背你的架构约束。这时才需插件辅助:
- 用 Google ErrorProne 配置自定义检查规则,扫描所有密封父类的 permits 子类,若发现非 final 且非 sealed 的实现,发出警告或错误
- 在 Maven 的 maven-compiler-plugin 中启用 -Xplugin:ErrorProne,并加载你写的
SealedSubclassMustBeFinal规则 - 或用 ArchUnit 写测试,在构建阶段验证:所有
@Sealed接口的直接实现类必须带有final修饰符(注意:ArchUnit 运行在字节码层,适合 CI 拦截)
模块系统才是第一道防线
仅靠语法修饰不够。如果第三方把子类打到另一个模块,哪怕没写 implements,也可能通过反射或服务加载绕过限制。所以必须配合:
- 在 module-info.java 中用
requires static声明核心框架模块 - 对密封接口及其 permits 类,全部设为
package-private或通过exports精确控制可见包 - 禁止导出包含密封类型定义的包给不受信模块(如
exports com.example.payment to java.base;是危险的)
运行时兜底:主动拒绝非法类型
编译期不能 100% 覆盖所有绕过手段(比如 ASM 动态生成类)。可在关键入口加一层校验:
- 调用
Class.getPermittedSubclasses()获取当前密封类型的全部许可子类数组 - 在工厂方法或 SPI 加载处,检查传入的 class 是否在该数组中;不在则抛
SecurityException - 此逻辑建议封装为
SealedTypeGuard.checkAllowedSubtype(Shape.class, actual.getClass()),统一复用

















