Android-设计模式与项目架构-01-编译插桩技术- APT(编译期注解处理器)-Java 版本

919 阅读8分钟

一、APT 介绍

1)什么是 APT ?

APT 全称 Annotation Processing Tool,翻译过来即注解处理器。引用官方一段对 APT 的介绍:APT 是一种处理注释的工具, 它对源代码文件进行检测找出其中的注解,并使用注解进行额外的处理。

2)APT 有什么用?

APT 能在编译期根据编译阶段注解,给我们自动生成代码,简化使用。很多流行框架都使用到了 APT 技术,如 ButterKnife,Retrofit,Arouter,EventBus 等等

二、APT 工程

1)APT 工程创建

一般情况下,APT 大致的的一个实现过程:

1、创建一个 Java-library Module: annotation ,用来编写注解

2、创建一个 Java-library Module : annotation_compiler,用来自定义注解注解处理器,并根据指定规则,生成相应的类文件

3、创建一个 Java-library Module :practice_java, 调用编译生成后的 class 类文件 注意这里创建的不是 application 模块

2)Module 的 Groovy DSL 配置

image.png

工程创建好后,我们就需要理清楚各个 Module 的Groovy配置信息:

1、 annotation 模块


plugins {
    id 'java-library'
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

2、 annotation_compiler 模块

plugins {
    id 'java-library'
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17


}

dependencies {

    implementation project(':annotation')

    implementation 'javax.annotation:javax.annotation-api:1.3.2' // 引入 javax.annotation-api

    implementation 'com.google.auto.service:auto-service:1.0.1' // 引入 auto-service
    annotationProcessor 'com.google.auto.service:auto-service:1.0.1' // 这是关键部分,用于Java项目





}

3、 practice_java 模块

plugins {
    id 'java-library'
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

//向编译器添加注解处理器参数
//tasks.withType(JavaCompile).configureEach {
//    options.compilerArgs += ["-Xlint:unchecked", "-Xlint:deprecation"] // 添加其他编译器参数
////    options.compilerArgs += ["-Akey=value"] // 添加注解处理器的参数
//
//}

dependencies {


    //测试自定义apt
    implementation project(':annotation')
    implementation project(':annotation_compiler')
    annotationProcessor project(':annotation_compiler')

}

APT 工程配置好之后,我们就可以对各个 Module 进行一个具体代码的编写了

三、annotation 注解编写

这个 Module 的处理相对来说很简单,就是编写相应的自定义注解就好了,我编写的如下:

// 定义注解
@Target(ElementType.TYPE)  // 指定注解可以用在类或接口上
@Retention(RetentionPolicy.SOURCE)  // 注解只在源码中保留,不会进入字节码
public @interface APTAnnotation {
    String value() default "Default Value";  // 定义一个属性
//    int priority() default 1;  // 定义一个优先级属性
}

四、annotation_compiler 注解处理器

这个 Module 相对来说比较复杂,我们把它分为以下 3 个步骤:

1、注解处理器声明

2、注解处理器注册

3、注解处理器生成类文件

1)注解处理器声明

首先引入一个依赖,用于自定义注解处理器

implementation 'javax.annotation:javax.annotation-api:1.3.2' 

(1) 新建一个类,继承 AbstractProcessor

//新建一个类,继承 `javax.annotation.processing` 这个包下的 AbstractProcessor
public class APTAnnotationCompile extends AbstractProcessor {

    /**
     * 编写生成 Java 类的相关逻辑
     *
     * @param set              支持处理的注解集合
     * @param roundEnvironment 通过该对象查找指定注解下的节点信息
     * @return true: 表示注解已处理,后续注解处理器无需再处理它们;false: 表示注解未处理,可能要求后续注解处理器处理
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

重点看下第一个参数中的 TypeElement ,这个就涉及到 Element 的知识,我们简单的介绍一下:

Element 介绍

实际上,Java 源文件是一种结构体语言,源代码的每一个部分都对应了一个特定类型的 Element ,例如包,类,字段,方法等等:

package com.dream;         // PackageElement:包元素

public class Main<T> {     // TypeElement:类元素; 其中 <T> 属于 TypeParameterElement 泛型元素

    private int x;         // VariableElement:变量、枚举、方法参数元素

    public Main() {        // ExecuteableElement:构造函数、方法元素
    }
}

Java 的 Element 是一个接口,源码如下:

public interface Element extends javax.lang.model.AnnotatedConstruct {
    // 获取元素的类型,实际的对象类型
    TypeMirror asType();
    // 获取Element的类型,判断是哪种Element
    ElementKind getKind();
    // 获取修饰符,如public static final等关键字
    Set<Modifier> getModifiers();
    // 获取类名
    Name getSimpleName();
    // 返回包含该节点的父节点,与getEnclosedElements()方法相反
    Element getEnclosingElement();
    // 返回该节点下直接包含的子节点,例如包节点下包含的类节点
    List<? extends Element> getEnclosedElements();

    @Override
    boolean equals(Object obj);

    @Override
    int hashCode();

    @Override
    List<? extends AnnotationMirror> getAnnotationMirrors();

    //获取注解
    @Override
    <A extends Annotation> A getAnnotation(Class<A> annotationType);

    <R, P> R accept(ElementVisitor<R, P> v, P p);
}

我们可以通过 Element 获取如上一些信息(写了注释的都是一些常用的)

由 Element 衍生出来的扩展类共有 5 种:

1、PackageElement 表示一个包程序元素

2、TypeElement 表示一个类或者接口程序元素

3、TypeParameterElement 表示一个泛型元素

4、VariableElement 表示一个字段、enum 常量、方法或者构造方法的参数、局部变量或异常参数

5、ExecuteableElement 表示某个类或者接口的方法、构造方法或初始化程序(静态或者实例)

可以发现,Element 有时会代表多种元素,例如 TypeElement 代表类或接口,此时我们可以通过 element.getKind() 来区分:

Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(AptAnnotation.class);
for (Element element : elements) {
    if (element.getKind() == ElementKind.CLASS) {
        // 如果元素是类

    } else if (element.getKind() == ElementKind.INTERFACE) {
        // 如果元素是接口

    }
}
复制代码

ElementKind 是一个枚举类,它的取值有很多,如下:

PACKAGE	//表示包
ENUM //表示枚举
CLASS //表示类
ANNOTATION_TYPE	//表示注解
INTERFACE //表示接口
ENUM_CONSTANT //表示枚举常量
FIELD //表示字段
PARAMETER //表示参数
LOCAL_VARIABLE //表示本地变量
EXCEPTION_PARAMETER //表示异常参数
METHOD //表示方法
CONSTRUCTOR //表示构造函数
OTHER //表示其他
复制代码

关于 Element 就介绍到这,我们接着往下看

(2) 重写方法解读

除了必须实现的这个抽象方法,我们还可以重写其他 4 个常用的方法,如下:

public class APTAnnotationCompiler extends AbstractProcessor {
    //...

    /** 
     * 节点工具类(类、函数、属性都是节点)
     */
    private Elements mElementUtils;

    /** 
     * 类信息工具类
     */
    private Types mTypeUtils;

    /**
     * 文件生成器
     */
    private Filer mFiler;

    /**
     * 日志信息打印器
     */
    private Messager mMessager;

    /**
     * 做一些初始化的工作
     * 
     * @param processingEnvironment 这个参数提供了若干工具类,供编写生成 Java 类时所使用
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mElementUtils = processingEnv.getElementUtils();
        mTypeUtils = processingEnv.getTypeUtils();
        mFiler = processingEnv.getFiler();
        mMessager = processingEnv.getMessager();
    }

    /**
     * 接收外来传入的参数,最常用的形式就是在 build.gradle 脚本文件里的 javaCompileOptions 的配置
     *
     * @return 属性的 Key 集合
     */
    @Override
    public Set<String> getSupportedOptions() {
        return super.getSupportedOptions();
    }

    /**
     * 当前注解处理器支持的注解集合,如果支持,就会调用 process 方法
     *
     * @return 支持的注解集合
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    /**
     * 编译当前注解处理器的 JDK 版本
     *
     * @return JDK 版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }
}
复制代码

注意getSupportedAnnotationTypes()getSupportedSourceVersion()getSupportedOptions() 这三个方法,我们还可以采用注解的方式进行提供:


@SupportedAnnotationTypes("com.example.annotation.APTAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class APTAnnotationCompiler extends AbstractProcessor {
//定义注解处理器,注解处理器是在编译时自动调用的
 
    //...
}

2)注解处理器注册

注解处理器声明好了,下一步我们就要注册它,其中注册有两种方式:

  • 1、手动注册
  • 2、自动注册

(1) 手动注册

手动注册暂不介绍

(2) 自动注册

1、首先我们要在 apt-processor这个 Module 下的 build.gradle 文件导入如下依赖:

dependencies {
    compileOnly 'com.google.auto.service:auto-service:1.0.1'
    annotationProcessor 'com.google.auto.service:auto-service:1.0.1'
}

注意:这两句必须都要加,否则注册不成功,我之前踩坑了

2、在注解处理器上加上 @AutoService(Processor.class) 即可完成注册

@AutoService(Processor.class)
public class AptAnnotationProcessor extends AbstractProcessor {
    //...
}

3)注解处理器生成类文件

注册完成之后,我们就可以正式编写生成 Java 类文件的代码了,其中生成也有两种方式:

  • 1、常规的写文件方式
  • 2、通过 javapoet 框架来编写

1 的方式比较死板,需要把每一个字母都写上,不推荐使用,这里就不讲了。我们主要看下通过 javapoet 这个框架生成 Java 类文件

(2) javapoet 方式

这种方式更加符合面向对象编码的一个风格,对 javapoet 还不熟的朋友,可以去 github 上学习一波 传送门,这里我们介绍一下它常用的一些类:

TypeSpec:用于生成类、接口、枚举对象的类

MethodSpec:用于生成方法对象的类

ParameterSpec:用于生成参数对象的类

AnnotationSpec:用于生成注解对象的类

FieldSpec:用于配置生成成员变量的类

ClassName:通过包名和类名生成的对象,在JavaPoet中相当于为其指定 Class

ParameterizedTypeName:通过 MainClass 和 IncludeClass 生成包含泛型的 Class

JavaFile:控制生成的 Java 文件的输出的类

1、导入 javapoet 框架依赖
implementation 'com.squareup:javapoet:1.13.0'
2、按照指定代码模版生成 Java 类文件
@SupportedAnnotationTypes("com.example.annotation.APTAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@AutoService(Processor.class)  // 自动生成处理器配置文件
public class APTAnnotationCompiler extends AbstractProcessor {//定义注解处理器,注解处理器是在编译时自动调用的
    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        messager.printMessage(Diagnostic.Kind.NOTE, "注解处理器初始化 " );

    }
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE, "注解处理器执行了");

        for (Element element : roundEnv.getElementsAnnotatedWith(APTAnnotation.class)) {
            APTAnnotation annotation = element.getAnnotation(APTAnnotation.class);
            String value = annotation.value();

            // 在这里生成代码或执行其他操作
            generateClass(element, value);
        }
        return true;
    }

    private void generateClass(Element element, String value) {
        // 实现代码生成逻辑,例如生成一个Java类
        // 使用Filer创建文件并写入内容
        // 获取注解
        APTAnnotation annotation = element.getAnnotation(APTAnnotation.class);
        String className = element.getSimpleName() + "Generated";
        String packageName = processingEnv.getElementUtils().getPackageOf(element).toString();

        // 生成类内容
        String content = "package " + packageName + ";\n\n" +
                "public class " + className + " {\n" +
                "    public static void print() {\n" +
                "        System.out.println("" + annotation.value() + "");\n" +
                "    }\n" +
                "}";

        try {
            // 创建Java源文件
            JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(packageName + "." + className);
            Writer writer = fileObject.openWriter();
            writer.write(content);
            writer.close();
        } catch (IOException e) {
            System.out.println("生成异常: "+e.getMessage());
            e.printStackTrace();
        }
    }

}

五、编译并调用生成代码

1) 编译生成:

annotationProcessor注解必须有调用的地方,注解处理器才能找到并处理,只定义不使用则不会编译生成文件,所以在practice_java module 中可看到编译生成的文件

image.png

2) 反射调用:

//测试自定义注解生成的类
public class Test {
    public static void init() {
        try {
            Class c = Class.forName("com.example.java.senior.b_annotation.apt.MyClassGenerated");
            Constructor declaredConstructor = c.getDeclaredConstructor();
            Object o = declaredConstructor.newInstance();
            Method test = c.getDeclaredMethod("print");
            test.invoke(o);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
       init();
    }
}

运行 main 函数结果如下:

image.png

六、总结

1、编译的jdk版本(graoovy配置),项目的jdk 版本,以及@SupportedSourceVersion(SourceVersion.RELEASE_17)需要保持一致.

2、Java 源文件实际上是一种结构体语言,源代码的每一个部分都对应了一个特定类型的 Element

3、采用 auto-service 对注解处理器进行自动注册

4、采用 javapoet 框架编写所需生成的 Java 类文件

5、通过反射封装,调用编译后的 class 文件

七 参考

Android APT 系列 (三):APT 技术探究前言 很高兴遇见你~ 在本系列的上一篇文章中,我们对注解进行了讲解 - 掘金 (juejin.cn)