Java 注解处理器怎么避免重复生成:Filer、RoundEnvironment 与增量编译排查
来源:17golang原创
时间:2026-08-18 20:27:20 300浏览 收藏
Java 注解处理器第一次跑通时用着顺手,第二次接入增量编译却常常报 FilerException:同一个源文件明明已经生成过内容,处理器又试图重复写入。这个问题基本不是编译器随机抽风重复调用,多半是开发阶段没把处理轮次、来源元素和输出文件名当成一个整体来设计导致的。
process可能经历多轮,最后一轮没有待处理注解时也可能被调用。RoundEnvironment.processingOver()为真时只做收尾,不再创建新的源文件。Filer.createSourceFile的完整限定名必须稳定且唯一,重复创建应被视为设计错误。- 把“发现元素”和“生成文件”分开,再用来源元素、输出集合和 clean build 复查增量编译行为。
先看清注解处理器为什么会被调用多轮
JSR 269 的处理模型不是“扫描一次就结束”。编译器把源文件中的根元素交给处理器;处理器生成的新源文件可能在下一轮再次成为输入。官方 Processor.process 文档还特别说明:如果处理器被请求参与某轮,后续轮次仍可能调用它,包括最后一轮没有注解的情况。
@SupportedAnnotationTypes("demo.AutoDto")
@SupportedSourceVersion(SourceVersion.RELEASE_25)
public final class AutoDtoProcessor extends AbstractProcessor {
@Override
public boolean process(Set extends TypeElement> annotations,
RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
return false;
}
for (Element element : roundEnv.getElementsAnnotatedWith(AutoDto.class)) {
// 只在这里收集生成计划,稍后统一写文件
}
return true;
}
}
所以,看到同一个元素在日志里出现两次,先不要随便把集合改成“全局只跑一次”的粗暴逻辑。正确做法是确认每一轮的输入和结束标记,再判断输出是否已经存在。

Filer 的文件名就是生成协议的一部分
Filer 负责让处理器创建新的源文件、类文件或辅助资源。以生成 UserDto 为例,输出名应由包名和类型名稳定计算,而不是拼接时间戳或随机后缀:
String packageName = elementUtils.getPackageOf(typeElement)
.getQualifiedName()
.toString();
String generatedName = packageName + "." + typeElement.getSimpleName() + "Dto";
JavaFileObject file = processingEnv.getFiler()
.createSourceFile(generatedName, typeElement);
try (Writer writer = file.openWriter()) {
writer.write("package " + packageName + ";\n");
writer.write("public record " + typeElement.getSimpleName() + "Dto() {}\n");
}
把 typeElement 作为 originating element 传给 createSourceFile,能让工具链知道生成文件由哪个源元素产生。它不允许同一个处理器或另一个处理器再次创建相同限定名;重复写入时抛出的 FilerException 是重要的报错信号。
| 症状 | 优先检查 | 修复方向 |
|---|---|---|
| 同名文件已存在 | 生成名是否含随机值、处理轮次是否重复 | 固定限定名,结束轮次不再写入 |
| 没有生成任何文件 | 支持的注解名和元素筛选 | 核对 @SupportedAnnotationTypes 与 getElementsAnnotatedWith |
| 增量编译结果残留 | 旧 generated-sources 目录和 clean build 差异 | 先清理输出,再比较同一输入的生成结果 |
把发现、去重和生成拆成三个阶段
实际项目里更建议把处理器的主体拆开:第一步只从 RoundEnvironment 收集元素,第二步用完整限定名去重,第三步统一交给一个生成器写入。这样日志可以直接定位“重复来自输入,还是重复来自输出”。
Setplanned = new LinkedHashSet(); for (Element element : roundEnv.getElementsAnnotatedWith(AutoDto.class)) { TypeElement type = (TypeElement) element; String output = outputName(type); if (!planned.add(output)) { processingEnv.getMessager().printMessage( Diagnostic.Kind.NOTE, "skip duplicate: " + output, element); continue; } generate(type, output); }
这个集合只能解决当前轮次内的重复计划,不能替代稳定的源文件命名,也不能覆盖多个处理器之间的协作。若多个处理器都写同一个类型,应重新划分职责,或指定一个处理器拥有该输出文件的写入权限。
最后一轮和增量编译怎么验收
- 在
process开头记录轮次、注解集合数量和processingOver()值。 - 当
processingOver()为真时只输出统计或诊断信息,不调用Filer.createSourceFile。 - 用一次 clean build 生成基线,再只改业务源文件,比较 generated-sources 目录是否只发生必要变化。
- 人为让两个输入映射到同一个输出名,确认日志能定位来源元素,而不是只打印“已存在”。

常见问题
为什么最后一轮还会调用 process?
处理工具需要通知处理器处理阶段已经结束。最后一轮可能没有新的注解元素,所以必须先检查 processingOver(),不能按“有调用就生成”的逻辑处理。
FilerException 是不是只能删目录解决?
删目录只能清理一次残留,不能修复重复生成协议。应先确认完整限定名、处理轮次和多个处理器之间是否写了同一个文件。
为什么要传 originatingElements?
它把生成文件和来源元素关联起来,便于工具链追踪来源、报告诊断并处理增量编译。它不是防重复开关,但缺少它会让排查难度大幅上升。
处理器返回 true 还是 false?
如果当前处理器已经认领了所支持的注解,通常返回 true;返回 false 表示未认领,后续处理器仍可能获得这些注解。这个返回值影响处理器协作,不等于“本轮是否生成文件”。
注解处理器的稳定性,关键不在于把 process 写得更复杂,而在于让输入元素、处理轮次和输出文件名彼此可追踪。先用 clean build 建基线,再观察增量编译,重复生成问题通常很快就能落到具体的一行命名或轮次判断上。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
文章 · java教程 | 20小时前 | 精度 · Java教程 · BigDecimal · 金额计算 · 异常排查 · 精度 ArithmeticException 金额计算 RoundingMode Java BigDecimal.divide455 收藏
-
文章 · java教程 | 1天前 | 密码学 · Java教程 · 安全编程 · Java 25 · 密钥派生 · aes Java 25 KDF HKDF JEP 510 HKDFParameterSpec183 收藏
-
文章 · java教程 | 1天前 | Java教程 · Java 25 · 语言特性 · 模式匹配 · 代码实践 · Switch instanceof Java 25 原始类型模式 JEP 507 when315 收藏
-
文章 · java教程 | 1天前 | 命令行工具 · Java教程 · Java 25 · 语言特性 · 入门实践 · Java 25 Compact Source Files instance main JEP 512 java.lang.IO478 收藏
-
文章 · java教程 | 2天前 | 面向对象 · Java教程 · Java 25 · 语言特性 · 构造器 · Java 25 JEP 513 Flexible Constructor Bodies super 构造器 Java 构造器200 收藏
-
文章 · java教程 | 2天前 | 面向对象 · Java教程 · Java 25 · 语言特性 · 构造器 · Java 25 JEP 513 Flexible Constructor Bodies super 构造器 Java 构造器271 收藏
-
文章 · java教程 | 2天前 | 反序列化 · Java教程 · 安全编程 · Java IO · java Java安全 ObjectInputFilter 反序列化过滤 Serialization Filter257 收藏
-
文章 · java教程 | 2天前 | 反序列化 · Java教程 · 安全编程 · Java IO · java Java安全 ObjectInputFilter 反序列化过滤 Serialization Filter186 收藏
-
文章 · java教程 | 2天前 | 密码学 · Java教程 · Java 25 · HKDF · 密钥派生 · Java 25 KDF Key Derivation Function API HKDF JEP 510 密钥派生147 收藏
-
143 收藏
-
270 收藏
-
文章 · java教程 | 2天前 | Java教程 · Java 24 · 字节码 · Class-File API · JVM工具 · 字节码 Java 24 Class-File API ClassModel ASM替代 java.lang.classfile352 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习