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

简介

APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。

简单来说就是在编译期,通过注解生成.java文件。

作用

使用APT的优点就是方便、简单,可以少些很多重复的代码。根据规则,帮我们生成代码、生成类文件,如ButterKnife、Dagger、EventBus等注解框架。

Element 程序元素
元素 描述
PackageElement 表示一个包程序元素,提供对有关包及其成员的信息的访问
ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例)
TypeElement 表示一个类或接口程序元素,提供对有关类型及其成员的信息的方法
VaribaleElement 表示一个字段、enum常量、方法或构造方法参数、局部变量或异常参数
常用API
方法 描述
getEnclosedElements() 返回该元素直接包含的子元素
getEnclosingElement() 返回包含该element的父element,与上一个方法相反
getKind() 返回element的类型,判断是那种element
getModifiers() 获取修饰关键字,入public static final 等关键字
getSimpleName() 获取名字,不带包名
getQualifiedName() 获取全名,如果是类的话,包含完整的包名路径
getParameters() 获取方法的参数元素,每个元素是一个VariableElement
getReturnType() 获取方法元素的返回值
getConstantValue() 如果属性变量被final修饰,则可以使用该方法获取它的值

Demo

项目结构

  • 创建Android Module命名为app
  • 创建Java library Module命名为 apt-annotation 存放自定义注解
  • 创建Java library Module命名为 apt-compiler 依赖 apt-annotation、auto-service 指定注解处理器
  • 创建Android library Module 命名为 apt-library 存放工具类

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

环境配置

apt-annotation 对应的 build.gradle 进行配置
dependencies {
    ...
    // DES: 导入annotation库, 因lib中使用到了内置注解
    implementation 'androidx.annotation:annotation:1.0.0@jar'
}
// DES: 指定编码格式
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
// DES: 指定Java JDK 兼容及目标版本
// DES: 7 表示 Java JDK 1.7
sourceCompatibility = "7"
targetCompatibility = "7"
apt-compiler 对应的 build.gradle 进行配置
dependencies {
    ...
    // DES: 依赖自定义注解
    implementation project(":apt-annotation")
    // DES: 导入官方APT处理Service
    compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'

    // DES: 导入JavaPoet库用于类调用的形式来生成Java代码
    implementation 'com.squareup:javapoet:1.9.0'
}
// DES: 指定编码格式
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
// DES: 指定Java JDK 兼容及目标版本
// DES: 7 表示 Java JDK 1.7
sourceCompatibility = "7"
targetCompatibility = "7"
版本问题
// Android Studio 3.3.2 + Gradle 4.10.1 (临界版本)
// 注册注解,并对其生成META-INF的配置信息,rc2在gradle 5.1.1后有坑
// AS3.3.2 + Gradle 4.10.1 + auto-service:1.0-rc2
implementation 'com.google.auto.service:auto-service:1.0-rc2'

// Android Studio 3.4.1 + Gradle 5.1.1(向下兼容)
// AS3.4.1 + Gradle 5.1.1 + auto-service:1.0-rc4
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
apt-library 无需依赖
app 对应的 build.gradle 进行配置
dependencies {
    ...
    // 导入library
    implementation project(":apt-library")
    // 导入自定义注解
    implementation project(":apt-annotation")
    // 指定注释处理器
    annotationProcessor project(":apt-compiler")
    ...
}

相关代码

apt-annotation 自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 自定义注解 @BindView 牛油刀
 * @author XGY
 */
@Target(ElementType.FIELD) // 作用于成员属性上
@Retention(RetentionPolicy.RUNTIME) // 指定为运行时
public @interface BindView {
    int value();
}

apt-compiler
节点信息
/**
 * 节点信息Bean
 * @author XGY
 */
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; }
}
处理器
import com.example.annotation.BindView;
import com.example.compiler.bean.NodeInfo;
import com.google.auto.service.AutoService;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.Processor;
import javax.annotation.processing.ProcessingEnvironment;
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.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

/**
 * 自定义注解 @BindView 编译器
 * @author XGY
 */
@AutoService(Processor.class) // 指定注解处理器
@SupportedAnnotationTypes("com.example.annotation.BindView") // 指定需要处理的注解
@SupportedSourceVersion(SourceVersion.RELEASE_7) // 指定Java JDK编译版本
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<>();

    /** 初始化 */
    @Override
    public synchronized void init(ProcessingEnvironment environment) {
        super.init(environment);
        // 获取相关工具
        mElementUtils = environment.getElementUtils();
        mTypeUtils = environment.getTypeUtils();
        mMessager = environment.getMessager();
        mFiler = environment.getFiler();

        // 打印Build提示
        mMessager.printMessage(Diagnostic.Kind.NOTE, "开始处理自定义 @BindView 注解");
    }

    /** 处理注解 */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 判断是否有使用 @BindView 注解
        if (annotations != null && !annotations.isEmpty()) {
            // 获取所有 @BindView 节点
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
            // 判断节点集合不为空
            if (elements != null && !elements.isEmpty()) {
                // 循环遍历节点
                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 simpleName = 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, "simpleName:" + simpleName);
                    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, simpleName, value));
                        // 缓存
                        mCache.put(key, nodeInfos);
                    } else {
                        // 载入
                        nodeInfos.add(new NodeInfo(packageName, className, typeName, simpleName, value));
                    }
                }
                // 判断临时缓存是否不为空
                if (!mCache.isEmpty()) {
                    // 遍历临时缓存文件
                    for (Map.Entry<String, List<NodeInfo>> stringListEntry : mCache.entrySet()) {
                        try {
                            // 创建文件
                            createFile(stringListEntry.getValue());
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }

                return true;
            }
        }

        return false;
    }

    /** 创建Java文件 */
    private void createFile(List<NodeInfo> infos) throws IOException {
        // 获取第一个节点用来获取信息
        NodeInfo info = infos.get(0);
        // 生成的类名称
        String className = info.getClassName() + "$$ViewBinding";
        // JavaFileObject
        JavaFileObject file = mFiler.createSourceFile(info.getClassName() + "." + className);
        // Writer
        Writer writer = file.openWriter();
        // 设置包路径
        writer.write("package " + info.getPackageName() + ";\n\n");
        // 设置类名称
        writer.write("public class " + className + " {\n\n");
        writer.write("\tpublic static void bind(" + info.getClassName() + " target) {\n");
        // 循环遍历设置方法体
        for (NodeInfo node : infos) {
            writer.write("\t\ttarget." + node.getNodeName() + " = (" + node.getTypeName() +
                    ") target.findViewById(" + node.getValue() + ");\n");
        }
        writer.write("\t}\n");
        writer.write("}");
        // 关闭
        writer.close();
    }
}

==注意:==

// 指定注解处理器
@AutoService(Processor.class) 
// 指定需要处理的注解
@SupportedAnnotationTypes("com.example.annotation.BindView") 
// 指定Java JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)

关于文件生成可以使用JavaPoet,
相关地址:
Github


apt-library 工具类
import android.app.Activity;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * 自定义牛油刀工具类
 * @author XGY
 */
public final class ButterKnife {

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

app 中使用
public class MainActivity extends AppCompatActivity {
    
    // 使用自定义注解
    @BindView(R.id.textView)
    TextView textView;

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

推荐阅读更多精彩内容