Java编译期注解处理器APT

Java之编译时注解处理器(APT).png

1. APT简介

1.1 什么是APT?

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

1.2 APT的作用

通过APT可以获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省去了手动编写。在Android中有如ButterKnife、Dagger、EventBus等第三方框架,都采用了APT。
注意,获取注解及生成代码都是在代码编译时候完成的,相比反射在运行时处理注解大大提高了程序性能。

2. 创建APT的项目结构

apt项目结构.png
2.1 创建Android Module命名为app(主工程)
dependencies {
    ...
    // 导入自定义注解
    implementation project(":apt-annotation")
    // 指定注释处理器
    annotationProcessor project(":apt-compiler")
    ...
}
2.2 创建Java library Module命名为 apt-annotation 存放自定义注解

gradle配置文件如下:

apply plugin: 'java-library'

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

sourceCompatibility = "1.8"
targetCompatibility = "1.8"
2.3 创建Java library Module命名为 apt-compiler 依赖 apt-annotationauto-service

gradle配置文件如下:

apply plugin: 'java-library'

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

    implementation project(":apt-annotation")

    // 注册注解,并对其生成META-INF的配置信息
    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
    // 第三方自动生成代码框架(可以通过类的调用方式来自动生成代码,简洁高效)
    implementation 'com.squareup:javapoet:1.10.0'
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

为什么两个模块一定要是Java Library而不是Android Library?

  • 如果创建Android Library模块会发现不能找到AbstractProcessor这个类,这是因为Android平台是基于OpenJDK的,而OpenJDK中不包含APT的相关代码。因此,在使用APT时,必须在Java Library中进行。

3. AbstractProcessor分析(核心)

AbstractProcessor是一个抽象类,需要通过继承它来实现我们自己的注解解释器

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

    /**
     * 注解处理器的初始化阶段,可以通过ProcessingEnvironment来获取一些帮助我们来处理注解的工具类
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    /**
     * 指明有哪些注解需要被扫描到,返回注解的全路径(包名+类名)
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    /**
     * 用来指定当前正在使用的Java版本,一般返回SourceVersion.latestSupported()表示最新的java版本即可
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }

    /**
     * 核心方法,注解的处理和生成代码都是在这个方法中完成
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        return false;
    }
}

-@AutoService的作用是用来生成META-INF/services/javax.annotation.processing.Processor文件,并且自动将该注解标记的注解处理器添加到该文件中。

  • 在注解处理器的初始化阶段,可以通过参数ProcessingEnvironment来获取一些帮助我们来处理注解的工具类
// Element操作类
Elements elementUtils = processingEnv.getElementUtils();

// 类信息工具类
Types typeUtils = processingEnv.getTypeUtils();

// 日志工具类
Messager messager = processingEnv.getMessager();

// 文件工具类
Filer filer = environment.getFiler();

3.1 了解Element

在Java中Element是一个接口,表示一个程序元素,它可以指代包、类、方法或者一个变量。Element已知的子接口有如下几种:

  • PackageElement 表示一个包程序元素。提供对有关包及其成员的信息的访问。
  • ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
  • TypeElement 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。
  • VariableElement 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。

我们就拿一个简单的类举例,就可以很容易理解这些元素。

不同类型Element其实就是映射了Java中不同的类元素

package com.aptdemo.user; // PackageElement

public class User { // TypeElement
    private long id; // VariableElement
    private String username; // VariableElement

    public long getId() { // ExecutableElement
        return id;
    }

    public void setId( // ExecutableElement
                    long id) { // VariableElement
        this.id = id;
    }

    public String getUsername() { // ExecutableElement
        return username;
    }

    public void setUsername( // ExecutableElement
                    String username) { // VariableElement
        this.username = username;
    }
}

4. APT实例讲解

仿造ButterKnife中的 @BindView 注解,自动生成findViewById代码

4.1 在apt-annotation模块中自定义注解 @BindView
@Target(ElementType.FIELD) // 作用于成员属性上
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

4.2 在apt-compiler模块中,编写相关的注解处理逻辑

解析相关的节点信息存放到NodeInfo

public class NodeInfo {
    /**
     * 包路径
     */
    private String packageName;
    /**
     * 节点所在类名称
     */
    private String className;
    /**
     * 节点类型名称
     */
    private String typeName;
    /**
     * 节点名称
     */
    private String nodeName;
    /**
     * 注解的value
     */
    private int value;

    public NodeInfo(String packageName, String className, String typeName,
                    String nodeName, int value) {
        this.packageName = packageName;
        this.className = className;
        this.typeName = typeName;
        this.nodeName = nodeName;
        this.value = value;
    }

    public String getPackageName() {
        return packageName;
    }

    public String getClassName() {
        return className;
    }

    public String getTypeName() {
        return typeName;
    }

    public String getNodeName() {
        return nodeName;
    }

    public int getValue() {
        return value;
    }
}

编写核心的注解处理器

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    /**
     * Element操作类
     */
    private Elements mElementUtils;
    /**
     * 类信息工具类
     */
    private Types mTypeUtils;
    /**
     * 日志工具类
     */
    private Messager mMessager;
    /**
     * 文件创建工具类
     */
    private Filer mFiler;

    /**
     * 节点信息缓存
     */
    private Map<String, List<NodeInfo>> mCache = new HashMap<>();

    /**
     * 注解处理器的初始化阶段,可以通过ProcessingEnvironment来获取一些帮助我们来处理注解的工具类
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mElementUtils = processingEnv.getElementUtils();
        mTypeUtils = processingEnv.getTypeUtils();
        mMessager = processingEnv.getMessager();
        mFiler = processingEnv.getFiler();
    }

    /**
     * 指明有哪些注解需要被扫描到,返回注解的全路径(包名+类名)
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(BindView.class.getCanonicalName());
    }

    /**
     * 用来指定当前正在使用的Java版本,一般返回SourceVersion.latestSupported()表示最新的java版本即可
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 核心方法,注解的处理和生成代码都是在这个方法中完成
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (annotations == null || annotations.isEmpty()) return false;

        // 获取所有 @BindView 节点
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
        if (elements == null || elements.isEmpty()) return false;

        // 遍历节点
        for (Element element : elements) {
            // 获取节点包信息
            String packageName = mElementUtils.getPackageOf(element).getQualifiedName().toString();
            // 获取节点类信息,由于 @BindView 作用于成员属性上,所以这里使用 getEnclosingElement() 获取父节点信息
            String className = element.getEnclosingElement().getSimpleName().toString();
            // 获取节点类型
            String typeName = element.asType().toString();
            // 获取节点标记的属性名称
            String nodeName = element.getSimpleName().toString();
            // 获取注解的值
            int value = element.getAnnotation(BindView.class).value();

            // 打印
            mMessager.printMessage(Diagnostic.Kind.NOTE, "packageName:" + packageName);
            mMessager.printMessage(Diagnostic.Kind.NOTE, "className:" + className);
            mMessager.printMessage(Diagnostic.Kind.NOTE, "typeName:" + typeName);
            mMessager.printMessage(Diagnostic.Kind.NOTE, "nodeName:" + nodeName);
            mMessager.printMessage(Diagnostic.Kind.NOTE, "value:" + value);

            // 缓存KEY
            String key = packageName + "." + className;
            // 缓存节点信息
            List<NodeInfo> nodeInfos = mCache.get(key);
            if (nodeInfos == null) {
                nodeInfos = new ArrayList<>();
                nodeInfos.add(new NodeInfo(packageName, className, typeName, nodeName, value));
                // 缓存
                mCache.put(key, nodeInfos);
            } else {
                nodeInfos.add(new NodeInfo(packageName, className, typeName, nodeName, value));
            }
        }

        // 判断临时缓存是否不为空
        if (!mCache.isEmpty()) {
            // 遍历临时缓存文件
            for (Map.Entry<String, List<NodeInfo>> stringListEntry : mCache.entrySet()) {
                try {
                     // 创建文件
                     createFile(stringListEntry.getValue());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        return false;
    }

    /**
     * 创建文件,自动生成代码
     */
    private void createFile(List<NodeInfo> infos) throws IOException {
        NodeInfo info = infos.get(0);

        // 生成的文件名(类名)
        String className = info.getClassName() + "$$ViewBinding";

        // 方法参数
        ParameterSpec parameterSpec = ParameterSpec.builder(
                ClassName.get(info.getPackageName(), info.getClassName()), "target")
                .build();


        // 方法
        MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .addParameter(parameterSpec)
                .returns(void.class);

        // 给方法添加代码块
        for (NodeInfo nodeInfo : infos) {
            // target.textView = (TextView) target.findViewByID(R.id.text_view);
            methodSpecBuilder.addStatement("target.$L = ($L)target.findViewById($L)",
                    nodeInfo.getNodeName(),
                    nodeInfo.getTypeName(),
                    nodeInfo.getValue());
        }

        // 类
        TypeSpec typeSpec = TypeSpec.classBuilder(className)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(methodSpecBuilder.build())
                .build();

        // 生成文件
        JavaFile.builder(info.getPackageName(), typeSpec)
                .build()
                .writeTo(mFiler);
    }
}

javapoet的具体使用教程请参考:javapoet官方文档

4.3 在app模块中应用注解

在MainActivity中使用注解 @BindView

@BindView(R.id.text_view)
TextView textView;

然后再Rebuild Project,这样就可以在build->generated->ap_generated_sources->debug->包名路径下,看见自动生成的文件MainActivity$$ViewBinding

APT自动生成的文件MainActivity$$ViewBinding代码如下:

public class MainActivity$$ViewBinding {
  public static void bind(MainActivity target) {
    target.textView = (android.widget.TextView)target.findViewById(2131165354);
  }
}

这样我们就可以仿造ButterKnife,在App模块下新建一个ButterKnife类,利用反射来调用上面方法

public class ButterKnife {

    public static void bind(Activity target) {
        try {
            Class<?> clazz = target.getClass();
            // 反射获取apt生成的指定类
            Class<?> bindViewClass = Class.forName(clazz.getName() + "$$ViewBinding");
            // 获取它的方法
            Method method = bindViewClass.getMethod("bind", clazz);
            // 执行方法
            method.invoke(bindViewClass.newInstance(), target);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里我们的BindView注解功能就大功告成了

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.text_view)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ButterKnife.bind(this);
        textView.setText("Hello BindView");

    }
}

5. 参考

注解 - APT编译时注解处理器

Java编译时注解处理器(APT)详解

javapoet官方文档

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270