登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

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;
    }
}

所以,看到同一个元素在日志里出现两次,先不要随便把集合改成“全局只跑一次”的粗暴逻辑。正确做法是确认每一轮的输入和结束标记,再判断输出是否已经存在。

Java 注解处理器按轮次接收 AutoDto 根元素,processingOver 后停止生成分支

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 是重要的报错信号。

症状优先检查修复方向
同名文件已存在生成名是否含随机值、处理轮次是否重复固定限定名,结束轮次不再写入
没有生成任何文件支持的注解名和元素筛选核对 @SupportedAnnotationTypesgetElementsAnnotatedWith
增量编译结果残留旧 generated-sources 目录和 clean build 差异先清理输出,再比较同一输入的生成结果

把发现、去重和生成拆成三个阶段

实际项目里更建议把处理器的主体拆开:第一步只从 RoundEnvironment 收集元素,第二步用完整限定名去重,第三步统一交给一个生成器写入。这样日志可以直接定位“重复来自输入,还是重复来自输出”。

Set planned = 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);
}

这个集合只能解决当前轮次内的重复计划,不能替代稳定的源文件命名,也不能覆盖多个处理器之间的协作。若多个处理器都写同一个类型,应重新划分职责,或指定一个处理器拥有该输出文件的写入权限。

最后一轮和增量编译怎么验收

  1. process 开头记录轮次、注解集合数量和 processingOver() 值。
  2. processingOver() 为真时只输出统计或诊断信息,不调用 Filer.createSourceFile
  3. 用一次 clean build 生成基线,再只改业务源文件,比较 generated-sources 目录是否只发生必要变化。
  4. 人为让两个输入映射到同一个输出名,确认日志能定位来源元素,而不是只打印“已存在”。

Java Filer 生成协议校验:稳定输出名进入唯一集合,重复写入分支被拦截

常见问题

为什么最后一轮还会调用 process?

处理工具需要通知处理器处理阶段已经结束。最后一轮可能没有新的注解元素,所以必须先检查 processingOver(),不能按“有调用就生成”的逻辑处理。

FilerException 是不是只能删目录解决?

删目录只能清理一次残留,不能修复重复生成协议。应先确认完整限定名、处理轮次和多个处理器之间是否写了同一个文件。

为什么要传 originatingElements?

它把生成文件和来源元素关联起来,便于工具链追踪来源、报告诊断并处理增量编译。它不是防重复开关,但缺少它会让排查难度大幅上升。

处理器返回 true 还是 false?

如果当前处理器已经认领了所支持的注解,通常返回 true;返回 false 表示未认领,后续处理器仍可能获得这些注解。这个返回值影响处理器协作,不等于“本轮是否生成文件”。

注解处理器的稳定性,关键不在于把 process 写得更复杂,而在于让输入元素、处理轮次和输出文件名彼此可追踪。先用 clean build 建基线,再观察增量编译,重复生成问题通常很快就能落到具体的一行命名或轮次判断上。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>