java注解

process方法的调用方式

注释处理分多轮完成。每一轮都从编译器开始搜索源文件中的注释并选择适合这些注释的注释处理器。依次在相应的源上调用每个注释处理器。

如果在此过程中生成了任何文件,则以生成的文件作为其输入开始另一轮。这个过程一直持续到在处理阶段没有新文件产生。

依次在相应的源上调用每个注释处理器。如果在此过程中生成了任何文件,则以生成的文件作为其输入开始另一轮。这个过程一直持续到在处理阶段没有新文件产生。

注释处理 API 位于javax.annotation.processing包中。您必须实现的主要接口是Processor接口,它具有AbstractProcessor类形式的部分实现。这个类是我们将要扩展以创建我们自己的注释处理器的类。

1
2
3
4
5
6
7
8
9
10
原文:
The annotation processing is done in multiple rounds. Each round starts with the compiler searching for the annotations in the source files and choosing the annotation processors suited for these annotations. Each annotation processor, in turn, is called on the corresponding sources.

If any files are generated during this process, another round is started with the generated files as its input. This process continues until no new files are generated during the processing stage.

Each annotation processor, in turn, is called on the corresponding sources. If any files are generated during this process, another round is started with the generated files as its input. This process continues until no new files are generated during the processing stage.

The annotation processing API is located in the javax.annotation.processing package. The main interface that you’ll have to implement is the Processor interface, which has a partial implementation in the form of AbstractProcessor class. This class is the one we’re going to extend to create our own annotation processor.


这是一个实现处理器的例子:

1
2
3
4
5
6
7
8
9
10
11
@SupportedAnnotationTypes("com.baeldung.annotation.processor.BuilderProperty")//指定这个处理器能处理的注解
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class BuilderProcessor extends AbstractProcessor {

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

不仅可以指定具体的注释类名称,还可以指定通配符,例如“com.baeldung.annotation.\*” 来处理com.baeldung.annotation包及其所有子包中的注释,甚至可以“*”来处理所有注释.

我们必须实现的单一方法是处理本身的process方法。编译器为每个包含匹配注释的源文件调用它。

注释作为第一个Set<? extends TypeElement> annotations参数,有关当前处理轮次的信息作为RoundEnviroment roundEnv参数传递。

如果您的注释处理器已经处理了所有传递的注释,并且您不希望它们被传递到列表中的其他注释处理器,则返回布尔值应该为true

1
2
3
4
5
6
7
8
原文:
You can specify not only the concrete annotation class names but also wildcards, like “com.baeldung.annotation.*” to process annotations inside the com.baeldung.annotation package and all its sub packages, or even “*” to process all annotations.

The single method that we’ll have to implement is the process method that does the processing itself. It is called by the compiler for every source file containing the matching annotations.

Annotations are passed as the first Set<? extends TypeElement> annotations argument, and the information about the current processing round is passed as the RoundEnviroment roundEnv argument.

The return boolean value should be true if your annotation processor has processed all the passed annotations, and you don't want them to be passed to other annotation processors down the list.

总结:

在我们自定义的处理器类中,我们指定了这个处理器类可以处理的注解。

然后,java编译器会依次调用注册好的注解处理器处理注解,这会有多轮处理。

在调用注解处理器的时候,编译器会筛选出这个注解处理器可以处理的注解信息给它处理。

在当前注解处理器处理注解的时候,如果产生的新类里依旧包含注解,那么,在其他所有注解处理器完成处理之后,会进行第二轮处理,第三轮,直到没有新的注解产生。

  • 例子:

    • MainActivity

      1
      2
      3
      4
      5
      public class MainActivity extends AppCompatActivity {

      @BindView(R.id.tv_hello)
      TextView tvHello;
      }
    • 在第一轮处理注解时,我们生成了新的类,而这个类使用**@Keep**注解标记了,那么注解处理器就会 开始下一轮的处理,直到生成的类里面再也没有任何注解了。

      1
      2
      3
      4
      @Keep
      public class MainActivity$Binding {
      ...
      }

解析注解的步骤

插件化注解处理API的使用步骤大概如下:

  • 1、自定义一个Annotation Processor,需要继承javax.annotation.processing.AbstractProcessor,需要重写process()方法。
    • 1.1、此外还需要实现几个简单的方法init ()getSupportedSourceVersion()getSupportedAnnotationTypes()
  • 2、自定义一个注解,注解的元注解需要指定@Retention(RetentionPolicy.SOURCE)
  • 3、需要在声明的自定义Annotation Processor中使用javax.annotation.processing.SupportedAnnotationTypes指定在第2步创建的注解类型的名称(注意需要全类名,”包名.注解类型名称”,否则会不生效)。
  • 4、需要在声明的自定义Annotation Processor中使用javax.annotation.processing.SupportedSourceVersion指定编译版本。
  • 5、可选操作,可以通在声明的自定义Annotation Processor中使用javax.annotation.processing.SupportedOptions指定编译参数。

配置和注册

  • 创建一个自定义Annotation Processor继承于AbstractProcessor
1
2
3
4
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
。。。省略。。。
}
  • @AutoService(Processor.class) :向javac注册我们这个自定义的注解处理器,这样,在javac编译时,才会调用到我们这个自定义的注解处理器方法。
    AutoService这里主要是用来生成
    建议直接采用@AutoService(Processor.class)进行自定义注解处理器注册,简洁方便

  • 其他方式注册

    例如:

    我们模仿一下测试框架Junit里面的@Test注解,在运行时通过Annotation Processor获取到使用了自定义的@Test注解对应的方法的信息。因为如果想要动态修改一个类或者方法的代码内容,需要使用到字节码修改工具例如ASM等,这些操作过于深入,日后再谈。先定义一个注解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package club.throwable.processor;

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;

    /**
    * @author throwable
    * @version v1.0
    * @description
    * @since 2018/5/27 11:18
    */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Test {

    }

    定义一个注解处理器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @SupportedAnnotationTypes(value = {"club.throwable.processor.Test"})
    @SupportedSourceVersion(value = SourceVersion.RELEASE_8)
    public class AnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    System.out.println("Log in AnnotationProcessor.process");
    for (TypeElement typeElement : annotations) {
    System.out.println(typeElement);
    }
    System.out.println(roundEnv);
    return true;
    }
    }

    编写一个主类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Main {

    public static void main(String[] args) throws Exception{
    System.out.println("success");
    test();
    }

    @Test(value = "method is test")
    public static void test()throws Exception{

    }
    }

    接着需要指定Processor,如果使用IDEA的话,Compiler->Annotation Processors中的Enable annotation processing必须勾选。然后可以通过下面几种方式指定指定Processor。

    • 1、直接使用编译参数指定,例如:javac -processor club.throwable.processor.AnnotationProcessor Main.java。

    • 2、通过服务注册指定,就是META-INF/services/javax.annotation.processing.Processor文件中添加club.throwable.processor.AnnotationProcessor。

      • 具体手动注册方法如下:

        1. 创建一个
          META-INF/services/javax.annotation.processing.Processor文件,
          其内容是一系列的自定义注解处理器完整有效类名集合,以换行切割。

          1
          2
          3
          com.example.MyProcessor
          com.foo.OtherProcessor
          net.blabla.SpecialProcessor
          • 文件放在/src/main/resources/META-INF/services/javax.annotation.processing.Processor
          • 处理器类文件放在/src/main/java/com/example/annotationcomplierlib/AnnotationComplier.java
        2. 将自定义注解处理器和
          META-INF/services/javax.annotation.processing.Processor打包成一个.jar文件。所以其目录结构大概如下所示:

          1
          2
          3
          4
          5
          6
          7
          8
          MyProcessor.jar
          - com
          - example
          - MyProcessor.class

          - META-INF
          - services
          - javax.annotation.processing.Processor
    • 3、通过Maven的编译插件的配置指定如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.5.1</version>
    <configuration>
    <source>1.8</source>
    <target>1.8</target>
    <encoding>UTF-8</encoding>
    <annotationProcessors>
    <annotationProcessor>
    club.throwable.processor.AnnotationProcessor
    </annotationProcessor>
    </annotationProcessors>
    </configuration>
    </plugin>

    值得注意的是,以上三点生效的前提是club.throwable.processor.AnnotationProcessor已经被编译过,否则编译的时候就会报错:

    1
    2
    3
    [ERROR] Bad service configuration file, or exception thrown while
    constructing Processor object: javax.annotation.processing.Processor:
    Provider club.throwable.processor.AnnotationProcessor not found

    解决方法有两种,第一种是提前使用命令或者IDEA右键club.throwable.processor.AnnotationProcessor对它进行编译;第二种是把club.throwable.processor.AnnotationProcessor放到一个独立的Jar包引入。我在这里使用第一种方式解决。

    最后,使用Maven命令mvn compile进行编译。输出如下:

    1
    2
    3
    4
    5
    6
    Log in AnnotationProcessor.process
    [errorRaised=false, rootElements=[club.throwable.processor.Test,club.throwable.processor.Main, club.throwable.processor.AnnotationProcessor, processingOver=false]
    Log in AnnotationProcessor.process
    [errorRaised=false, rootElements=[], processingOver=false]
    Log in AnnotationProcessor.process
    [errorRaised=false, rootElements=[], processingOver=true]

    可见编译期间AnnotationProcessor生效了。

备注:

  • Gradle配置如下:
1
2
3
4
5
6
7
8
9
10

dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')

implementation project(':annotation')
//用于自动为 JAVA Processor 生成 META-INF 信息。
implementation 'com.google.auto.service:auto-service:1.0-rc3'
//快速生成.java文件的库
implementation 'com.squareup:javapoet:1.8.0'
}
  • Processor 重写父类的几个方法:

    • process()方法,这里是处理注解内部逻辑的,也是本文的关键点之一;
    • getSupportedSourceVersion():设置支持的版本,一般用最新的就好;
    • getSupportedAnnotationTypes():添加支持的注解类型,可以是单个/多个,用Set存储;

    • init ():一些初始化操作,获取一些有用的系统工具类,比如生成文件、打印信息、处理元素等;

getSupportedSourceVersion()getSupportedAnnotationTypes()这2个方法还有一种 简单的方式来实现,如下:

1
2
3
4
@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.zx.annotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ViewInjectProcessor extends AbstractProcessor {}

是通过注解来实现的,看上去是比较简洁

  • @SupportedAnnotationTypes()可以申明一个注解数组,但是这种字符串拼接容易出错;
  • @SupportedSourceVersion:设置支持的源码版本,可以是RELEASE_0~RELEASE_8,但是不能使用latestSupported()设置最新的版本;

方法讲解

我们今天只说Processor。先从接口的方法介绍起把。

变量和类型 方法 描述
Iterable<? extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) 返回一个空的迭代完成。
Set<String> getSupportedAnnotationTypes() 如果处理器类使用SupportedAnnotationTypes进行批注,则返回与注释具有相同字符串集的不可修改集。
Set<String> getSupportedOptions() 如果处理器类使用SupportedOptions进行批注,则返回具有与批注相同的字符串集的不可修改集。
SourceVersion getSupportedSourceVersion() 如果处理器类使用SupportedSourceVersion进行批注,请在批注中返回源版本。
void init(ProcessingEnvironment processingEnv) 通过将 processingEnv字段设置为 processingEnv参数的值,使用处理环境初始化处理器。
boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 处理源自前一轮的类型元素的一组注释类型,并返回此处理器是否声明了这些注释类型。 如果返回true ,则声明注释类型,并且不会要求后续处理器处理它们; 如果返回false ,则注释类型无人认领,可能会要求后续处理器处理它们。 处理器可以总是返回相同的布尔值,或者可以根据其自己选择的标准改变结果。
  • init:
    初始化工作,我们可以得到一些有用的工具,例如 Filer,我们需要它将生成的代码写入文件中

    init(ProcessingEnvironment env):每个Annotation Processor必须***
    有一个空的构造函数 ***。编译期间,init()会自动被注解处理工具调用,并传入ProcessingEnviroment参数,通过该参数可以获取到很多有用的工具类: Elements , Types , Filer 等等

  • process:
    最重要的方法,所有的注解处理都是在此完成

    process(Set<? extends TypeElement> annoations, RoundEnvironment roundEnv):Annotation Processor扫描出的结果会存储进roundEnv中,可以在这里获取到注解内容,编写你的操作逻辑。注意,process()函数中不能直接进行异常抛出,否则的话,运行Annotation Processor的进程会异常崩溃,然后弹出一大堆让人捉摸不清的堆栈调用日志显示.

  • getSupportedAnnotationTypes:
    返回我们所要处理的注解的一个集合

    getSupportedAnnotationTypes(): 该函数用于指定该自定义注解处理器(Annotation Processor)是注册给哪些注解的(Annotation),注解(Annotation)指定必须是完整的包名+类名(eg:com.example.MyAnnotation)

  • getSupportedSourceVersion:
    要支持的java版本

    getSupportedSourceVersion():用于指定你的java版本,一般返回:SourceVersion.latestSupported()。当然,你也可以指定具体java版本:
    return SourceVersion.RELEASE_7;

ProcessingEnvironment

这个类很重要,要考的。这个类会在函数init的时候被传入,主要的工具类方法都在这个类上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface ProcessingEnvironment {
Map<String, String> getOptions();

Messager getMessager();

Filer getFiler();

Elements getElementUtils();

Types getTypeUtils();

SourceVersion getSourceVersion();

Locale getLocale();
}

Filer 就是文件流输出路径,当我们用AbstractProcess生成一个java类的时候,我们需要保存在Filer指定的目录下。

Messager 输出日志工具,需要输出一些日志相关的时候我们就要使用这个了。

Elements 获取元素信息的工具,比如说一些类信息继承关系等。

Types 类型相关的工具类,processor java代码不同的是,当process执行的时候,class的由于类并没有被传递出来,所以大部分都行都是用element来代替了,所以很多类型比较等等的就会转化成type相关的进行比较了。

类型相关的都被转化成了一个叫TypeMirror,其getKind方法返回类型信息,其中包含了基础类型以及引用类型。

举个简单的例子,当一个实现了注解的Element被传入的时候,我们要判断Element是不是实现了特定接口,那么应该如何做呢?

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
private Elements elementUtils;
private Types types;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
types = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
}

public boolean isSubType(Element element, String className) {
return element != null && isSubType(element.asType(), className);
}

public TypeMirror typeMirror(String className) {
return typeElement(className).asType();
}

public TypeElement typeElement(String className) {
return elementUtils.getTypeElement(className);
}

public boolean isSubType(TypeMirror type, String className) {
return type != null && types.isSubtype(type, typeMirror(className));
}

其中isSubType方法是判断传入的Element是不是一个接口的实现类。首先我们要将对象都转化成Element, 然后将两个element转化成TypeMirror,之后调用Types的isSubtype方法对两个TypeMirror进行比较,如果发现类型一样,则该输入的Element是特定接口的实现类。

process()

扫描代码的时候会把当前获取到的,此processer能处理的annotations传入当前方法

Processor的kapt优化

kotlin对apt做了很多优化,内部完成了增量编译。但是对于低版本的autoservice,其增量编译会被关闭。

这里简单给各位大佬做下这方面的升级就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apply plugin: 'java-library'
apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'

dependencies {
implementation 'com.google.auto.service:auto-service:1.0-rc5'
implementation 'com.squareup:javapoet:1.10.0'
implementation 'com.github.leifzhang:RouterAnnotation:0.5.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
kapt "com.google.auto.service:auto-service:1.0-rc5"
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

简单的说就是把processor 升级到rc5,然后用kapt的方式去把它注册起来就行了。

缺点和总结

缺点:apt能做的事情还是比较有限的

  1. javapoet只能新增一个类,而不能对当前类进行更改。
  2. proessor在javac执行之前,所以只能对当前moudule生效
  3. 当Module一多,可能会有类名冲突的问题

但是apt还是能帮助我们解决很多问题的,我们可以把一些机械化的操作,通过anntation的方式去简化,比如butterknife,这样开发就可以有更多的精力去专注做写别的事情。一部分abtest赋值的操作其实也可以用同样的方式去调整。


一些疑问

  • 注解处理器processor为什么要在META-INF注册?
  • 注解处理器processor是如何被系统调用的?
  • 注解申明和注解处理器为什么要分Module处理?
  • apt项目不会增加apk体积?

先来回顾一下之前项目的部分目录结构

img

image.png

先明确一些概念:
AbstractProcessor是抽象处理器,开发apt时都必须继承这个类来生成.java文件,实现的这个类后叫做注解处理器,也就是这里的ButterKnifeProcessor

Q1:注解处理器processor为什么要在META-INF注册?


META-INF的作用
META-INF, 相当于一个信息包,用于存放一些meta information相关的文件。用来配置应用程序、扩展程序、类加载器和服务manifest.mf文件,在用jar打包时自动生成。

在之前的文章中说过,通过@AutoService(Processor.class)注解把注解处理器ButterKnifeProcessor注册到META-INF/services中,这里的包名是META-INF/services/javax.annotation.processing.Processor,
这个文件的内容是

1
com.zx.processor.ButterKnifeProcessor

这个包名其实就是@AutoService(Processor.class)里面的Processor类;而文件内容就是注解处理器。

在编译时,java编译器(javac)会去META-INF中查找实现了的AbstractProcessor的子类,并且调用该类的process函数,最终生成.java文件。其实就像activity需要注册一样,就是要到META-INF注册 ,javac才知道要给你调用哪个类来处理注解。

Q2:注解处理器processor是如何被系统调用的?


一些细心的同学应该发现了这个问题,我们并没有手动调用AbstractProcessor这个注解处理器类,那系统是什么时间调用的?又是如何调用的?这其实就牵扯到apt工作机制。

在上一问中,我们已经了解到,在编译时javac会查找所有的 在META_INF 中注册的注解处理器来处理注解。

到这里,好像有点清楚了,大概知道javac会去找到Processor并调用。但是呢还是没找到直接源头,因为它不像我们面向对象编程中可以准确追踪到是哪个对象调用的。

别着急,先来看这么个东西.
是我项目中使用到注解的app.Gradle

1
2
3
4
5
6
dependencies {
implementation project(':annotation')
implementation project(':inject_api')
//gradle3.0以上apt的实现方式
annotationProcessor project(':processor')
}

这里的annotationProcessor有点特别,没错,它是APT实现方案的一种。这里简单介绍一下:

APT实现方案
android-aptannotationProcessor功能是一样的,都是apt的实现方案,前者是个人开发者提供,比较早(现在不再维护了),后者是google官方开发的内置在gradle里的apt。

annotationProcessor是APT工具中的一种,是google开发的内置框架,不需要引入,可以直接在build.gradle文件中使用。

只有在你使用注解的地方引入了annotationProcessor,系统才会主动调用注解处理类Processor,才会最终生成如下的.java文件

img

apt生成类.png

这里先简单总结一下:
2.1、在完成注解处理类Processor之后,需要做2件事情:

  • 1、在META-INF目录下注册Processor
  • 2、在项目中使用注解的地方添加apt工具annotationProcessor

2.2、APT 4要素
 注解处理器(AbstractProcess)+ 代码处理(javaPoet)+ 处理器注册(AutoService)+ apt(annotationProcessor)

APT(Annotation Processing Tool)总结
首先,APT是javac提供的一种工具,它在编译时扫描、解析、处理注解。它会对源代码文件进行检测,找出用户自定义的注解,根据注解、注解处理器和相应的apt工具自动生成代码。这段代码是根据用户编写的注解处理逻辑去生成的。最终将生成的新的源文件与原来的源文件共同编译(注意:APT并不能对源文件进行修改操作,只能生成新的文件,例如往原来的类中添加方法)。具体流程图如下图所示:

img

apt工作流程.png

APT技术的使用,需要我们遵守一定的规则。大家先看一下整个APT项目项目构建的一个规则图,具体如下所示:

img

Q3:注解申明和注解处理器为什么要分Module处理?


先来回顾一下之前的项目结构:

img

  • annotation:申明注解 (java lib)
  • processor:注解处理器(java lib)
  • inject_api:调用处理器中生成的类 (android lib)
  • app:项目使用 (android lib)

我们都知道注解处理器都需要继承AbstractProcessor类,但是AbstractProcessor是JDK中的类,不在android sdk中,所以需要放在单独的java lib中;而processor中需要依赖自定义注解,把annotation抽成一个独立的lib,便于维护。

那注解声明和注解处理为什么要分开呢?可不可以放在一起?
先说结论:可以放在一起,放在一起对功能上没有什么影响;但是一般不放在一起,原因如下:

我们都知道processor的作用是:在编译器解析注解、生成新的.java文件。这个lib只在编译器用到,是不会被打包进apk的。对于调用者来说,你只是想使用这个注解,而不希望你已经编译好的项目中引进注解处理器相关的内容,所以为了不引入没必要的文件,我们一般选择将注解声明和注解处理分开处理。

到这里apt相关知识就说完了,我们也可以理解为什么ButterKnife这种注解库不会增加项目体积了。

想了解更多apt知识,可以参考:
https://www.jianshu.com/p/b6b3283968e0

感谢
你必须知道的APT、annotationProcessor、android-apt、Provided、自定义注解

作者:唠嗑008
链接:https://www.jianshu.com/p/89ac9a2513c4
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


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