idea配置java注解处理器

IntelliJ IDEA 和 Gradle:为什么每个子模块有 3 个模块?

如果您只想为之前导入的项目禁用此选项,您可以通过编辑位于**.idea/gradle.xml 中的** idea gradle 配置文件来实现 。

添加将resolveModulePerSourceSet设置为false 的这一行:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
...
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
...
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

然后刷新gradle项目。

问题

在idea+gradle的环境中使用annotation processor生成代码,但是代码生成在了build文件夹下的classes里,且程序无法引用生成的类,若强行使用则报错找不到类。

第一步 配置idea

file -> settting -> Build,Execution,Deployment -> compiler -> annotation processor 进入此界面,然后:

  1. 勾上启用注解处理器
  2. 选中从项目类路径获取处理器
  3. 选中模块项目根
  4. 生产源目录输入../main/src/generated/java

第二步 配置gradle

这个项目有三个module:

一个是main 测试ProcessorLib库

一个是ProcessorLib 处理注解

一个是AnnotationsLib 定义注解

  • ProcessorLib 的gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//使编译时将文件生成到指定目录
compileJava {
//配置编译时生成代码的目录
options.compilerArgs << "-s"
options.compilerArgs << "$projectDir/src/main/generated/java"
//确保文件夹存在
doFirst {
file(new File(projectDir, "/src/main/generated/java")).mkdirs()
}
}
//在clean时删除编译生成的代码
clean.doLast {
// clean-up directory when necessary
file(new File(projectDir, "/src/main/generated")).deleteDir()
}
//依赖
dependencies {
implementation project(path: ':AnnotationsLib')//自己的注解定义module
implementation 'com.google.auto.service:auto-service-annotations:1.0.1'//autoservice
annotationProcessor 'com.google.auto.service:auto-service:1.0.1'//autoservice
implementation 'com.squareup:javapoet:1.13.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2'
}

//详细日志打印,没有调用处理器,可能是跳过了,直接进行编译了
// 参数可选,重点是 -verbose -XprintRounds -XprintProcessorInfo
allprojects {
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint" << "-verbose" << "-XprintRounds" << "-XprintProcessorInfo" << "-Xmaxerrs" << "100000"
}
}
}
  • 在main module的gradle中
1
2
3
4
5
6
7
dependencies {
implementation project(path: ':ProcessorLib')
annotationProcessor project(path: ':ProcessorLib')
implementation project(path: ':AnnotationsLib')
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2'
}

第三步 配置文件夹类型

在执行gradle的build任务后,会在src/main下生成指定目录以及代码,但是在我们的源文件中依然没有提示,那么我们需要指定其文件夹类型。

前面用gradle生成的目录:src/main/generated/java

把这个目录右键,将目录标记为“生成文件夹的根目录”

需要注意的细节

  • 在编写自定义Processor可能会出现处理器不起作用的情况
    答:其很有可能是你将Processor.class写成了Process.class
1
2
3
4
//正确的写法
@AutoService(Processor.class)
//错误的写法
@AutoService(Process.class)

即使是按照上面的步骤配置,但仍然无法引用生成的代码(删除线即当时认知错误,在第4点会讲原因)
答:可能是由于你生成代码的文件夹与你的源文件不在一个module中,由于idea中使用gradle创建一个module,他会识别为三个module。

两种解决方式:

1.开头部分。

2.在创建项目时取消 create separate per source set

  • annotation processor 生成类时报异常:错误: 类重复: com.cxyz.test.Test
    答:其实annotation processor只能生成额外的类,而不能在原先类的基础上做改动

写过自定义注解处理器的老司机们乍一看这个问题觉得挺简单,是的,因为网上基本通篇都在教你怎么打日志,但是你有没有想过如果连日志都打印不出来的时候你怎么定位呢?譬如如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 确认 META-INF/services/javax.annotation.processing.Processor 没问题
// 确认构建脚本没问题,确认注解 Bridge 有被使用且有参与构建
@AutoService(Processor.class)
public class TestAnnotationProcessor extends AbstractProcessor {
public TestAnnotationProcessor() {
System.out.println("TestAnnotationProcessor constrator");
}

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
System.out.println("TestAnnotationProcessor init");
}

@Override
public Set<String> getSupportedAnnotationTypes() {
System.out.println("TestAnnotationProcessor getSupportedAnnotationTypes");
Set<String> supported = new HashSet<String>();
supported.add(Bridge.class.getCanonicalName());
return supported;
}

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
System.out.println("TestAnnotationProcessor process");
return true;
}
}

运行构建后compileReleaseJavaWithJavac过程中没有先吐我 Annotation Processor 的任意一行日志,直接报错找不到我注解处理器产物类引用(即直接进行了 compile class 环节)。

你懵逼吗?反正我懵逼了!打印日志不好使了,哈哈,环境确认没问题,什么鬼,直接越过 Annotation Processor 进行 compile 了。

这时候就需要你稍微深入定位分析(撸javac源码的巨佬请自行飘过),前提就是你需要熟悉下 Annotation Processor 基本原理,然后我们通过一些额外的javac详细日志进行举例分析。

Annotation Processor 机制
注解和注解处理器是 JDK5 引入的机制,主要用来为类、方法、字段和参数等 Java 结构提供额外的信息。譬如常见的@Override就是仅仅对 Java 编译器生效的一个注解。Java 允许我们自定义注解,自定义的注解处理器就是用来处理这些自定义注解的(废话),注解处理器触发时机是由javac来处理的,所以整个javac过程的简要步骤如下图:

![在这里插入图片描述](TyporaRaw/idea 注解处理器.assets/20210111193442602.png)

可以看到,javac编译概要图主要分为如下几步:

把源文件解析为抽象语法树。
调用已注册的注解处理器。
如果注解处理器处理过程中生成了新的源文件,编译器重复第 1、2 步,当注解处理器不再生成新的源文件则进入最后一轮。
进入真正的 compile 字节码环节生成字节码。
如上就是注解处理器的核心机制,有了这个核心机制的认识我们就继续往下探索。

构建工具下 Annotation Processor 的本质
我们日常开发中(无论是 Java 后端还是 Android 移动端)总是多多少少会用到 JDK 提供的annotation processor能力,无论是什么构建工具(Gradle 或者 Maven 等)本质都是通过javac -processorpath命令参数显式指定哪些 Processer,或者显式声明META-INF/services/javax.annotation.processing.Processor来被javac发现并调用的(参见 google 的 AutoService 框架)。

正常情况下我们开发中使用及构建 Annotation Processor 技术都是上面几步走的方案,而且大多数照着网络上抄的都能正常工作,每次只用自己处理 process 就挺香的,因为只要按照规则声明放置,其他的 javac都能自己完美调用。

增强 javac 过程打印暴露问题
要解决一开始说的 Annotation Processor 中自己加的日志都不打印场景问题,我们需要获取一些额外的信息辅助定位。由于直接使用命令行javac的方式是最原始的操作,我们构建一般采用 Gradle,而 Gradle 的本质还是调用javac,所以下面我们以 Gradle 为例来分析如何定位 Annotation Processor 问题。

下面简单粗暴点就是直接在根目录的build.gradle中给所有模块添加参数:

1
2
3
4
5
6
7
8
9

// 参数可选,重点是 -verbose -XprintRounds -XprintProcessorInfo
allprojects {
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint" << "-verbose" << "-XprintRounds" << "-XprintProcessorInfo" << "-Xmaxerrs" << "100000"
}
}
}

你也可以仅仅在自己有注解处理器的模块中添加,与上面一样,只要加给JavaCompile的参数就行。这里的参数其实就是我们平时命令行javac是否的参数,不懂的可以去命令行执行下javac -help观摩下含义吧,如下(JDK8,不同版本 JDK 略有差异):

1
2
3
4
5
6
7
8
9
10
yan@yanDeMackbookPro:~$ javac -help
用法: javac <options> <source files>
其中, 可能的选项包括:
-g 生成所有调试信息
......
-verbose 输出有关编译器正在执行的操作的消息
......
-processor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
-processorpath <路径> 指定查找注释处理程序的位置
......

至于脚本中其他几个在javac -help中没有的参数可以看下官方文档https://docs.oracle.com/en/java/javase/11/tools/javac.html ,里面详细解释了参数含义。

添加上面参数后一定要将你的构建日志追加到一个磁盘文件中,因为日志会变得非常庞大,同时也变得很容易定位问题。

通过构建日志分析定位问题
执行你的构建任务,完毕后分析定位主要分为如下几个步骤,每一步都是一种场景的定位,循序渐进定位分析即可。

在你的日志中搜索你的 Processor 类名,譬如TestAnnotationProcessor.class,看到的日志会是如下。

1
2
3
4
5
// 如果你的注解处理器在项目中是源码形式的日志
[loading RegularFileObject[/home/user/yan/test/target/classes/cn/yan/test/TestAnnotationProcessor.class]]

// 如果你的注解处理器在项目中是依赖 jar 形式的日志
[loading ZipFileIndexFileObject[....../test.jar(cn/yan/test/TestAnnotationProcessor.class)]]

分析: 如果你的日志中搜不到上面信息,说明你的注解处理器没有被添加到javac的 classpath 中。一般问题就是你的META-INF/services/javax.annotation.processing.Processor声明有问题,javac无法找到你的注解处理器。有些同学可能是通过 google 的 AutoService 来生成META-INF/services/javax.annotation.processing.Processor的,这种情况下也要自己检查是否 OK(譬如之前安卓中 AGP 有一段时间的中间过渡版本就修改了 classpath,需要手动将 compile 改成 annotationProcessor 才行)。

在你的日志中搜索Round关键字,建议直接搜Round 1:这样的格式容易点,看到的日志会是如下。

1
2
3
4
Round 1:
input files: {cn.yan.test.Application, ......, cn.yan.test.UseMarkedAnnotation}
annotations: [java.lang.Override, cn.yan.annotation.Bridge]
last round: false

上面日志中的input files:部分是扫到的你的源码,annotations:部分就是扫到你代码中使用了哪些注解,如果你注解处理器声明了要处理这种注解(譬如@cn.yan.annotation.Bridge),则日志如上才是正常的。

分析: 如果你日志中没搜到上面的Round,则说明javac没有触发调用任何注解处理器(无论是你写的还是依赖三方框架的),最大的可疑点就是检查下自己有没有禁用javac注解处理器,也就是确认javac执行时没有-proc:none参数。如果你的日志中有Round,但是input files:和annotations:没有你的注解类和使用类,则说明你没有在代码中使用你注解处理器要处理的注解。

在你的日志中搜索Loaded cn.yan.test.TestAnnotationProcessor关键字,看到的日志会是如下。

1
[Loaded cn.yan.test.TestAnnotationProcessor from file:/home/user/yan/test/target/classes/cn/yan/test/TestAnnotationProcessor.class]

分析: 如果你看不到上面这行日志,说明你的注解处理器类自己没有被加载成功,为什么没有我也不知道怎么分析了,但是至少说明没加载成功,你可能需要仔细核对哪里不规范或者不合法导致的了。

上面都排查完了,如果还是找不到问题原因,不妨换个思路,去仔细检查下你参与构建的普通 java 文件,是否存在语法错误或者什么问题(譬如常量没声明等);如果有,解决完了再试试,别问我为什么,我也没有深入研究javac这块源码,只是我遇到过,且也没有异常堆栈信息,最终发现是合并解决冲突后代码少了一个变量声明,就是单纯的越过了 Annotation Processor 过程直接进行 compile to class 流程了)。
这个技能有什么鸟用?
不瞒你说,我也算是老司机了,好些年前 Annotation Processor 就玩的很 6 了,但是最近项目升级构建和 Java8 及 androidX 支持后 merge 了下代码,然后项目中的注解处理器、dataBinding 全部都不工作了,更可气的是,这个不工作是真的很吝啬,什么错误堆栈都没有,大致如下奇葩构建日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test:compileReleaseJavaWithJavac'.
// 本来这里该先吐我注解处理器内部的日志,然后才继续 javac 编译,实际什么都没吐
> Compilation failed; see the compiler error output for details.
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':moffice:compileReleaseJavaWithJavac'.
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$1(ExecuteActionsTaskExecuter.java:200)
......
Caused by: org.gradle.api.internal.tasks.compile.CompilationFailedException: Compilation failed; see the compiler error output for details.
at org.gradle.api.internal.tasks.compile.JdkJavaCompiler.execute(JdkJavaCompiler.java:57)

Gradle 构建命令已经添加了各种详细参数供查看堆栈和详细日志,但奇妙的事情就是他走到compileReleaseJavaWithJavac就直接出错了,前后没有任何错误提示(有的只是一坨 Gradle 自己的 task 调用链)。我特么大意了,我就同步了下代码,编不过就编不过啊,你倒是提示下问题啊!啥也不提示直接干到 compile class 环节了,跳过了 Annotation Processor 流程,这就很恼火了。好在按照上面方式定位修复了,哈哈。


引用

引用


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!