Java面向切面编程技术一:apt

406 阅读2分钟

[toc]

一句话理解

注解(annotation)是一种另类的注释,如果不与其它技术结合,它不会起任何作用;常常结合的技术包括apt(annotation processing tool),asm等;它常常用来改变原有的执行逻辑,比如根据注解修改或新增源码(apt/asm),或者在运行期间与反射结合修改执行逻辑。

一.注解的产生与定义

注解是在jdk 1.5之后引入的一种注释,与普通被虚拟机忽略的注释不一样,注解根据它的作用期不一样,在被注解处理器处理后,会产生不同的效果。它可以被注解在指定的类,方法,字段,参数前。

如果没有注解处理器,它将和普通的注释一样,不起任何作用。

它产生的目的是通过一种简单而直观的方式,在不同阶段影响字节码的运行流程。

1.注解的定义

通过@interface这个标记,我们可以定义一个注解 它包含两个核心的元素:

  • 注解的生命周期 @Rentention,用于像编译器告知注解产生作用的时机,它主要包含源码期(.java文件处理阶段),编译器(.class文件处理阶段),运行期(字节码解释运行阶段)
  • 注解的作用对象 @Target , 它标记了注解作用的对象,一般包含类,属性,方法,参数以及注解等,标记注解的注解,我们常常称为元注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

2.注解与Annotation的关系

测试下定义的注解与Annotation的关系

System.out.println("an=" + an);
System.out.println("an.annotationType=" + an.annotationType());
System.out.println("an.getClass:()=" + an.getClass());
System.out.println("an instanceof Annotation=" + (an instanceof Annotation));
System.out.println("BindView.class=" + OnClick.class);

输出如下:

System.out: an=@com.hch.ioc.BindView(value=[2131231092])
System.out: an.annotationType=interface com.hch.ioc.BindView
System.out: an.getClass:()=class $Proxy1
System.out: an instanceof Annotation=true
System.out: BindView.class=interface com.hch.ioc.BindView

由此可以得出结论:

  • 通过@interface定义的注解,它本身是一个Annotation
  • 注解的annotationType与通过.class获取的注解类型一致,都是具体的注解类型interface com.hch.ioc.BindView
  • 通过getClass得到的并不是Annotation,而是一个代理class对象

二.注解的生命周期

注解的生命周期决定了编译器在哪个阶段去处理注解 通过@Rentention这个元注解,可以定义注解的作用期,对于每个注解,仅能作用于一个作用期,它的定义如下:

package java.lang.annotation;
public enum RetentionPolicy {
    SOURCE,            /* Annotation信息仅存在于编译器处理期间,编译器处理完之后就没有该Annotation信息了  */

    CLASS,             /* 编译器将Annotation存储于类对应的.class文件中。默认行为  */

    RUNTIME            /* 编译器将Annotation存储于class文件中,并且可由JVM读入 */
}

从生命周期来看, RUNTIME > CLASS > SOURCE

三.注解的作用对象

注解处理器处理时,需要告知得到注解所作用的对象,它通过@Target这个元注解来标注,常用的作用对象包括:

package java.lang.annotation;

public enum ElementType {
    TYPE,               /* 类、接口(包括注释类型)或枚举声明  */
    FIELD,              /* 字段声明(包括枚举常量)  */
    METHOD,             /* 方法声明  */
    PARAMETER,          /* 参数声明  */
    CONSTRUCTOR,        /* 构造方法声明  */
    LOCAL_VARIABLE,     /* 局部变量声明  */
    ANNOTATION_TYPE,    /* 注释类型声明  */
    PACKAGE ,            /* 包声明  */
    TYPE_PARAMETER ,            /* 泛型参数申明  */
    TYPE_USE ,            /* 泛型变量使用申明  */
}

其中TYPE_PARAMETER , TYPE_USE 从java 1.8之后引入,用于注解泛型的类型

public class Callback<@NoneNull T> {
    ...
}

@Target(ElementType.TYPE_PARAMETER)
public @interface NoneNull {}
@Test String text;

@Target(ElementType.TYPE_USE)
public @interface Test {}

四.注解处理器

annotation Processing tool 负责扫描并处理注解。它可以处理处于任何生命周期的注解

使用apt的步骤总结如下:

  • 新建Java library ,命名为annotation,用于存放需要处理的注解
  • 新建Java library ,命名为annotationProcessor ,用于处理注解,它依赖于annotation library
  • 在android app 依赖annotation 和annotationProcessor

下面以BindView 这个注解来举例说明

1.新建 java library库(annotation)

新建流程很简单

graph LR
file-->new_module
new_module --> javalibrary

为什么是java library ,因为annotationProcessor库必须是java库,而java库无法依赖android 库。

添加BindView

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

2.新建 java library库(annotationProcessor)

在build.gradle中添加依赖

  • auto-service用于自动生成annotationProcessor的描述,并且对不同的注解分别采用step处理,每个step都只需要申明的annotation.
  • javapoet 用于生成新的java文件
  • 对于annotation library的依赖
plugins {
    id 'java-library'
}

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

tasks.withType(JavaCompile){
    options.encoding = "UTF-8"
}

dependencies {
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc3'
    implementation "com.google.auto.service:auto-service:1.0-rc3"
    implementation "com.google.auto:auto-common:1.1.2"
    implementation 'com.squareup:javapoet:1.13.0'

    // 依赖于注解
    implementation project(':annotation')
}

新建注解处理器HCHProcessor,

  • steps方法定义了要处理的注解和实际的处理逻辑。
  • @AutoService 会帮助自动生成annotationsProcessor在build/classes/META-INF/services/javax.annotation.processing.Processor中的注解申明内容为com.huya.annotationprocessor.HCHProcessor
  • getSupportedSourceVersion 指定要支持的java的源码版本
@AutoService(Processor.class)
public class HCHProcessor extends BasicAnnotationProcessor {

    @Override
    protected Iterable<? extends Step> steps() {
        return ImmutableList.of(new BindViewStep(processingEnv) , new FastClickStep(processingEnv));
    }


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

BindViewStep中代码如下:

  • annotations 方法定义了该step需要处理的注解类型
  • process方法为实际的处理逻辑,它包含生成java文件和写java文件两个步骤,生成java文件通过javapoet来操作,这个部分比较复杂,后续篇幅再说明。
public class BindViewStep extends BasicStep {

    public BindViewStep(ProcessingEnvironment processingEnv) {
        super(processingEnv);
    }

    @Override
    protected void process(List<Element> elementsByAnnotation) {
        // 1. build class  file
        TypeSpec typeSpec = buildClass(elementsByAnnotation);

        // 2.write file
        //如何解决包名获取问题
        JavaFile javaFile = JavaFile.builder("com.hch",typeSpec).build();
        mProcessionEnv.getMessager().printMessage(Diagnostic.Kind.NOTE , javaFile.toString());

        try {
            javaFile.writeTo(mProcessionEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private TypeSpec buildClass(List<Element> elementsByAnnotation) {
        Element enclosingElement = elementsByAnnotation.get(0).getEnclosingElement();
        TypeMirror enclosingType = enclosingElement.asType();
        TypeSpec typeSpec = TypeSpec.classBuilder(enclosingElement.getSimpleName().toString()+"_viewBinding")
                .addModifiers(Modifier.PUBLIC)
                .addMethod(buildMethod(elementsByAnnotation))
                .addSuperinterface(IBinder.class)
                .build();
        return typeSpec;
    }

    private MethodSpec buildMethod(List<Element> elementsByAnnotation) {
        Element enclosingElement = elementsByAnnotation.get(0).getEnclosingElement();
        TypeMirror enclosingType = enclosingElement.asType();
        MethodSpec.Builder build = null;
        build = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .returns(TypeName.VOID)
                //从一个element类型获取一个Type
//                .addParameter(TypeName.get(enclosingType)  , "target");
                .addParameter(TypeName.get(Object.class)  , "target");

        for (Element variable : elementsByAnnotation) {
            //如何获取转化类型
            build.addStatement("(($L)target).$L = (android.view.View)(($L)target).findViewById($L)"
                    , enclosingType.toString()
                    , variable.getSimpleName().toString()
                    , enclosingType.toString()
                    , variable.getAnnotation(BindView.class).value());
        }
        MethodSpec methodSpec = build.build();
        return methodSpec;
    }

    @Override
    public Set<String> annotations() {
        return new HashSet<String>(Arrays.asList(BindView.class.getCanonicalName()));
    }
}

五.app中的依赖

app中只需要简单依赖annotation,并且申明 annotationProcessor即可

implementation project(':annotation')
annotationProcessor project(':annotationProcessor')

选择 build-rebuild project ,会在javac这个任务的任务下输出处理注解的内容 ,例子中的输出如下:

Task :app:compileHuaweiApi21DebugJavaWithJavac
The following annotation processors are not incremental: jetified-annotationProcessor-1.1.0.jar (project :annotationProcessor), jetified-auto-service-1.0-rc3.jar (com.google.auto.service:auto-service:1.0-rc3).
Make sure all annotation processors are incremental to improve your build speed.
注: BindViewStep xxxxxxxx key=MainActivity element=testAnnotation elementKind=FIELD asType=android.view.View element.getEnclosingElement=com.hch.MainActivity element.getEnclosingElement.asType=com.hch.MainActivity element.getEnclosingElement=class com.sun.tools.javac.code.Symbol$ClassSymbol
注: 
注: BindViewStep xxxxxxxx key=MainActivity element=testDagger elementKind=FIELD asType=android.view.View element.getEnclosingElement=com.hch.MainActivity element.getEnclosingElement.asType=com.hch.MainActivity element.getEnclosingElement=class com.sun.tools.javac.code.Symbol$ClassSymbol

生成的辅助类内容如下:

package com.hch;

import com.hch.annotation.IBinder;
import java.lang.Object;
import java.lang.Override;

public class MainActivity_viewBinding implements IBinder {
  @Override
  public void bind(Object target) {
    ((com.hch.MainActivity)target).testAnnotation = (android.view.View)((com.hch.MainActivity)target).findViewById(2131231093);
    ((com.hch.MainActivity)target).testDagger = (android.view.View)((com.hch.MainActivity)target).findViewById(2131231094);
  }
}

六.注解中的一些基础概念

1.Element

public class Foo { // TypeElement

    private int a; // VariableElement
    private Foo other; // VariableElement

    public Foo() {} // ExecuteableElement

    public void setA( // ExecuteableElement
            int newA // TypeElement
    ) {
    }
}

官方说明

2.MirrorType

代表所处理的注解在java语言中的class类型

官方说明

3.Elements

用来对程序元素(Element)进行操作的实用工具方法

官方说明

4.Types

用来对类型(MirrorType)进行操作的实用工具方法。

官方说明

5.ProcessingEnvironment

编写新文件、报告错误消息并查找其他实用工具的聚合上下文环境

官方说明

七.参考资料

cloud.tencent.com/developer/a…

github.com/google/auto

kahnsen.github.io/kahnblog/20…