
1. 注解处理器到底解决了什么问题从样板代码到编译期生成1.1 先回忆一下你写过的那些重复代码做Java开发久了你会发现大量时间其实不是花在“业务逻辑”上而是花在“写样板代码”上。比如一个简简单单的数据类你要写getter、setter、toString、equals、hashCode还要写Builder模式支持链式调用。手写吧啰嗦且容易漏用IDE生成吧字段一变就要重新生成版本管理里一片红。更麻烦的是当你需要为某个接口实现一个自动分发逻辑或者为几十个策略类写注册代码时真的会写到怀疑人生。我之前在一个支付系统里就踩过这种坑。网关需要根据不同的渠道编码去路由到对应的支付处理器一开始是手写switch-case每接一个渠道就要改一次类测试流程长不说还经常漏注册。后来换成Spring的ApplicationContext.getBeansOfType倒是好了一点但框架侵入性强而且启动期反射扫描的内存和耗时在老机器上很明显。当时我就想要是能在编译期自动生成这些注册代码就好了。答案就是Java注解处理器Annotation Processor。它能在javac编译Java源码的阶段读取到你代码里的注解信息然后动态生成新的.java或.class文件这些生成的文件会参与后续的编译流程。换句话说你在源码里写一个Builder注解编译后编译器就自动帮你生成完整的Builder类你写一个AutoRegister注解编译器就自动帮你生成一段“把所有标了该注解的类注册到一个Map里”的代码。1.2 为什么是编译期生成而不是反射或运行时代码生成很多初学者会问这类“根据元数据生成代码”的需求用反射不也能实现吗甚至用CGLIB、ByteBuddy做运行时代码生成不也行这个问题问得好也是理解注解处理器价值的关键。反射的问题在于类型安全差、性能有损耗、泛型信息会擦除而且很多框架在运行时拿不到编译期的结构信息比如源码里的注释、类型参数的边界。你写一个String类型字段的getter反射只能通过Field拿到类型但如果你需要根据字段注解生成特定逻辑反射做起来就很别扭。运行时代码生成CGLIB、ByteBuddy的问题在于生成的字节码是动态的调试时很难看到生成内容而且很多场景比如Android、GraalVM原生镜像对动态字节码支持不友好还会拖慢类加载速度。注解处理器的核心优势是生成的是“实实在在的源码或字节码文件”构建完你就看得见、摸得着、能打断点调试。因为生成步骤发生在编译期最终产物类型安全、无反射开销对性能极其敏感或对启动速度有要求的项目非常友好。还有一个很容易被忽视的好处一个编译报错类因为Java代码是静态类型的生成的代码如果有问题编译期就会直接报错给你。而反射的代码经常是运行到某个接口才抛出NoSuchMethodException排错成本完全不同。这也是为什么Lombok、MapStruct、Dagger、AutoService这些知名库全都选择了注解处理器路线。2. 核心机制拆解Javac如何发现并运行你的处理器2.1 Processor接口与AbstractProcessor生命周期如果你要自己写一个注解处理器一般不会直接实现javax.annotation.processing.Processor接口而是继承AbstractProcessor。这个抽象类帮你屏蔽了一堆细节你只需要关注几个关键方法。一个典型的处理器骨架长这样package com.example.processor; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.TypeElement; import java.util.Set; SupportedAnnotationTypes(com.example.annotation.MyBuilder) SupportedSourceVersion(SourceVersion.RELEASE_17) public class MyBuilderProcessor extends AbstractProcessor { Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); // processingEnv里有几个非常核心的工具对象稍后细说 } Override public boolean process(Set? extends TypeElement annotations, RoundEnvironment roundEnv) { // 核心逻辑拿到所有标注了目标注解的元素生成代码 return false; } }注意到注解了吗SupportedAnnotationTypes声明了这个处理器关心的注解类型SupportedSourceVersion声明了支持的最高Java版本。这两个信息会被javac用来判断“这个处理器是否需要参与这一轮的编译”。process方法的返回值表示“该组注解是否已经被本处理器消费”。一般来说返回false就好或者干脆返回true也行Lombok等复杂处理器会在这里做精细控制。这里补充一个细节处理器的实例化是无参构造但真正初始化要靠init方法。javac会为每次编译创建一个新的Processor实例别把状态存在实例变量里多轮编译时容易踩坑。2.2 处理轮次与多轮处理的执行逻辑注解处理器一个容易被忽略的机制是“多轮处理Rounds”。javac并不是把所有源码一次性喂给处理器而是分多轮进行第一轮解析源码发现有注解A调用关心A的处理器。如果处理器生成了新的源文件javac会把这些新源文件加入到编译队列里然后进入第二轮再次检查是否有需要处理的注解。如果有新注解需要处理则继续直到没有新文件生成或者没有处理器关心的注解为止。这个过程设计得非常巧妙。它意味着你的处理器可以“生成一个文件而这个文件里又有别的注解然后再被另一个处理器处理”。例如我在实际项目里写过一套组合先用处理器A给接口生成实现类实现类上自动标注了AutoRegister注解然后处理器B再去扫描生成的实现类把它们注册进一个Map里。这种链式处理在编译期就完成了运行时完全是干净的普通Java代码。当然多轮处理也有副作用如果你的处理器逻辑写得有bug比如每次编译都生成新文件就会导致无限轮次最终报错“Too many rounds”。我见过一个同事在process方法里没有判断roundEnv.processingOver()导致最后一轮还在疯狂生成文件编译直接卡死。所以process方法的第一步通常要检查roundEnv.processingOver()确认所有源文件都处理完了再收尾。2.3 关键APIElements、Types、Filer、MessagerProcessingEnvironment提供了四个核心对象几乎所有代码生成逻辑都离不开它们。Elements元素操作的入口。你可以通过它拿到一个类的所有字段Element、方法、注解也可以反向通过类名查找对应的TypeElement。比如你要给一个类生成Builder第一步就是通过roundEnv.getElementsAnnotatedWith(MyBuilder.class)拿到所有标注了该注解的类再把这个类强转成TypeElement然后用element.getEnclosedElements()遍历它的字段和方法。Types类型操作的入口。主要是做类型判断和类型包含关系判断比如判断某个类型是不是另一个类型的子类或者提取泛型参数的实际类型。在做序列化器生成时特别有用因为要针对不同类型的字段生成不同处理逻辑。Filer文件生成器。这是整个注解处理器最重要的出口。你想生成新的Java文件不是自己new File然后write而是调用filer.createSourceFile(packageName . className)拿到Writer再往里写内容。用Filer的好处是javac会自动把生成的文件纳入编译队列不用你去管理文件路径和编译状态。Messager日志与报错工具。支持printMessage(Kind.NOTE, ...)、Kind.WARNING、Kind.ERROR。当你输出Kind.ERROR时javac会直接把编译打断并且把这个错误信息定位到具体的源码元素上。这个能力特别适合做“注解参数合法性校验”比在运行期抛出异常友好得多。还有一个相对冷门但实用的对象是processingEnv.getOptions()它可以读取编译命令行里通过-Akeyvalue传入的配置。我在一个实际项目里用它控制是否生成单元测试桩代码默认不生成只有CI构建时传入开关才生成非常好用。3. 从零到一实现一个可用的Builder注解处理器3.1 项目结构与依赖配置代码生成器本身建议拆成独立的Java模块和业务代码分开。因为业务模块编译时依赖它如果混在一起会出现先有鸡还是先有蛋的死锁问题。标准结构是这样my-project ├── builder-annotation // 只放注解定义供业务模块依赖 ├── builder-processor // 注解处理器实现编译期使用 └── business-module // 实际业务代码依赖annotation和processorbuilder-processor的build.gradle核心配置如下dependencies { implementation project(:builder-annotation) // 使用JavaPoet生成Java代码极大简化字符串拼接 implementation com.squareup:javapoet:1.13.0 } // 通过SPI机制注册处理器Java 9也可以直接用AutoServiceMaven下pom.xml的关键片段dependency groupIdcom.squareup/groupId artifactIdjavapoet/artifactId version1.13.0/version /dependency为什么要用JavaPoet而不是手写字符串我最早写处理器的时候也觉得自己new StringBuilder拼代码没问题。但拼了几次就发现代码缩进、泛型、方法链式调用、静态导入这些格式问题能把人逼疯。JavaPoet用面向对象的方式描述类、方法、参数、语句生成的是格式规范、可读性强的Java源码强烈建议使用。3.2 定义注解与Processor的编写先定义一个最简单的注解package com.example.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target(ElementType.TYPE) Retention(RetentionPolicy.SOURCE) public interface MyBuilder { }注意Retention用的是SOURCE因为注解只在编译期用运行期不需要保留这样生成的字节码也更干净。然后是实现处理器的主要逻辑解析目标类的所有字段生成一个静态builder()方法以及一个内部Builder类。package com.example.processor; import com.example.annotation.MyBuilder; import com.squareup.javapoet.*; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.*; import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; import java.util.List; import java.util.Set; import java.util.stream.Collectors; SupportedAnnotationTypes(com.example.annotation.MyBuilder) SupportedSourceVersion(SourceVersion.RELEASE_17) public class MyBuilderProcessor extends AbstractProcessor { private Filer filer; private Messager messager; private Elements elementUtils; private Types typeUtils; Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); this.filer processingEnv.getFiler(); this.messager processingEnv.getMessager(); this.elementUtils processingEnv.getElementUtils(); this.typeUtils processingEnv.getTypeUtils(); } Override public boolean process(Set? extends TypeElement annotations, RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(MyBuilder.class)) { if (element.getKind() ! ElementKind.CLASS) { messager.printMessage(Diagnostic.Kind.ERROR, MyBuilder只能标注在类上, element); return true; } TypeElement typeElement (TypeElement) element; try { generateBuilder(typeElement); } catch (Exception e) { messager.printMessage(Diagnostic.Kind.ERROR, 生成Builder失败: e.getMessage(), element); } } return true; } private void generateBuilder(TypeElement typeElement) throws Exception { String packageName elementUtils.getPackageOf(typeElement).getQualifiedName().toString(); String className typeElement.getSimpleName().toString(); String builderClassName className Builder; List? extends Element fields typeElement.getEnclosedElements().stream() .filter(e - e.getKind() ElementKind.FIELD) .filter(e - !e.getModifiers().contains(Modifier.STATIC)) .collect(Collectors.toList()); // 生成内部Builder类的方法 TypeSpec.Builder builderType TypeSpec.classBuilder(builderClassName) .addModifiers(Modifier.PUBLIC, Modifier.STATIC); // 每个字段对应Builder内部的一个同名字段 for (Element field : fields) { TypeMirror fieldType field.asType(); String fieldName field.getSimpleName().toString(); builderType.addField(FieldSpec.builder(fieldType, fieldName, Modifier.PRIVATE).build()); } // 核心每个字段生成一个setter风格方法返回this for (Element field : fields) { TypeMirror fieldType field.asType(); String fieldName field.getSimpleName().toString(); MethodSpec setter MethodSpec.methodBuilder(fieldName) .addModifiers(Modifier.PUBLIC) .addParameter(fieldType, fieldName) .addStatement(this.$N $N, fieldName, fieldName) .addStatement(return this) .returns(ClassName.get(packageName, builderClassName)) .build(); builderType.addMethod(setter); } // build方法创建目标类实例并赋值 MethodSpec buildMethod MethodSpec.methodBuilder(build) .addModifiers(Modifier.PUBLIC) .addStatement($T instance new $T(), ClassName.get(packageName, className), ClassName.get(packageName, className)) .build(); // 这里为了简化直接为每个字段赋值 MethodSpec.Builder buildMethodBuilder MethodSpec.methodBuilder(build) .addModifiers(Modifier.PUBLIC) .returns(ClassName.get(packageName, className)) .addStatement($T instance new $T(), ClassName.get(packageName, className), ClassName.get(packageName, className)); for (Element field : fields) { String fieldName field.getSimpleName().toString(); buildMethodBuilder.addStatement(instance.$N this.$N, fieldName, fieldName); } buildMethodBuilder.addStatement(return instance); builderType.addMethod(buildMethodBuilder.build()); // 在目标类中写静态builder方法 MethodSpec builderMethod MethodSpec.methodBuilder(builder) .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(ClassName.get(packageName, builderClassName)) .addStatement(return new $T(), ClassName.get(packageName, builderClassName)) .build(); TypeSpec targetClass TypeSpec.classBuilder(className) .addModifiers(Modifier.PUBLIC) .addMethod(builderMethod) .addType(builderType.build()) .build(); JavaFile javaFile JavaFile.builder(packageName, targetClass).build(); javaFile.writeTo(filer); } }这个生成逻辑其实有个关键点如果目标类已经有构造函数或字段初始化逻辑我们不能直接覆盖它否则会冲突。上面为了演示简化了生产环境要考虑是否合并、是否跳过已存在的Builder类。JavaPoet的TypeSpec支持addOriginatingElement来标注这个生成文件来源于哪个元素方便IDE做代码追踪。3.3 Filer与JavaPoet协作生成代码的完整细节上面代码里最后一步javaFile.writeTo(filer)JavaPoet内部会调用Filer.createSourceFile并把生成的包名、类名传进去。这个过程中最需要注意的是包名和类名的一致性如果JavaPoet里写的packageName和ClassName不匹配Filer生成的文件路径编译期不会报错但IDE打开生成文件时会出现红色波浪线。另外JavaPoet对生成文件里的中文、特殊字符也有处理自动转义。我遇到过一次生成代码里包含金额等业务字段注释含中文“¥”因为源文件默认编码是UTF-8JavaPoet会自动转成Unicode转义生成的文件还能正常编译。但如果你绕开JavaPoet直接用Writer就要自己处理文件编码问题。还有一点小技巧生成文件最好在开头加一行“Generated by MyBuilderProcessor, do not edit manually!”这既是给后来维护者看也是给自己排查问题用的。当你在业务代码里看到奇怪的文件时一眼就能知道这是自动生成的避免有人手改了生成内容、下次编译后被覆盖还不明所以。3.4 在Maven/Gradle中配置annotationProcessorPaths注解处理器不是放到classpath就能生效的必须配置为“注解处理器路径”maven-compiler-plugin或Gradle的annotationProcessor配置才能正确加载SPI注册信息。Maven的配置方式build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration release17/release annotationProcessorPaths path groupIdcom.example/groupId artifactIdbuilder-processor/artifactId version1.0.0/version /path path groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.30/version /path /annotationProcessorPaths /configuration /plugin /plugins /build注意这种配置下业务代码本身不依赖builder-processor的jar包不用把它放进compile scope只有编译期才能看到。如果把processor放到了classpath里而工具库又包含依赖冲突处理起来会非常麻烦。让我强调一下annotationProcessorPaths的隔离是故意设计的千万别图省事直接把它加到dependency里。Gradle的配置更简洁dependencies { implementation project(:builder-annotation) annotationProcessor project(:builder-processor) // 使用Lombok也走annotationProcessor配置 compileOnly org.projectlombok:lombok:1.18.30 annotationProcessor org.projectlombok:lombok:1.18.30 }在Gradle里不少人喜欢在IDEA的Settings - Build - Compiler - Annotation Processors里手动开关enable annotation processing。Gradle 8.x默认是能自动识别META-INF/services里的处理器的但如果你在IDEA里做调试时发现处理器没生效优先检查这个开关。3.5 注册SPIMETA-INF/services的正确姿势光有Processor实现类是不够的javac需要知道“这个jar包里有哪些处理器”。Java的SPI机制要求你在META-INF/services/目录下建一个名为javax.annotation.processing.Processor的文件内容就是处理器的全类名每行一个。com.example.processor.MyBuilderProcessor用AutoService可以减少这一步package com.example.processor; import com.google.auto.service.AutoService; import javax.annotation.processing.Processor; AutoService(Processor.class) public class MyBuilderProcessor extends AbstractProcessor { // ... }AutoService会在编译时自动帮你生成META-INF/services文件。这个库本身也是用注解处理器实现的属于“用注解处理器生成注解处理器的注册文件”很有点套娃的趣味。我第一次用的时候发现只要在Processor上标注AutoService构建产物里就自动出现了SPI文件省去了手写配置的可能拼写错误。但这里有个坑AutoService自身需要作为compileOnly依赖加到processor模块里不是annotationProcessor依赖自己否则无法注册。很多人一上来直接写annotationProcessor com.google.auto.service:auto-service:1.0.1结果处理器模块编译后根本没有SPI文件。正确写法是compileOnly加annotationProcessor都加。4. 生产环境实战从模拟案例到大规模应用场景4.1 实战场景一为接口批量生成Mock实现如果你的团队在写单元测试时经常需要Mock一个第三方接口而接口方法又特别多比如一个支付网关接口有十几个方法手写Mock类非常费劲。我参与过一个项目用注解处理器生成了一个自动Mock类定义注解AutoMock标注在接口上。处理器扫描该接口的所有方法为每个方法生成一个默认实现返回null、0或空集合。同时生成一个builder风格的配置入口允许测试代码覆盖特定方法的返回值。生成的Mock类让测试代码从几十行简化为一行PaymentGateway mockGateway MockPaymentGateway.builder() .withPayResult(SUCCESS) .build();这一类代码生成非常适合接口未稳定、方法频繁变动的阶段。接口每新增一个方法重新编译后Mock类自动多一个方法测试代码不需要改动。对比手写Mock最大的便利不是少打字而是“接口变更时的自动同步”。4.2 实战场景二自动注册分发器与工厂模式回到我前面提到的支付网关场景。传统做法有两种一种是手写if-else或switch-case另一种是运行时通过Spring扫描ApplicationContext拿到所有实现类。前者改一次加一次麻烦后者有启动扫描开销且绑死Spring。用注解处理器可以这样解决首先定义注解Handler(channel ALIPAY)。处理器在编译期扫描所有标注了Handler的类分析它们的channel属性。生成一个PaymenChannelRegistry类内部有一个静态Map把所有渠道编码和实现类new出来的实例放进去。业务代码直接PaymenChannelRegistry.get(ALIPAY).pay(...)new操作在编译期生成的代码里类型安全又没有任何反射。这里需要提醒的是处理器只能看到“编译当前模块时扫描到的元素”。如果接口实现类在别的模块里而那个模块没有把注解处理器作为annotationProcessor引用那么当前模块的处理器是扫描不到这些元素的。实践中要么把注册处理器应用到所有相关模块要么约定“实现类都放在同一个模块里”后者更常见、生命周期也更简单。4.3 实战场景三序列化器与类型适配器生成MapStruct、AutoValue这类库的核心逻辑本质上就是“把一种类型映射到另一种类型”的代码生成器。原理很简单用户定义一个接口Mapper public interface UserMapper { UserDTO toDto(User user); }处理器扫描这个接口的所有方法分析入参类型、返回值类型、字段映射关系然后生成一个接口实现类里面是普通Java代码把一个对象的字段赋值给另一个对象的同名字段。我自己还做过一个针对自定义日志协议的序列化器生成器。协议格式是字段名类型字节偏移量手写序列化代码量大且容易错。我定义了一个ProtocolField(order 1, length 4)注解处理器扫描所有带注解的字段生成一个SerializationHelper类包含toBytes和fromBytes方法自动处理字节序、补位、整型与字节数组转换。生成完的代码一目了然还能工具类直接单元测试。这个场景特别能体现注解处理器对“结构性重复”的消化能力。在团队协作上这套方案还有个非常大的好处因为生成的是真实代码代码评审时可以直观看到生成结果是否正确。如果直接用反射或字节码增强review时往往只能看到“调用了一个魔法方法”最终生成什么完全靠猜。5. 常见问题与排查技巧实录5.1 生成了文件但编译报“找不到符号”这是最经典的入门问题。你确认了注解处理器已经跑起来也看到生成文件确实出现在build/generated/sources/annotationProcessor/java/main目录里但业务代码就是在编译期找不到生成的类。可能的原因有三个生成的文件包名不对业务代码import错了。JavaPoet生成的包名和类名务必通过确认target/core处理逻辑来保持一致性。IDE没有把generated sources标记为源目录。Maven和Gradle在命令行下会自动加入但IDEA有时需要手动右键目录Mark Directory as Generated Sources Root。处理器所在的jar没有通过annotationProcessorPaths引用而是混进了编译classpath。这种情况下javac并不会按处理器加载生成行为不稳定推荐严格使用annotationProcessorPaths。5.2 多轮处理导致的ConcurrentModificationException有人会在process方法里直接用roundEnv.getElementsAnnotatedWith()返回的Set进行遍历同时在该方法里又调用了filer.createSourceFile()生成新文件。编译轮次多起来后某些JDK版本上会抛ConcurrentModificationException。原因很简单getElementsAnnotatedWith返回的是一个不可修改的视图底层数据结构在编译器内部可能被修改而你一边遍历一边生成新文件导致内部状态变化。解决办法是先把所有需要处理的元素复制到一个新的List里List? extends Element annotatedElements new ArrayList(roundEnv.getElementsAnnotatedWith(MyBuilder.class));然后基于这个List做遍历和生成就不会出现并发修改问题了。别想当然地以为“我又没有多线程”javac内部的注解处理逻辑是多轮交替执行的容不得这种隐含副作用。5.3 Lombok与自定义处理器共存的血泪坑Lombok也是注解处理器而且它特别霸道。它不是在process方法里生成新文件而是直接修改用户源码对应的抽象语法树AST。如果你的自定义处理器也依赖AST结构比如想读取某个类的所有字段而Lombok已经在前面把某个字段生成好了你的处理器可能拿不到Lombok生成的字段。一个典型的例子你在业务类上同时用了Lombok的Getter和自定义注解AutoMapper自定义处理器想遍历这个类的所有getter方法但处理器看到的是没有getter的原始类因为Lombok对AST的修改发生在很晚的阶段。这个问题没有完美解一般只能约定不要让自定义处理器的逻辑依赖Lombok生成的代码。如果一定要用建议把Lombok的处理器和自定义处理器分开在不同模块使用业务模块只引用最终编译结果。这是我用了很久的妥协方案比耗费大量精力去Debug javac内部AST顺序要靠谱得多。5.4 增量编译导致生成文件残留Gradle从4.7版本开始默认开启增量编译这个机制对编译速度是好事但对注解处理器有个大坑当源文件删除时上一次编译生成的.java文件可能不会被清理。我遇到过一次从代码库删掉了一个带MyBuilder的类重新编译后Build目录里还留着旧生成的Builder文件导致Spring启动时扫描到两个同名的Bean定义报冲突错误。解决方案一是编译前clean简单粗暴但很费时。方案二是在Gradle中配置注解处理器的增量编译支持通过IncrementalAnnotationProcessorType声明你处理的元素类型// 在处理器上声明 IncrementalAnnotationProcessorType(AGGREGATING) public class MyBuilderProcessor extends AbstractProcessor { // ... }AGGREGATING表示这个处理器会影响集合结果适合生成注册中心这类全局类。ISOLATING表示每个元素独立生成文件适合Builder这种逐类生成的应用增量编译失效的风险更低。正确使用这些标注后Gradle就能更智能地判断何时需要重跑处理器。最后一个关于调试的个人建议注解处理器在调试上比普通Java代码要麻烦不能在process方法里随便System.out.println因为javac不一定会把这些输出打到控制台。我强烈建议你熟练使用Messager尤其是printMessage(Kind.MANDATORY_WARNING)级别的输出它一定能显示在编译日志里。开发阶段还可以配合IDE的断点调试在Gradle执行编译时直接挂debug模式操作也简单终端执行gradle compileJava -Dorg.gradle.debugtrueIDE以Remote JVM连接5005端口即可。另一个提升开发效率的土办法是写一个很小的main方法直接手动调用处理器的process方法传入用编译API模拟出来的RoundEnvironment让测试数据集驱动。这个办法不受构建工具限制迭代速度快很多等主流程OK了再回到真正的编译器环境里验证。从最早手动拼接字符串生成代码到后来稳定用JavaPoet再到现在熟练处理多轮编译和增量编译问题。这条路走下来最大的体会是注解处理器真正强大的地方不是帮你“省掉几行代码”而是把“重复的模式化工作”从人的手里转移到了编译器的管道里。它会逼迫你用更严谨的视角去审视类型系统、元素结构和编译流程这对Java开发者的成长帮助很大。如果你正好也在为重复样板代码头疼不妨试试自己写一个处理器从我上面这个Builder的案例开始。