Java注解处理器(APT)详解及常见问题

注解处理器(AnnotationProcessorTool)作为Java的一个高级语法特性,合理使用能给我们的开发带来极大便利。

例如Spring、Lombok以及Jetbrains自带的一些注解及其对应功能都是基于APT实现的。

Lombok可以利用APT自动检测你代码中的 @Data @AllArgsConstructor 等注解,干预Java的编译过程,从而实现自动生成一些代码来简化开发,提高源代码的可读性。

基本知识

在Java中,javax.lang.model.AnnotatedConstruct 是APT中大多数类的最顶级父类,例如TypeElement、TypeMirror等都是它的子类。

现在我们介绍一下Element和TypeMirror这两个接口。

回忆一下面向对象中类与对象的区别:类是一个抽象的概念,它为其对应的所有对象定义了共同的属性和方法。而对象是类的实例,例如Student类可以有多个对象,Student类定义了其所有对象的Name和Age属性,那么对于不同的对象,它们都有这两个属性,但是它们各自的值可以不一样。

现在可以简单理解为TypeMirror是Element的镜像,也就是说TypeMirror是对Element的抽象,Element是与其对应的TypeMirror的实现。(TypeMirror是类,Element是对象)

Element

Element接口的方法

  • TypeMirror asType() 获取Element对应的TypeMirror
  • ElementKind getKind() 获取Element的类型,ElementKind是一个枚举
  • Set<Modifier> getModifiers() 获取Element的关键字,Modifier也是一个枚举
  • Name getSimpleName() 获取Element的简单名称,如果这个Element是一个方法,那么SimpleName就是方法名,类、接口、变量等以此类推
  • Element getEnclosingElement() 获取定义这个Element的Element,这个比较复杂,放在下面介绍
  • List<? extends Element> getEnclosedElements() 获取Element中的所有子Element,例如对某个TypeElement使用此方法可以得到这个类中的成员变量、构造函数、方法等Element
  • List<? extends AnnotationMirror> getAnnotationMirrors() 获取修饰此Element的所有注解镜像
  • <A extends Annotation> A getAnnotation(Class<A> annotationType) 获取修饰此Element的注解,不存在返回null
  • <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationType) 作用同上,可以得到多个同类型的注解

Element接口的子类

  • PackageElement 包元素
  • TypeElement 类元素,通常是类、枚举、接口、注解(枚举是类的一种,注解是接口的一种)
  • VariableElement 变量元素,通常是类的成员变量、方法的参数
  • ExecutableElement 可执行元素,通常是构造函数和方法
  • TypeParameterElement 泛型参数元素,例如List<E>中的E
  • Parameterizable 可泛型参数化的,例如类和方法都能使用<K, V, T>来定义泛型,因此 Parameterizable 是 ExecutableElement 和 TypeElement 的父类
  • QualifiedNameable 可命名的,是PackageElement 和 TypeElement 的父类

上面的 VariableElement 和 TypeParameterElement 是 Element 的直接子类。

首先来看一段代码:

public class Person<T> { // TypeElement, TypeParameterElement
    String name; // VariableElement
    int age; // VariableElement
    String sex; // VariableElement
   
    public void get() { // ExecutableElement
        System.out.println("name:" + name + " age:" + age + " sex:"+ sex);
    }

    public void sayHello(String to) { // ExecutableElement, VariableElement
        System.out.println("Hello, " + to + "!");
    }
}

看到这里你应该大致了解这些子类的区别了,但是 TypeParameterElement 和方法中的 VariableElement都需要通过其他Element得到。

例如,通常你能得到 TypeElement 和 ExecutableElement,你可以通过 TypeElement 得到这个类的泛型参数集合,通过 ExecutableElement 可以得到这个可执行元素的参数元素集合 VariableElement。

getEnclosingElement()

这个方法比较复杂,先看看JDK里的注释是怎么写的:

简单来说,这个方法的返回值取决于具体是哪种Element的子类。

通常会用这个方法从 ExecutableElement 中得到定义这个构造函数或方法的类,例如对Person类的构造函数Element使用这个方法,可以得到Person类的TypeElement。

如果是从 TypeParameterElement 中调用,得到的是这个泛型的具体类型,例如对List<Person>的 TypeParameterElement 使用这个方法可以得到Person的TypeElement。

如果从一个类或接口的 TypeElement 中调用,得到的是他们的包元素,也就是 PackageElement。

至于提到的record component和module是后续JDK更新加入的新元素,有兴趣可以自己了解一下。

TypeMirror

TypeMirror是一个抽象的概念,与Element相比可提供的信息就很少了。

TypeMirror接口的方法

  • TypeKind getKind() 获取TypeMirror的类型
  • List<? extends AnnotationMirror> getAnnotationMirrors() 修饰该类的接口镜像
  • <A extends Annotation> A getAnnotation(Class<A> annotationType) 同Element中的方法
  • <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationType) 同上

TypeMirror的子类

  • PrimitiveType 基本类型,例如int、float、char等
  • NullType 空类型
  • ArrayType 数组类型,例如String[]
  • DeclaredType 通常是 TypeElement 对应的 TypeMirror 的类型。
  • TypeVariable 泛型参数类型,是 TypeParameterElement 对应的 TypeMirror 的类型。
  • WildcardType 通配符类型,例如 ? ? extends T ? super T
  • ExecutableType 可执行类型,也就是 ExecutableElement 对应的 TypeMirror 的类型。
  • NoType 无类型,通常是包、模块、空类型的TypeMirror类型
  • ErrorType 错误类型,通常表示一个类无法被正确的转换为TypeMirror

还有其他的UnionType、IntersectionType用的比较少,感兴趣可以自己了解一下。

NullType、ArrayType 、DeclaredType、TypeVariable 是 Reference 的子类,而 Reference 才是 TypeMirror 的直接子类。

ErrorType 是 DeclaredType的子类。

TypeMirror常见问题

无父类的父类的TypeMirror

对应 TypeElement 可以通过 getSuperClass() 方法得到它的父类的TypeMirror,如果它没有父类,得到的则是 Object 类的 TypeMirror。

Object类的父类的TypeMirror

Object类是Java中最顶级的类,也就意味着它没有父类。如果你对 Object类的 TypeElement 使用 getSuperClass() 方法,会得到一个 NoType,它是 TypeMirror 的子类。

TypeMirror的 toString() 方法

你可以通过这个方法得到这个 TypeMirror 对应的类的完整包名。

需要注意的是,这个方法返回的有可能含有其他符号,例如 ExecutableElement 的 TypeMirror$toString 只后是小括号()+返回值类名,例如”()java.lang.String”。

而对于NoType来说,toString() 返回的是字符串none。

准备工作

先准备一个注解,例如:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Component {

}

然后创建一个新的类,继承 javax.annotation.processing.AbstractProcessor 类,实现process()与getSupportedAnnotationTypes()两个基本的方法。

为了方便,这里直接引入@AutoService注解,它可以自动在META-INF中生成对应的service配置。

<dependency>
    <groupId>com.google.auto.service</groupId>
    <artifactId>auto-service</artifactId>
    <version>1.0-rc6</version>
</dependency>

用法如下所示

@AutoService(Processor.class)
public class MyTestAPT extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Sets.newHashSet(
                Component.class.getCanonicalName()
        );
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
        // return super.getSupportedSourceVersion();
    }
}

这就是一个最简单的注解处理器了,如果需要在其他项目使用,直接引入依赖就行。

maven依赖的scope建议设置为provided。

如果是gradle,请使用annotationProcessor来引入依赖。

下面对这些方法逐一介绍。

process()

这个方法有两个参数,一个是 Set<? extends TypeElement> annotations,另一个是 RoundEnvironment roundEnv

首先annotations这个集合储存的是被编译项目中存在的所有受支持的注解,受支持的注解由下面的getSupportedAnnotationTypes方法提供。

例如一个SpringBoot项目你使用了@SpringBootApplication与@Autowired,这个集合中保存的就是这两个注解对应的TypeElement类。

RoundEnvironment中有两个get方法,分别是:

  • getRootElements 获取所有被注解处理器支持的注解所修饰的元素
  • getElementsAnnotatedWith 获取所有被指定注解修饰的元素

举个例子说明这两个方法,例如有下面的代码:

public interface UserService {
    UserEntity getUserById(Long userId);
}
@Component
public class UserServiceImpl implements UserService {
    @Resource
    private UserMapper userMapper;


    @Override
    public UserEntity getUserById(Long userId) {
        return null;
    }
}

此时调用getRootElements方法,可以得到的是UserServiceImpl 和 UserService,因为UserServiceImpl 继承了 UserService,因此即使UserService没有注解,但是它的实现类存在,这也算是一个RootElement。

如果调用getElementsAnnotatedWith方法获取被@Compoent注解修饰的元素,此时只能得到UserServiceImpl。

getSupportedAnnotationTypes()

这是一个很重要的方法,它的返回值是一个String类型的集合。

这个集合存放的也就是对应注解类的完整路径,一般用getCanonicalName()来获取,至于它和一般的getName()的区别,可以自行百度了解一下。

这个方法的作用是告诉你的注解处理器,当编译时在项目中存在至少一个在这个集合中的注解时,需要调用process()方法。

也就是说,只有在这个集合中的注解才会参与注解处理。

常用方法

你已经了解了APT及其基本模型,现在可以开始你的创作了。

下面介绍一些常用的方法来辅助开发。

JavaPoet

通常注解处理器会被用来自动生成一些代码,但是生成代码的过程用字符串拼接很麻烦,可以尝试一下JavaPoet库,通过简单的Java代码来生成标准格式的代码。

在注解处理器中,你可以按照如下写法在org.example.generated包内生成一个GeneratedClass。

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    try {
        TypeSpec build = TypeSpec.classBuilder("GeneratedClass")
                .addModifiers(Modifier.PUBLIC)
                .addMethod(MethodSpec.methodBuilder("say")
                        .addModifiers(Modifier.PUBLIC)
                        .returns(ClassName.get(String.class))
                        .addStatement(CodeBlock.of("return \"Hello\"")).build())
                .build();
        JavaFile.builder("org.example.generated", build).build().writeTo(this.filer);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    return true;
}

生成的GeneratedClass代码如下:

public class GeneratedClass {
  public String say() {
    return "Hello";
  }
}

你可以直接在你的项目中使用这个生成的类,前提是你执行了compile或者build命令让注解处理器先生成这些类。

这些生成的类和包会随着你的项目一起打包出去,因此引入的注解处理器scope为provided即可,它们只会在你编译时生效。

常见问题

注解处理器是一个比较麻烦的东西,例如项目需要调用生成的代码,就需要先让注解处理器生成对应的代码,才能进行项目的下一步编译。

如果你打算写一个注解处理器,建议把项目分成两个不同的module,分别是compiler和annotation。

annotation主要用于存放你的自定义注解以及一些通用的工具类。

compiler就是你实现AbstractProcessor的模块了,如果是gradle项目,直接把这个模块作为annotationProcessor引入即可。如果是maven项目,直接以provided的模式引入即可。

Compilation failed: internal java compiler error

错误信息大致如下:

java: java.util.ServiceConfigurationError: javax.annotation.processing.Processor: Provider org.example.MyTestAPT not found

这个错误一般出现在打包编译一个注解处理器项目时,当你完成一个注解处理器的编写后,你希望把它打包出来给别的项目使用,但是编译时可能会出现这个错误。

例如你的项目使用了lombok,打包时就需要让lombok先做一些操作,然后才能输出你的项目class文件,这是建立在你已经引入了lombok为依赖的前提。

这个错误的主要原因是打包时maven会认为你的项目也需要使用你的注解处理器,但此时你的项目还没有被打包,而打包你的项目又需要用到你的项目中的注解处理器,这样就产生循环依赖了。

解决的办法也很简单,在maven打包时让proc参数为none即可,也就是忽略所有的注解处理器,这样打包时就不会产生循环依赖了。

在compiler模块中的pom.xml加入下面的片段即可:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
                <proc>none</proc>
            </configuration>
        </plugin>
    </plugins>
</build>

加上这个插件后一定要记得先clean再install,否则可能不会生效。

不过这个方法也有一定的缺陷,它直接让所有的注解处理器不生效,这样的话如果你的compiler模块使用了lombok或其他含有注解处理器的库也同样不会生效。

如果你没有使用任何其他的注解处理器,用上面的方法即可。

当然你也可以只屏蔽你自己的注解处理器,稍作修改即可:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <compilerArgs>
                    <arg>-proc:none</arg>
                    <arg>-Aorg.example.MyTestAPT</arg>
                </compilerArgs>
            </configuration>
        </plugin>
    </plugins>
</build>

利用proc的参数可以指定关闭哪些注解处理器,在-A的后面紧跟注解处理器的包名和类型即可。

结束语

Java的注解处理器远不止这么简单,例如其中的Element、TypeMirror等都没有详细介绍,但通过这篇文章了解一下它们简单的用法,实际上就应该可以应对大部分场景了。

这一部分内容我自己也并不完全明白,本文仅供学习参考,如有错误欢迎指出。

未经允许禁止转载本站内容,经允许转载后请严格遵守CC-BY-NC-ND知识共享协议4.0,代码部分则采用GPL v3.0协议
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇