Skip to content

编译期互斥校验

MutualExclusionProcessor 是本模块唯一的 AbstractProcessor,职责单一:阻止 @RawResponse 与 @MyApiResponse 出现在同一个元素上。

APT 机制 30 秒

Java APT(Annotation Processing Tool)是 javac 内置的扩展机制。编译器在 round-by-round 的解析过程中,会从 classpath 上读取 META-INF/services/javax.annotation.processing.Processor 列出的处理器,实例化后调用它们的 process(...) 方法,让处理器有机会读取语法树元素、生成新代码或报告错误。

整个机制纯编译期,不影响运行时性能。

SPI 注册

META-INF/services/javax.annotation.processing.Processor 文件内容只有一行:

cn.cvking.forge.processor.check.MutualExclusionProcessor

业务工程在编译时只要把 processor jar 放入 classpath(通过 Maven 依赖即可),javac 就会自动发现并加载它,无需在 pom.xml 额外配置 <annotationProcessorPaths>。这种「依赖即生效」的体验是 SPI 的标准设计。

@SupportedAnnotationTypes 与 @SupportedSourceVersion

java
@SupportedAnnotationTypes({
        "cn.cvking.forge.processor.annotation.MyApiResponse",
        "cn.cvking.forge.processor.annotation.RawResponse"
})
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class MutualExclusionProcessor extends AbstractProcessor {
  • @SupportedAnnotationTypes:声明处理器关心的注解,javac 只会在源码出现这两个注解时才回调 process,避免无效轮询
  • @SupportedSourceVersion:声明支持的 Java 版本。如果使用更高版本编译会得到一条警告,但处理器仍会运行。框架要求 JDK 17+,这里直接写死 RELEASE_17

核心算法:双向扫描

java
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    for (Element element : roundEnv.getElementsAnnotatedWith(MyApiResponse.class)) {
        if (element.getAnnotation(RawResponse.class) != null) {
            messager.printMessage(
                    Diagnostic.Kind.ERROR,
                    "Semantic Error: @MyApiResponse and @RawResponse cannot be used on the same element.",
                    element
            );
        }
    }

    for (Element element : roundEnv.getElementsAnnotatedWith(RawResponse.class)) {
        if (element.getAnnotation(MyApiResponse.class) != null) {
            messager.printMessage(
                    Diagnostic.Kind.ERROR,
                    "Semantic Error: @RawResponse and @MyApiResponse cannot be used on the same element.",
                    element
            );
        }
    }
    return true;
}

实现非常直白:

  1. 第一轮,遍历所有带 @MyApiResponse 的 Element,检查其是否同时存在 @RawResponse
  2. 第二轮,反向遍历
为什么需要双向扫描

单向扫描在大多数情况下足够,但 APT 是分轮次(round)的:第一个 round 可能只解析到带 @MyApiResponse 的源文件,第二个 round 才解析到带 @RawResponse 的源文件。双向扫描可以确保只要任意一轮里两边都出现在同一个 Element 上,就能被捕获。

实际上 Element.getAnnotation(...) 检查的是当前 Element 的元数据,与 round 无直接关系,所以单向已经足够。这里写双向更多是「防御式编程」,代价低、保险高。

错误定位精度

messager.printMessage(kind, msg, element) 的第三个参数 element 决定了错误在源码中的高亮位置。对于方法上的注解,element 是 ExecutableElement,javac 会把整个方法名高亮;对于类上的注解,element 是 TypeElement,会高亮类名行。

IDE 通常订阅 javac 的 Diagnostic 事件,所以这条错误会直接在编辑器里飘红,不必等到完整编译失败才被发现。

不会生成代码

本处理器只走「校验」路径,没有调用 processingEnv.getFiler() 生成任何文件。process 方法返回 true 表示「已经处理过这些注解,后续处理器不必再处理」,对当前用法实际效果与 false 没有差异,因为这两个注解只属于本处理器。

扩展指南

想新增其它互斥规则

只需在 process 方法里追加一组扫描。例如,假设要求 @APIVersion 不能与 @ExceptionHandler 同时出现:

java
for (Element element : roundEnv.getElementsAnnotatedWith(APIVersion.class)) {
    if (element.getAnnotation(ExceptionHandler.class) != null) {
        messager.printMessage(
                Diagnostic.Kind.ERROR,
                "Semantic Error: @APIVersion cannot be used on @ExceptionHandler methods.",
                element
        );
    }
}

记得把 @APIVersion 与 @ExceptionHandler 加进 @SupportedAnnotationTypes,否则 javac 不会回调本处理器。

想做更复杂的元数据生成

例如根据注解生成 OpenAPI 描述文件、生成路由清单 JSON,可以引入 processingEnv.getFiler().createSourceFile(...),但此时建议拆出独立的 Processor 类,避免单类承担过多职责。

为什么不用 Lombok / Hutool 之类的现成框架

Lombok 走的是「修改 AST」路径,使用了 javac 的内部 API(com.sun.tools.javac),对版本兼容性敏感。本模块只做「读取 + 报错」,是 APT 的标准用法,不需要任何第三方依赖。