APT使用流程简述

·  阅读 398

APT是什么?

APT即为Annotation Processing Tool:注解处理器,它是javac的一个工具,APT可以用来在编译时扫描和处理注解。

现在很多主流的三方库都使用到了该技术,比如说ARouter、ButterKnife等。 本文主要是记录下APT的使用流程。其中很多细节已经一些坑。

1.自定义注解

先创建一个java module,专门定义注解相关逻辑:

image-20210202221336359 image-20210202221656116

在新生成的lib中自定义一个注解:FRC_TEST

/**
 * @author: frc
 * @description: 测试注解
 * @date: 2021/2/2 10:18 PM
 */
@Target(ElementType.TYPE)//注解使用位置,放在类上
@Retention(RetentionPolicy.CLASS)//注解有效时间
public @interface FRC_TEST {
}

复制代码

1.1 元注解

元注解是可以用在注解上的注解

元注解@Target用来标注当前自定义注解的使用位置,有如下几个位置,比如类上、方法上、参数上等。

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}
复制代码

元注解Retention用来标记当前注解有效期间,比如说运行时,编译时等。

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}


复制代码

这里的SOURCE和CLASS,经常让人觉得混淆。下面是个人理解:SOURCE在编译时会被移除,用来在源码阶段给开发者提示作用,比如说@Override。而CLASS是默认状态,在编译时是存在的,在运行时会被移除,它想表达的东西不会被虚拟机识别,也就是它不能被运行时反射。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
复制代码

所以上面定义的FRC_TEST可用于类或接口上的编译时注解

2. 编译时获取注解

上面自定义的FRC_TEST注解,需要在编译时被拿到。所以在编译时要读取到该注解。如何读取呢?

javac 再将.java文件编译成.class文件之前会显扫描所有的.java文件,我们可以通过注册一个服务来监听当前扫描行为。如果检测到扫描行为,就可以过滤出被FRC_TEST注释的文件。拿到这些文件后,在真正编译前生成一些辅助的.java文件。一起编译到.class中。

我们可以再创建一个 Java module叫frc-compiler,专门用来处理编译期间的注解相关逻辑。

2.1 注解处理类

首先我们需要定义一个类继承AbstractProcessor,这个类会在编译时被触发,用来处理自定义的注解想做的事。我们称为 注解处理器类:

package com.rong.cheng.frc_compiler;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;

/**
 * @author: frc
 * @description: FRC_TEST注解的注解处理器
 * @date: 2021/2/2 10:50 PM
 */
@SupportedSourceVersion(SourceVersion.RELEASE_8)//支持到的java版本
@SupportedAnnotationTypes("com.rong.cheng.frc_annotation.FRC_TEST")//支持的注解是哪个
class FrcTestAnnotationProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}

复制代码
  • 继承AbstractProcessor

  • @SupportedSourceVersion()指定支持的最大的java版本

  • @SupportedAnnotationTypes()指定当前注解处理器要处理那些注解,javac只有在扫描到指定的注解时才会回调给当前处理器。

@SupportedAnnotationTypes支持传入多个注解如下,所以一个注解处理器是支持处理多个注解的:

@Documented
@Target(TYPE)
@Retention(RUNTIME)
public @interface SupportedAnnotationTypes {
    /**
     * Returns the names of the supported annotation types.
     * @return the names of the supported annotation types
     */
    String [] value();
}

复制代码

上面定义了要在编译时扫描哪些注解,那么如何让当前的注解处理器能在编译时被通知到呢?需要将当前类注册成服务。

2.2 注册服务

这个需要用到google的 AutoService

在frc-compiler的build.gradle中添加如下依赖

dependencies {
    //个人项目中如果只是annotationProcessor,是无法获取到AutoService,
    compileOnly("com.google.auto.service:auto-service:1.0-rc7")
    annotationProcessor("com.google.auto.service:auto-service:1.0-rc7"
    
     //依赖注解工程
    implementation project(":frc-annotation")
}
复制代码

然后在FrcTestAnnotationProcessor上添加@AutoService注解

@AutoService(Processor.class)//注册成服务
@SupportedSourceVersion(SourceVersion.RELEASE_8)//支持到的java版本
@SupportedAnnotationTypes({"com.rong.cheng.frc_annotation.FRC_TEST"})//支持的注解是哪个
class FrcTestAnnotationProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}

复制代码

我们通过Messager打印信息来验证是否成功:

@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.rong.cheng.frc_annotation.FRC_TEST"})//支持的注解是哪个
@SupportedSourceVersion(SourceVersion.RELEASE_8)//支持到的java版本
class FrcTestAnnotationProcessor extends AbstractProcessor {
    private  Messager messager;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager=processingEnv.getMessager();
      	//注意不能使用Diagnostic.Kind.ERROR,不然直接异常
        messager.printMessage(Diagnostic.Kind.NOTE,"--------->FrcTestAnnotationProcessor init");
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE,"--------->FrcTestAnnotationProcessor process");

        return false;
    }
}
复制代码

然后再主工程中依赖注解处理器:

dependencies {

    //依赖自定义的注解
    implementation project(":frc-annotation")
    //使用自定义的注解处理器
    annotationProcessor project(":frc-compiler")
}
复制代码

需要注意如果该注解是在kotlin代码中被使用需要使用kapt来依赖注解

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'//需要加kapt插件,同时这个插件必须在'kotlin-android'下面
}

dependencies {
    //依赖自定义的注解
    implementation project(":frc-annotation")
    //使用自定义的注解处理器
    kapt project(":frc-compiler")
}
复制代码

我们可以通过手动查看下面文件有没有,判断注解处理器是否生效:

image-20210205144754240

上面代码其实还有两个问题:

由于之前定义注解处理器类的时候忘记加 public修饰,导致编译时找不到FrcTestAnnotationProcessor类:

image-20210202235526992

所以FrcTestAnnotationProcessor一定要加public修饰

@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.rong.cheng.frc_annotation.FRC_TEST"})//支持的注解是哪个
@SupportedSourceVersion(SourceVersion.RELEASE_8)//支持到的java版本
public class FrcTestAnnotationProcessor extends AbstractProcessor {
	
}
复制代码

第二个问题是 只有init中的日志输出了:

image-20210202235703531

这是因为我们的FRC_TEST注解并没有真的被使用,不会执行到process方法。

下面再主app中随便添加下:

@FRC_TEST
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
复制代码
image-20210203000001938

这次process方法被执行了

发现两个问题:

- 日志不会换行
- process方法触发了两次
复制代码

至此 在编译时读取注解就完成了。

note:如果编译没反应,可以先尝试clean项目,再make

2.3 读取Android项目传入的参数

现在都是多个module协作开发,有时注解需要根据不同module的情况做差异化处理,这时我们就要读取module传入的配置参数。

2.3.1 配置参数

build.gradle配置文件中提供了注解处理时参数设置功能:

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.0"

    defaultConfig {
        applicationId "com.rong.cheng.study"
        minSdkVersion 19
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

        javaCompileOptions{
            annotationProcessorOptions{
                arguments = ["moduleName":"app"]
            }
        }
    }
}
复制代码

javaCompileOptions:java编译时的配置选项

annotationProcessorOptions:注解处理的配置选项

arguments:配置的参数,key:value。

所以可以理解为:在java编译时配置注解处理的配置。

2.3.2 读取参数

  • 首先需要在注解处理器类上加@SupportedOptions注解
  • 注解处理器init时,通过ProcessingEnvironment获取

代码如下:

@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.rong.cheng.frc_annotation.FRC_TEST"})//支持的注解是哪个
@SupportedSourceVersion(SourceVersion.RELEASE_8)//支持到的java版本
@SupportedOptions("moduleName")//读取使用该注解处理器的项目中传入的参数
public class FrcTestAnnotationProcessor extends AbstractProcessor {

    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        messager.printMessage(Diagnostic.Kind.NOTE, "--------->FrcTestAnnotationProcessor init \n");
        String projectName = processingEnv.getOptions().get("moduleName");

        messager.printMessage(Diagnostic.Kind.NOTE, "--------->module name is :" + projectName);

    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE, "--------->FrcTestAnnotationProcessor process \n");

        return true;
    }
}

复制代码

编译一下看下输出:

image-20210204181552296

2.3 读取注解修饰对象的信息

我们之前是用FRC_TEST注解修饰了MainActivity类。现在我们要在注解处理器中读到该类当前信息,这样才能做相应操作。

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE, "--------->FrcTestAnnotationProcessor process \n");

        //获取被FRC_TEST注解的类的相关信息
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(FRC_TEST.class);		
        //由于同一注解可能会被多地使用,所以这里要遍历下
        for (Element element : elements) {
			//此处通过这个Element对象就能获取到个种  被注解类的信息 
            //获取被注解的类名
          messager.printMessage(Diagnostic.Kind.NOTE, "--------->class name  \n"+element.getSimpleName());

        }


        return true;
    }
复制代码

3. 生成辅助类

生成辅助类的本质,就是构建一个符合javalei的文件格式,然后写入本地。最直接的方式就是一行一行写,先写包名,再写导入的类,然后类名,方法等逐个往文件中写。比如EventBus框架:

image-20210205161131906

我们也可以借助工具来帮我们生成,减少代码量。

JavaPoet就是这样的工具:

frc-compiler module中添加javapoet依赖

dependencies {
  	...
    //javapoet
    compileOnly("com.squareup:javapoet:1.13.0")
		...
}
复制代码

参考JavaPoet中的demo,我们生成一个HelloWorld类,里面打印个"Hello, JavaPoet!"。

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE, "--------->FrcTestAnnotationProcessor process \n");

        //获取被FRC_TEST注解的类的相关信息
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(FRC_TEST.class);

        for (Element element : elements) {


            /**
             * package com.example.helloworld;
             *
             * public final class HelloWorld {
             *   public static void main(String[] args) {
             *     System.out.println("Hello, JavaPoet!");
             *   }
             * }
             */

            //生成一个main方法
            MethodSpec mainMethod = MethodSpec.methodBuilder("main")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(void.class)
                    .addParameter(String[].class, "args")
                    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                    .build();
            messager.printMessage(Diagnostic.Kind.NOTE, "--------->element name  \n"+element.getSimpleName());

//            //生成类
            TypeSpec helloWorldClass = TypeSpec.classBuilder("HelloWorld")
                    .addMethod(mainMethod)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .build();

            //写入包信息, 整个java文件信息完成
            JavaFile javaFile = JavaFile.builder("com.rong.cheng.study", helloWorldClass).build();

            //将javaFile 生成文件写入本地。此处需要用到 Filer,mFiler = processingEnv.getFiler();在init中获取
            try {
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                messager.printMessage(Diagnostic.Kind.NOTE, "生成FRC_TEST注解处理器,生成HelloWorld文件失败: " + e.getMessage());
                e.printStackTrace();
            }


        }
复制代码

编译后就会在下面位置生成如下文件:

image-20210205154415067

如果是使用的kapt来依赖注解处理器会在下面位置生成文件:

![image-20210205160645956](/Users/frc/Library/Application Support/typora-user-images/image-20210205160645956.png)

生成后,就可以在源码中使用了:

image-20210205160832755

至此整APT的正常使用流程就通了。


本文只是对APT的使用方式进行了简单的总结,方便自己以后查阅的同时分享给大家。对于APT的具体实际使用建议参考ARouter。

分类:
Android