一、午夜凶铃:编译器的“手术刀”
想象一下,你正在编写一段Java代码。
@AutoWired
private UserService userService;
你按下 javac 的那一刻,到底发生了什么?
大多数人以为,编译器只是忠实地将 .java 翻译成 .class。错!在词法分析、语法树构建之后,编译器开启了一个“平行时空”。
在这个时空里,有一群“幽灵”正在潜伏——它们就是注解处理器。
它们比JVM更早看到你的代码。它们不需要等待程序运行,就能在编译期直接读取、甚至改写你的代码结构。这是一场与时间的赛跑:处理器必须在编译结束前完成所有逻辑,否则就会被丢进垃圾箱。
这是Java最隐秘的后门,也是Lombok、Dagger、ButterKnife等神器的力量源泉。
今天,我们将深入编译器的腹地,编写一个“全自动API文档生成器”。我们将利用注解处理器,在编译期扫描所有带有 @ApiController 的类,自动生成JSON格式的接口文档。这不仅仅是APT(注解处理工具),这是一场对Java编译流程的“外科手术”!
二、编译期的“第四天灾”
Java编译器(javac)提供了一个插件接口 javax.annotation.processing.Processor。一旦你注册了一个Processor,你就获得了“上帝视角”:
发现阶段:编译器扫描所有源文件,找出你感兴趣的注解。
处理阶段:你读取Element(类、方法、字段),分析它们的元数据。
生成阶段:你甚至可以凭空创造出新的Java文件,并且让编译器接着编译它们!
这导致了一个恐怖的后果:你的项目里最终编译的.class文件,可能根本就不存在于你最初的源码目录中!
这就是为什么Lombok可以在编译期把 @Getter 变成 getXxx() 方法——因为它在编译器看到语法错误之前,就已经把语法树补全了。
三、实战:编写“全自动API文档生成器”
我们将使用 javax.annotation.processing 和 javax.lang.model 包,深入AST(抽象语法树)内部。
核心逻辑:
监听所有带有 @DocumentedApi 的类。
解析类名、方法名、参数类型、返回值。
生成一个 ApiDocGenerator.java 文件,其中包含一个静态方法,返回所有接口的JSON描述。
首先,定义我们的注解:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
标记这是一个需要生成文档的API控制器
*/
@Retention(RetentionPolicy.SOURCE) // 注意:SOURCE级别,运行时不需要,只在编译期存在
@Target(ElementType.TYPE)
public @interface DocumentedApi {
String value() default “”; // API的模块名称
}
接下来,是核心处理器 ApiDocProcessor。
import javax.annotation.processing.;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
/**
API文档注解处理器
这是一个编译期的“潜伏者”,它在.class文件生成前介入
*/
@SupportedAnnotationTypes(“DocumentedApi”) // 声明处理哪种注解
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ApiDocProcessor extends AbstractProcessor {
// 用于打印日志到编译器(如:javac -processor ...) private Messager messager; // 用于操作元素(类、方法、包等) private Elements elementUtils; // 用于生成新的源文件 private Filer filer; // 存储收集到的API信息 private final List apiInfos = new ArrayList(); /** 初始化处理器 在处理注解前被调用,获取关键工具 */ @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); this.messager = processingEnv.getMessager(); this.elementUtils = processingEnv.getElementUtils(); this.filer = processingEnv.getFiler(); messager.printMessage(Diagnostic.Kind.NOTE, "API文档处理器已启动..."); } /** 核心处理逻辑 这里的round非常关键:编译器是分轮次处理的 * @param annotations 请求处理的注解类型 @param roundEnv 当前轮次的环境 @return 是否由本处理器声明这些注解(通常返回true,表示已处理) */ @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { try { // 1. 扫描所有被 @DocumentedApi 标记的元素 for (Element element : roundEnv.getElementsAnnotatedWith(DocumentedApi.class)) { // 确保是类元素 if (element.getKind() == ElementKind.CLASS) { TypeElement typeElement = (TypeElement) element; // 解析这个类的详细信息 ApiInfo apiInfo = parseApiClass(typeElement); if (apiInfo != null) { apiInfos.add(apiInfo); } } } // 注意:process可能会被调用多轮 // 只有在最后一轮(没有更多的注解需要处理时),才生成文件 if (roundEnv.processingOver()) { // 2. 所有扫描完成,生成最终的文档类 generateDocumentation(); } } catch (Exception e) { // 异常处理必须小心,注解处理器中的异常会导致编译失败 messager.printMessage(Diagnostic.Kind.ERROR, "处理器发生致命错误: " + e.getMessage()); e.printStackTrace(); // 这里只是为了调试,正式环境应记录日志 } return true; // 声明已处理,避免其他处理器处理该注解 } /** 解析单个被注解的类 深入AST,提取方法、参数、返回值 */ private ApiInfo parseApiClass(TypeElement classElement) { ApiInfo apiInfo = new ApiInfo(); // 获取类上的注解值 DocumentedApi annotation = classElement.getAnnotation(DocumentedApi.class); apiInfo.moduleName = annotation.value(); apiInfo.className = classElement.getSimpleName().toString(); // 遍历此类的所有成员 for (Element enclosed : classElement.getEnclosedElements()) { if (enclosed.getKind() == ElementKind.METHOD) { ExecutableElement method = (ExecutableElement) enclosed; // 简单过滤,只看public方法 if (method.getModifiers().contains(Modifier.PUBLIC)) { MethodInfo methodInfo = new MethodInfo(); methodInfo.methodName = method.getSimpleName().toString(); methodInfo.returnType = method.getReturnType().toString(); // 解析参数 for (VariableElement param : method.getParameters()) { methodInfo.parameters.add(param.asType().toString() + " " + param.getSimpleName()); } apiInfo.methods.add(methodInfo); } } } return apiInfo; } /** 生成文档文件 这里我们生成一个Java类,它包含一个main方法,打印JSON */ private void generateDocumentation() { try { // 定义生成的类名 String className = "AutoApiDocument"; // 获取包名(通常在META-INF里,或者默认包) String packageName = "com.generated.docs"; // 使用Filer创建一个新的源文件 JavaFileObject builderFile = filer.createSourceFile(packageName + "." + className); try (PrintWriter out = new PrintWriter(builderFile.openWriter())) { // 写入包声明 out.println("package " + packageName + ";"); out.println(); out.println("import com.google.gson.Gson;"); // 假设项目里有Gson out.println("import java.util.Arrays;"); out.println("import java.util.List;"); out.println(); // 写入类声明 out.println("/**"); out.println(" * 由注解处理器自动生成的API文档"); out.println(" * 不要手动修改!"); out.println(" */"); out.println("public class " + className + " {"); // 写入数据存储字段(这里为了简单,直接硬编码JSON字符串) String jsonContent = generateJsonContent(); out.println(" private static final String API_DOC_JSON = " + escapeJava(jsonContent) + ";"); // 写入获取文档的方法 out.println(); out.println(" public static String getApiDocumentation() {"); out.println(" return API_DOC_JSON;"); out.println(" }"); // 写入main方法,用于测试 out.println(" public static void main(String[] args) {"); out.println(" System.out.println(getApiDocumentation());"); out.println(" }"); out.println("}"); } messager.printMessage(Diagnostic.Kind.NOTE, "API文档生成成功: " + className + ".java"); } catch (IOException e) { messager.printMessage(Diagnostic.Kind.ERROR, "生成文件失败: " + e.getMessage()); } } /** 将收集到的ApiInfo转换为JSON字符串 (实际项目中应使用Gson,但这里为了不引入运行时依赖,手写) */ private String generateJsonContent() { StringBuilder sb = new StringBuilder(); sb.append("{n "apis": [n"); for (int i = 0; i methods = new ArrayList(); } private static class MethodInfo { String methodName = ""; String returnType = ""; List parameters = new ArrayList(); }}
四、如何运行这场“手术”?
要让上面的代码跑起来,你需要告诉javac:“嘿,编译的时候带上这个处理器。”
方法一:使用 javax.annotation.processing.Processor 文件
在 resources/META-INF/services/ 目录下创建文件 javax.annotation.processing.Processor,内容为:
ApiDocProcessor
方法二:命令行指定
javac -processor ApiDocProcessor *.java
五、测试用例
编写一个测试类:
@DocumentedApi(“用户模块”)
class UserController {
public UserResponse getUser(Long id) { return null; } public void createUser(UserRequest request) { }}
当你编译时,奇迹发生了:
编译器读取 UserController。
发现 @DocumentedApi,通知 ApiDocProcessor。
处理器解析出 getUser 和 createUser。
处理器生成 AutoApiDocument.java。
编译器继续编译,现在你的输出目录里有了三个class文件(包括那个自动生成的)。
六、深度思考:这究竟是魔法还是诅咒?
注解处理器是一把双刃剑。
优势:
性能无敌:逻辑在编译期完成,运行时零开销(不像反射)。
代码生成:减少样板代码,如ORM映射、Builder模式。
陷阱:
调试地狱:生成的代码看不见,报错时堆栈难以追踪。
轮次控制:如果A处理器生成的代码需要B处理器处理,你需要精确控制RoundEnvironment。
IDE支持:IntelliJ有时无法完美模拟编译器的行为,导致IDE里报红,但mvn compile却通过。
七、结语:掌握“编译期”的力量
你已经看到了,Java不仅仅是运行时的语言,它在编译期就拥有极强的可扩展性。
当你下次使用Lombok的 @Data 时,不要把它当成魔法。那是另一个Java程序,在编译器的“平行时空”里,默默地为你生成了getter和setter。
配置中心宕机了,你的应用或许能靠缓存活几分钟;但如果你的代码在编译期就被注解处理器优化得严丝合缝,那你的应用从诞生的那一刻起,就是完美的。
这才是真正的“高可用”——从字节码层面杜绝隐患。