JavaPoet 深度解析:编译期注解处理器与 Java 源码生成实战 1. 为什么我们还在聊 JavaPoet第一次接触 JavaPoet 是在做一个跨平台数据同步模块的时候。当时需要根据一份配置描述文件自动生成几十个结构几乎一样的实体类和数据访问接口。手写改一个字段要改十几个文件改完还得逐个核对眼睛都能看花。用反射运行时动态代理倒是能跑但性能损耗和调试难度让人头皮发麻。后来团队里一位老哥甩过来一个链接说“试试这个Square 出的专门用来写 Java 代码的 Java 代码”。那个链接指向的就是 JavaPoet。JavaPoet 是一个用来生成.java源文件的 Java 库。注意它生成的是源代码文件不是字节码也不是运行时动态代理。你可以把它理解成一把“代码雕刻刀”你用 Java 代码描述你想要的类长什么样——包名是什么、有哪些字段、方法签名如何、注解怎么加——它帮你把这些描述渲染成格式规范、缩进正确、导入完整的.java文件。这件事听起来好像没什么大不了但真正在编译期注解处理Annotation Processing、ORM 框架、API 客户端代码生成、协议编解码器这些场景里摸爬滚打过的人都知道能优雅地生成 Java 源码能省下多少重复劳动和人为错误。这篇文章适合谁看如果你正在写注解处理器或者你的项目里存在大量“结构重复但细节不同”的代码需要批量产出又或者你单纯对“如何用代码写代码”这件事感到好奇那接下来的内容应该能给你不少可直接抄作业的参考。我会从整体设计思路讲到核心 API 的实操细节再把我自己踩过的坑和排查经验一并倒出来。全文基于 JavaPoet 的常见实践展开具体版本差异我会在涉及的地方特别说明。2. 整体设计与核心思路拆解2.1 它到底解决了什么问题在没有 JavaPoet 之前想在编译期生成 Java 源码主流做法是拼字符串。比如你要生成一个简单的类StringBuilder sb new StringBuilder(); sb.append(package com.example;\n); sb.append(public class Foo {\n); sb.append( private String name;\n); sb.append(}\n);这种写法在类结构简单时还能忍一旦涉及泛型、通配符、嵌套类、注解参数、静态导入、可变参数字符串拼接就会变成一场灾难。缩进错乱、漏掉 import、泛型尖括号不匹配、注解参数格式写错——每一个问题都足以让你在编译失败时对着生成的源码发呆半小时。JavaPoet 的核心思路是用对象模型描述 Java 元素。它提供了一套与 Java 语言结构对应的类TypeSpec表示类或接口MethodSpec表示方法或构造器FieldSpec表示字段ParameterSpec表示参数AnnotationSpec表示注解JavaFile表示一个完整的源文件。你通过 Builder 模式把这些对象组装起来最后调用JavaFile.writeTo()输出文件。整个过程不需要你关心缩进、换行、import 排序这些琐事JavaPoet 会按照标准的 Java 代码风格帮你处理好。2.2 为什么选择“生成源码”而不是“生成字节码”这是一个经常被问到的问题。用 ASM、ByteBuddy 或者 Javassist 直接生成字节码不是更直接吗为什么还要绕一圈生成源码再编译我的理解是这样的生成源码和生成字节码面向的是完全不同的场景。字节码生成适合运行时增强、代理、AOP 这类需求它的优势是灵活、无需重新编译。但源码生成适合的是编译期代码生成场景比如注解处理器。在注解处理阶段你只能生成新的源文件不能修改已有的类也不能生成字节码。这是 Java 编译模型决定的。JavaPoet 恰好填补了“在注解处理器里优雅地写源文件”这个空白。另外生成源码有一个字节码生成无法比拟的好处可读、可调试、可版本控制。生成的代码就在你的build/generated目录下出问题了直接打开看哪一行不对一目了然。字节码生成出了问题你得用javap反编译或者借助字节码查看工具排查成本高出一个数量级。所以即便在可以用字节码的场景下很多团队仍然倾向于生成源码就是为了那份“看得见摸得着”的踏实感。2.3 核心类模型一览JavaPoet 的类模型设计得很干净几乎每个类都对应 Java 语言中的一个概念。下面这张表是我自己整理的核心类对照关系放在手边随时查很方便JavaPoet 类对应的 Java 概念常用创建方式JavaFile一个.java源文件JavaFile.builder(packageName, typeSpec).build()TypeSpec类、接口、枚举、注解TypeSpec.classBuilder(Foo)MethodSpec方法或构造器MethodSpec.methodBuilder(bar)FieldSpec字段FieldSpec.builder(TypeName.INT, count)ParameterSpec方法参数ParameterSpec.builder(TypeName.INT, count)AnnotationSpec注解AnnotationSpec.builder(Override.class)TypeName类型引用TypeName.get(String.class)CodeBlock代码块CodeBlock.builder().addStatement(...)这套模型的好处是类型安全。你不可能写出一个“字段名包含空格”或者“方法返回类型是 null”的代码因为 Builder 在构建时就会校验。相比字符串拼接这种类型安全带来的信心提升是巨大的。3. 核心细节解析与实操要点3.1 TypeSpec类的骨架怎么搭TypeSpec是整个 JavaPoet 的核心它描述了一个类的完整结构。创建一个类的基本流程是TypeSpec helloWorld TypeSpec.classBuilder(HelloWorld) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addField(FieldSpec.builder(String.class, name, Modifier.PRIVATE).build()) .addMethod(MethodSpec.methodBuilder(sayHello) .addModifiers(Modifier.PUBLIC) .returns(void.class) .addStatement(System.out.println($S), Hello, JavaPoet!) .build()) .build();这里有几个细节值得展开说。修饰符的顺序。JavaPoet 会自动按照 Java 语言规范推荐的顺序排列修饰符你不需要关心public和static谁在前谁在后。但要注意Modifier是javax.lang.model.element.Modifier不是java.lang.reflect.Modifier。这两个类名字一样但包不同导入错了编译器会报错而且报错信息不一定直观。我见过不止一个新手在这里卡住。字段的初始化。FieldSpec.builder()可以接受初始值表达式。比如FieldSpec.builder(String.class, DEFAULT_NAME, Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) .initializer($S, unknown) .build()$S是 JavaPoet 的格式化占位符表示字符串字面量。它会自动帮你加上引号并转义特殊字符。如果你直接写\unknown\虽然也能跑但遇到包含引号或换行的字符串就容易出错。用$S是更稳妥的做法。嵌套类。TypeSpec支持添加嵌套类型通过addType()方法。生成的代码会自动处理嵌套类的缩进和static修饰符如果是接口中的嵌套类JavaPoet 会自动加上public static因为接口成员默认如此。3.2 MethodSpec方法生成的细节陷阱方法生成是日常用得最多的功能也是坑最多的地方。先看一个典型的方法构建MethodSpec main MethodSpec.methodBuilder(main) .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, args) .addStatement($T.out.println($S), System.class, Hello) .build();这里$T是类型占位符它会自动处理 import。比如你写$T并传入System.classJavaPoet 会在文件顶部生成import java.lang.System;实际上java.lang包下的类不需要 importJavaPoet 会智能判断。如果你传入的是List.class它会生成import java.util.List;并在代码中写List而不是java.util.List。这个功能极大地简化了类型引用的处理。控制流语句。JavaPoet 提供了beginControlFlow()、nextControlFlow()、endControlFlow()三个方法来生成 if-else、for、while、try-catch 等结构MethodSpec compute MethodSpec.methodBuilder(compute) .addStatement(int result 0) .beginControlFlow(for (int i 0; i $L; i), 10) .addStatement(result i) .endControlFlow() .addStatement(return result) .build();注意beginControlFlow()的参数是一个格式字符串$L表示字面量literal会直接输出数值。生成的代码会自动处理大括号和缩进。这里有个容易犯的错误在beginControlFlow()里写了完整的for语句包括大括号比如beginControlFlow(for (int i 0; i 10; i) {)。这样会生成两个大括号编译不过。记住beginControlFlow()只写控制流头部大括号由 JavaPoet 自动添加。异常声明。方法可以声明抛出的异常MethodSpec read MethodSpec.methodBuilder(read) .addException(IOException.class) .addException(InterruptedException.class) .build();生成的代码会是throws IOException, InterruptedException。顺序按照你添加的顺序来不会自动排序。3.3 CodeBlock代码片段的复用与组合CodeBlock是 JavaPoet 中一个容易被忽视但极其强大的类。它表示一段可复用的代码片段可以带占位符可以嵌套组合。比如你有一段常用的日志语句CodeBlock logStatement CodeBlock.builder() .addStatement($T.out.println($S $L), System.class, value, $L) .build();然后可以在多个方法里通过addCode()插入。CodeBlock支持$T、$L、$S、$N名称等占位符其中$N特别有用它表示一个方法名或字段名JavaPoet 会检查这个名字是否合法不是关键字、不包含非法字符。占位符速查表占位符含义示例输入输出示例$T类型String.classString$L字面量4242$S字符串hellohello$N名称myMethodmyMethod$$美元符号无$注意$N不会自动加引号它输出的是标识符。如果你传入的字符串包含空格或特殊字符JavaPoet 会抛出异常。这个校验在生成阶段就能发现问题比等到编译时才发现要好得多。3.4 JavaFile最后一步的注意事项JavaFile负责把TypeSpec渲染成完整的源文件。构建方式JavaFile javaFile JavaFile.builder(com.example, helloWorld) .addFileComment(Generated by JavaPoet, do not edit.) .indent( ) .build();indent()可以自定义缩进字符串默认是四个空格。有些团队用两个空格有些用 Tab这里可以统一配置。addFileComment()会在文件顶部添加注释通常用来标注“此文件由工具生成请勿手动修改”。输出到文件javaFile.writeTo(new File(src/main/java));或者输出到Filer注解处理器场景javaFile.writeTo(filer);提示在注解处理器中使用writeTo(Filer)时包名和类名必须与Filer的约束匹配否则会抛出FilerException。常见原因是重复生成同一个类或者包名为空。建议在生成前先检查目标类是否已存在。4. 实操过程与核心环节实现4.1 环境准备与依赖引入JavaPoet 的依赖引入方式取决于你的构建工具。Maven 项目在pom.xml中添加dependency groupIdcom.squareup/groupId artifactIdjavapoet/artifactId version1.13.0/version /dependencyGradle 项目在build.gradle中添加implementation com.squareup:javapoet:1.13.0版本选择上1.13.0 是目前最稳定的版本支持 Java 8 及以上。如果你在用 Java 17 或更高版本这个版本也能正常工作因为它只依赖 JDK 的基础类库没有使用任何被移除的内部 API。注意如果你的项目是 Android 项目并且只在注解处理器模块中使用 JavaPoet建议用implementation而不是api避免把 JavaPoet 传递到主工程中造成不必要的方法数增加。4.2 一个完整的注解处理器示例下面这个例子模拟一个常见场景根据注解自动生成一个Builder类。假设我们有一个Buildable注解标注在类上处理器会为这个类生成一个对应的XxxBuilder类。首先定义注解Target(ElementType.TYPE) Retention(RetentionPolicy.SOURCE) public interface Buildable { }然后编写处理器SupportedAnnotationTypes(com.example.Buildable) SupportedSourceVersion(SourceVersion.RELEASE_8) public class BuildableProcessor extends AbstractProcessor { Override public boolean process(Set? extends TypeElement annotations, RoundEnvironment roundEnv) { for (TypeElement annotation : annotations) { for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { TypeElement typeElement (TypeElement) element; String className typeElement.getSimpleName().toString(); String packageName processingEnv.getElementUtils() .getPackageOf(typeElement).getQualifiedName().toString(); TypeSpec builder generateBuilder(typeElement, className); JavaFile javaFile JavaFile.builder(packageName, builder).build(); try { javaFile.writeTo(processingEnv.getFiler()); } catch (IOException e) { processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR, Failed to write: e.getMessage()); } } } return true; } private TypeSpec generateBuilder(TypeElement typeElement, String className) { String builderName className Builder; TypeSpec.Builder builder TypeSpec.classBuilder(builderName) .addModifiers(Modifier.PUBLIC, Modifier.FINAL); // 为每个字段生成一个私有成员和 setter 方法 for (Element enclosed : typeElement.getEnclosedElements()) { if (enclosed.getKind() ! ElementKind.FIELD) continue; VariableElement field (VariableElement) enclosed; String fieldName field.getSimpleName().toString(); TypeName fieldType TypeName.get(field.asType()); builder.addField(FieldSpec.builder(fieldType, fieldName, Modifier.PRIVATE).build()); builder.addMethod(MethodSpec.methodBuilder(fieldName) .addModifiers(Modifier.PUBLIC) .returns(ClassName.get(typeElement)) .addParameter(fieldType, fieldName) .addStatement(this.$N $N, fieldName, fieldName) .addStatement(return this) .build()); } return builder.build(); } }这个例子虽然简化了很多没有处理泛型、继承、内部类等复杂情况但展示了注解处理器的核心流程遍历被注解的元素、提取信息、构建TypeSpec、输出JavaFile。4.3 参数计算与类型处理在实际项目中类型处理是最容易出问题的环节。TypeName.get(TypeMirror)可以处理大部分情况但遇到泛型、数组、通配符时需要特别注意。泛型类型。假设字段类型是ListStringTypeName.get()会返回一个ParameterizedTypeName。你可以直接用它作为字段类型TypeName listOfString ParameterizedTypeName.get( ClassName.get(List.class), ClassName.get(String.class) );生成的代码会是ListString并且自动 importjava.util.List。数组类型。ArrayTypeName.of(String.class)生成String[]。如果是多维数组可以嵌套调用。通配符。WildcardTypeName.subtypeOf(Object.class)生成? extends ObjectWildcardTypeName.supertypeOf(Integer.class)生成? super Integer。这些在生成泛型方法签名时很有用。类型变量的处理。如果你的类有泛型参数比如class FooT需要用TypeVariableNameTypeVariableName t TypeVariableName.get(T); TypeSpec foo TypeSpec.classBuilder(Foo) .addTypeVariable(t) .build();生成的代码会是class FooT。如果类型变量有上界比如T extends ComparableT可以这样写TypeVariableName t TypeVariableName.get(T, ParameterizedTypeName.get(ClassName.get(Comparable.class), TypeVariableName.get(T)));这里有一个递归引用JavaPoet 能正确处理生成的代码是class FooT extends ComparableT。4.4 生成结果的验证与调试生成代码之后怎么确认它是对的我的做法是分三步走。第一步肉眼检查。打开生成的.java文件看包名、类名、方法签名、import 是否正确。这一步能发现大部分低级错误比如包名写错、方法名拼错、修饰符遗漏。第二步编译验证。把生成目录加入源码路径跑一次编译。如果编译通过说明语法层面没问题。这一步能发现类型不匹配、泛型错误、缺少 import 等问题。第三步单元测试。对生成的类写测试用例验证行为是否符合预期。比如生成的 Builder 类测试它的链式调用是否返回正确的对象。这一步能发现逻辑错误比如 setter 里赋值赋错了字段。实操心得在开发注解处理器时我习惯在process()方法里加一行日志打印出正在处理的类名和生成的类名。这样当生成结果不符合预期时能快速定位是哪个环节出了问题。日志用processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, ...)输出不会干扰编译错误信息。5. 常见问题与排查技巧实录5.1 生成代码编译失败的典型原因下面这张表是我自己整理的问题速查表涵盖了大部分常见故障现象可能原因排查方法解决方案找不到符号缺少 import检查生成的 import 语句用$T占位符代替硬编码类型名泛型尖括号不匹配手动拼接了泛型字符串检查ParameterizedTypeName的使用用ParameterizedTypeName.get()构建方法重复定义多次生成同一个类检查process()是否被多次调用用Filer的重复检查或加缓存包名错误JavaFile.builder()包名传错打印包名日志从Elements.getPackageOf()获取缩进混乱混用了 Tab 和空格检查indent()配置统一用indent( )注解参数格式错误手动拼接注解字符串检查AnnotationSpec的使用用AnnotationSpec.builder()构建5.2 注解处理器中的“轮次”陷阱注解处理器有一个容易被忽视的机制处理轮次round。编译器会多次调用process()每一轮处理新生成的源文件。如果你在第一轮生成了一个类这个类又包含被同一注解标注的元素第二轮就会再次触发处理可能导致无限循环。避免这个问题的办法是在生成新类时不要在新类上使用触发处理的注解。或者用roundEnv.processingOver()判断是否已经是最后一轮只在最后一轮执行生成逻辑。我个人的习惯是在process()开头加一个判断if (roundEnv.processingOver()) { return false; }这样能确保只在非最终轮执行生成避免重复生成。5.3 类型镜像与类名的转换坑TypeMirror和ClassName之间的转换是另一个高频出错点。TypeName.get(TypeMirror)在大多数情况下能正确工作但遇到以下情况时需要特别小心原始类型int、boolean等基本类型TypeName.get()返回TypeName.INT等常量可以直接用。void 类型TypeName.get(void.class)返回TypeName.VOID但TypeName.get(TypeMirror)对 void 的处理可能因 JDK 版本而异。建议直接用TypeName.VOID。嵌套类ClassName.get(com.example, Outer, Inner)生成Outer.Innerimport 的是com.example.Outer。如果嵌套层级更深继续添加参数即可。避坑技巧当你从TypeMirror获取类型时如果拿不准转换结果可以先打印typeMirror.toString()看看原始表示再决定用哪种TypeName构建方式。这个习惯帮我省了很多调试时间。5.4 生成代码的可读性优化JavaPoet 默认生成的代码已经相当规范但有些细节可以进一步优化。import 排序。JavaPoet 会按照字母顺序排列 import但不会区分静态 import 和普通 import。如果你希望静态 import 单独一组需要自己后处理。不过大多数情况下默认行为已经足够。空行控制。JavaPoet 会在字段、方法、内部类之间自动添加空行但不会在方法体内的语句之间添加。如果你希望某些语句块之间有空行可以在CodeBlock中手动添加\n。注释生成。TypeSpec、MethodSpec、FieldSpec都支持addJavadoc()方法添加 Javadoc。Javadoc 内容也支持占位符MethodSpec.methodBuilder(foo) .addJavadoc(Returns the $N of this object.\n, name) .addJavadoc(return the name\n) .build()生成的 Javadoc 会自动处理缩进和星号对齐。这个功能在生成 API 客户端代码时特别有用可以把服务端的接口文档直接嵌入到生成的代码中。5.5 性能考量与大规模生成当需要生成成百上千个类时性能就成了一个需要考虑的问题。JavaPoet 本身的性能很好瓶颈通常出现在Filer的 IO 操作上。我的经验是批量生成时尽量在一次process()调用中完成所有类的构建然后统一写出。避免频繁调用writeTo()。如果生成的类之间有依赖关系注意处理顺序。JavaPoet 不负责依赖解析你需要自己保证被依赖的类先被Filer创建。对于超大规模生成比如上千个类可以考虑用多线程构建TypeSpec但写出操作必须串行化因为Filer不是线程安全的。实测数据在一个生成约 500 个实体类的项目中纯构建TypeSpec的时间大约在 200 毫秒左右而写出文件的时间占了总耗时的 80% 以上。所以优化重点应该放在减少文件 IO 次数上比如合并生成、延迟写出等。6. 进阶用法与扩展思路6.1 生成 Kotlin 代码JavaPoet 本身只支持 Java 源码生成。如果你的项目需要生成 Kotlin 代码可以考虑 KotlinPoet它是 JavaPoet 的 Kotlin 版本API 设计非常相似。如果你已经熟悉了 JavaPoet 的模型迁移到 KotlinPoet 的成本很低。不过 KotlinPoet 是另一个库需要单独引入依赖。6.2 与模板引擎的对比有人会问用 FreeMarker、Velocity 这类模板引擎不是也能生成代码吗为什么还要用 JavaPoet我的看法是模板引擎适合生成文本JavaPoet 适合生成代码。区别在于JavaPoet 理解 Java 语言的语法结构它能保证生成的代码在语法层面是正确的。模板引擎只是字符串替换你写错了模板它就生成错误的代码而且错误可能在编译时才暴露。JavaPoet 在构建阶段就能发现很多问题比如非法的标识符、类型不匹配等。对于代码生成这种对正确性要求极高的场景JavaPoet 的类型安全是很大的优势。当然模板引擎也有它的优势学习成本低、模板和代码分离、非 Java 开发者也能修改模板。选择哪个取决于你的团队构成和具体需求。如果生成逻辑复杂、对正确性要求高JavaPoet 更合适如果只是简单的文本替换模板引擎可能更轻量。6.3 在 Android 项目中的实践Android 项目中 JavaPoet 的典型应用是生成路由表、生成Parcelable实现、生成依赖注入代码等。由于 Android 的方法数限制生成代码时要注意控制生成的方法数量。另外Android 的注解处理器通常运行在annotationProcessor配置下JavaPoet 的依赖应该加在处理器模块中而不是implementation到主工程。一个常见的坑是在 Android 库模块中使用 JavaPoet 生成代码时生成的类会被打包进 AAR。如果多个库模块生成了同名的类会导致冲突。解决办法是给生成的类加上模块前缀或者使用Filer的createSourceFile()时指定唯一的类名。6.4 生成代码的版本兼容性JavaPoet 生成的代码默认兼容 Java 8。如果你需要生成使用了 Java 11 或 Java 17 新特性的代码比如var、record、sealedJavaPoet 本身不直接支持这些语法。你需要用CodeBlock手动拼接或者升级到支持新语法的版本。不过在实际项目中生成代码通常追求最大兼容性所以用 Java 8 语法生成反而是更稳妥的选择。个人建议除非有明确的版本要求否则生成代码时尽量使用 Java 8 兼容的语法。这样生成的代码可以在更广泛的环境中编译和运行减少因版本差异导致的问题。7. 我踩过的那些坑说几个印象深刻的翻车经历希望能帮你省下一些调试时间。第一个坑是关于$N占位符的。有一次我生成一个方法方法名是从配置里读的配置里写的是get Name结果生成的方法名是getName看起来没问题。但后来配置改成了get name中间多了个空格JavaPoet 直接抛异常了。当时没反应过来以为是 JavaPoet 的 bug查了半天才发现是$N对非法标识符的校验。这个校验其实是好事它把问题提前暴露了而不是生成一个编译不过的文件让你去猜。第二个坑是关于beginControlFlow()的。我一开始习惯写beginControlFlow(if (x 0) {)结果生成的文件里有两个左大括号编译报错。后来仔细看文档才发现beginControlFlow()只写控制流头部大括号是自动加的。这个错误我犯了不止一次因为肌肉记忆太强了写 if 语句总想带上大括号。第三个坑是关于注解处理器的轮次的。我写了一个处理器生成的新类上又带了触发注解结果处理器无限循环编译一直不结束。当时以为是编译器卡死了重启了好几次 IDE。后来加了processingOver()判断才解决。这个问题的教训是生成的新类上不要使用触发当前处理器的注解否则会陷入死循环。第四个坑是关于TypeName.get()的。有一次处理一个泛型字段类型是MapString, ListIntegerTypeName.get()返回的类型名在生成代码时丢失了部分泛型信息生成的代码变成了MapString, List。排查后发现是TypeMirror在某个环节被擦除了。解决办法是用DeclaredType手动提取类型参数逐个构建ParameterizedTypeName。这个问题的根源是 Java 的类型擦除机制在注解处理阶段有些类型信息确实无法完整获取需要做降级处理。这些坑的共同点是它们都不是 JavaPoet 本身的缺陷而是对 Java 语言机制和 JavaPoet API 理解不到位导致的。每次踩坑之后我对 JavaPoet 的理解都会更深一层。现在回头看这些调试经历本身就是很好的学习过程。8. 一些实用的小技巧最后分享几个我日常使用中总结的小技巧都是能直接提升效率的。技巧一用CodeBlock做代码片段复用。如果你在多个方法里需要插入相同的代码片段比如参数校验、日志记录把它封装成CodeBlock用addCode()插入。这样修改时只需要改一处所有生成的地方都会同步更新。技巧二用AnnotationSpec的toBuilder()做注解变体。如果你有一个基础注解需要在不同场景下添加不同的参数可以用toBuilder()基于已有注解构建新注解避免重复代码。技巧三生成前先检查目标文件是否存在。在注解处理器中如果目标文件已经存在比如上一次编译的残留Filer会抛出异常。可以在生成前用processingEnv.getFiler().getResource()检查或者捕获异常后忽略。不过更推荐的做法是清理生成目录后再编译避免残留文件干扰。技巧四用JavaFile.toString()做单元测试。JavaPoet 的JavaFile重写了toString()返回完整的源码字符串。你可以在单元测试中直接断言生成的代码内容不需要真的写文件。这样测试跑得快也不依赖文件系统。技巧五给生成的类加上Generated注解。虽然 Java 9 之后javax.annotation.Generated被移除了但你可以自己定义一个Generated注解标注在生成的类上。这样静态分析工具和 IDE 能识别出这是生成代码减少误报的警告。这些技巧看起来不起眼但在实际项目中能省下不少时间。尤其是第一个和第四个我几乎在每个用到 JavaPoet 的项目里都会用到。写到这里关于 JavaPoet 的核心内容基本覆盖了。从最初的字符串拼接到现在的类型安全代码生成这个库帮我解决了很多重复劳动的问题。如果你正在做代码生成相关的工作建议从一个小场景开始尝试比如生成一个简单的 Builder 类感受一下它的工作方式。一旦上手你会发现它能做的事情远比想象中多。