Android 注解--(二)利用注解技术在编译期生成代码

上篇文章已经讨论了注解的作用和注解的生命周期,那么具体注解是如何简化我们工作,减少我们编写重复的代码的呢?接下我们将学习使用APT,在编译期生成代码。

APT是Annotation-Processing-tool的简写,称为注解处理器,一般来说,自定义注解是在运行时使用的,通过反射获取class上的注解,并进行解析处理,使用apt可以让我们在编译时处理注解(其实不仅仅可以处理注解,而是所有的类信息都可以处理,下面会有演示),我们这一篇一起来学习一下基本的套路。

我们使用Android studio编写一个基本的例子。

1.创建注解工程

同样我们先创建一个Java工程,编写一个注解类MyAnnotation

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
    String value() default "MyAnnotation";
}

然而这一次我们就不是在反射的时候获取这个注解的信息了,我们要在编译的时候获取这个注解的信息,如何获取,就要使用到注解处理器Processor。

注解处理器Processor

了解一下注解处理器,注解处理器有什么作用呢,首先它会在编译期被调用,可以扫描特定注解的信息,你可以为你自己的的注解注册处理器(如何注册后面会讲),一个特定的注解处理器以java源码作为输入,然后生成一些文件作(通常为java)为输出,这些java文件同样会被编译。这意味着,你可以根据注解的信息和被注解类的信息生成你想生成的代码!

要生成怎么样的文件,我们简单定一下需求:
定义一个注解MyAnnotation,去注解MainActivity,然后处理器扫描生成一个java文件,这个java文件有个输出Hello MyAnnotation的方法,运行的我们的MainAcitivity,然后调用这个java文件的方法。

2.创建Android工程

之前我们已经定义了我们注解MyAnnotation,并且有个默认值value是字符串MyAnnotation。接下来,我们要用这个去注解MainActivity,现在我们是在Java工程,那么我们新创建一个Android工程,里面有个MainActivity,这个工程依赖我们MyAnnotation所在的工程。

@MyAnnotation
public class MainActivity extends AppCompatActivity {

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

接下来我们就要通过自己定义的注解处理器去扫描这个注解进而生成java文件,但是在此之前,我们需要先了解注解处理的工作流程和相关API。

3.创建Compiler工程

AbstractProcessor

AbstractProcessor就是系统抽象出来的处理器类,编写一个MyProcessor继承AbstractProcessor,查看一下里面的方法

public class MyProcessor extends AbstractProcessor{

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
    
}

我们需要重写的方法有这么几个,
init(ProcessingEnvironment processingEnv) :所有的注解处理器类都必须有一个无参构造函数。然而,有一个特殊的方法init(),它会被注解处理工具调用,以ProcessingEnvironment作为参数。ProcessingEnvironment 提供了一些实用的工具类Elements, Types和Filer。我们在后面将会使用到它们。

process(Set<? extends TypeElement> annoations, RoundEnvironment env) :这类似于每个处理器的main()方法。你可以在这个方法里面编码实现扫描,处理注解,生成 java 文件。使用RoundEnvironment 参数,你可以查询被特定注解标注的元素。

getSupportedAnnotationTypes():在这个方法里面你必须指定哪些注解应该被注解处理器注册。注意,它的返回值是一个String集合,包含了你的注解处理器想要处理的注解类型的全称。换句话说,你在这里定义你的注解处理器要处理哪些注解。

getSupportedSourceVersion() : 用来指定你使用的 java 版本,建议使用SourceVersion.latestSupported()。

接下来你必须知道的事情是:注解处理器运行在它自己的 JVM 中。是的,你没看错。javac 启动了一个完整的 java 虚拟机来运行注解处理器。这意味着什么?你可以使用任何你在普通 java 程序中使用的东西。使用 guava! 你可以使用依赖注入工具,比如dagger或者任何其他你想使用的类库。但不要忘记,即使只是一个小小的处理器,你也应该注意使用高效的算法及设计模式,就像你在开发其他java程序中所做的一样。

注意:在此之前我相信很多同学和我一样把MyProcessor写在了跟MyAnnotation中一样的工程里面,这里有一个compiler工程的分离原则,processor就是我们的compiler工程,这个Processor对于目标工程也就是我们上面的MainActivity所在的Android工程是不需要的,Android主工程所需要的是生成java文件。所以我们需要新建一个java工程作为存放我们MyProcessor的compiler工程,这个工程同样要依赖MyAnnotation所在的注解工程。

注册处理器

我们在编译好的META-INF/services添加我们的处理器路径,谷歌已经提供一个很方便的库,帮助我们做这些东西,我们只需要在处理器工程添加依赖

compile 'com.google.auto.service:auto-service:1.0-rc2'

然后在Myprocessor中添加@AutoService(Processor.class)的注解,这样就完成了我们处理器的注册。


编译成生成的META-INF/services中就注册了我们的MyProcessor


接下来,我们编写一个我们自己的处理器,生成java文件,来讲解一下相关API,以及要注意的事项。

处理后的MyProcessor

public class MyProcessor extends AbstractProcessor{
    //处理Element的工具类
    private Elements mElementUtils;
    //生成文件的工具
    private Filer mFiler;
    //日志信息的输出
    private Messager mMessager;


    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mElementUtils = processingEnv.getElementUtils();
        mFiler = processingEnv.getFiler();
        mMessager = processingEnv.getMessager();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes(){
        Set<String> supportedType = new LinkedHashSet<String>();
        supportedType.add(MyAnnotation.class.getCanonicalName());
        return supportedType;
    }

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
       //do something
       return false
    }

接下来重点讲一下process方法的实现,之前提到,这个方法中会扫描文件,可以获取到带我们自定义注解的类,通过如下这个方法

//获取MyAnnotation注解的类的集合
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(MyAnnotation.class);

Element

Element是什么?Element代表着源代码中的各种结构类型。

package com.example;

public class Foo { // TypeElement

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

    public Foo() {} // ExecuteableElement

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

换句话说:Element代表程序中的元素,比如说 包,类,方法。每一个元素代表一个静态的,语言级别的结构。我们可以像这样去解析一个类源码来获取Element。

TypeElement fooClass = ... ;
for (Element e : fooClass.getEnclosedElements()){ // getEnclosedElements获取子Element
    Element parent = e.getEnclosingElement();  //获取父级Element 
}

上面提到我们process方法要扫描我们的指定注解的类,我们process方法简单编写为

public class MyProcessor extends AbstractProcessor{
    ···
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //获取MyAnnotation注解的类的集合
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(MyAnnotation.class);
        for (Element element : set){
            //这里往往要进行被注解类的匹配判断,看是否符合我们的要求
            // if(isVaild(element))...
            if(element.getKind() == ElementKind.CLASS){
                //TODO:写java文件
            }
        }
        return false;
    }
}

这里返回值的意思是是否停止扫描,在这里一般要结合匹配判断来使用的,因为在这里你根本不知道使用者会怎么使用你的注解,如果注解不正当使用,你需要提示用户出错了(mMessager输出日志),并且返回true,标识不再继续扫描了。我这里因为例子简单,容易控制,主要便于理解注解的原理,所以我省略了匹配判断。

上面element.getKind() == ElementKind.CLASS判断,说明我这个注解使用在类上面,我才拿来输出文件,使用在其他地方不做处理。

接下来就是生成java文件啦,首先脑子里要有生成文件的代码,根据我们的需求,我们首先是会在MainActivity类中使用我们的MyAnnotation注解的,生成的文件名需要区别开来,就在被注解类名后面加上$$HelloWorld,里面还有个输出Hello MyAnnotation的方法,那么我们的文件就生成这样的吧:

public final class MainActivity$$HelloWorld {

    public static void sayHello() {
        System.out.println("Hello MyAnnotation");
    }
}

生成java文件

继续编写我们process的方法,来编写java源文件,这里我使用的是javapoet,具体的使用可以查看他们的github说明文档,写文件非常好用。

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(MyAnnotation.class);
        for (Element element : set){
            if(element.getKind() == ElementKind.CLASS){
                TypeElement typeElement = (TypeElement) element;
                brewJavaFile(typeElement);
            }
        }
        return true;
    }

因为我们知道我们只会注解在类上面,所以直接把element强转为TypeElement。写文件的代码在brewJavaFile中

private void brewJavaFile(TypeElement pElement){
        //sayHello 方法
        MyAnnotation myAnnotation = pElement.getAnnotation(MyAnnotation.class);
        MethodSpec methodSpec = MethodSpec.methodBuilder("sayHello")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC).returns(void.class)
                .addStatement("$T.out.println($S)",System.class,"Hello"+myAnnotation.value()).build();

        // class
        TypeSpec typeSpec = TypeSpec.classBuilder(pElement.getSimpleName().toString()+"$$HelloWorld").addModifiers(Modifier.PUBLIC,Modifier.FINAL).addMethod(methodSpec).build();
        // 获取包路径,把我们的生成的源码放置在与被注解类中同一个包路径中
        JavaFile javaFile = JavaFile.builder(mElementUtils.getPackageOf(pElement).getQualifiedName().toString(),typeSpec).build();
        try {
            javaFile.writeTo(mFiler);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

那么我们的生成文件的代码就已经写完了,这里要提一下,我们在编写的过程中,我想看一下这个类的路径运行的时候怎么查看呢,我们没有编写输出日志呀,那么日志如何查看,或者说怎么调试定位错误,这里使用mMessager来输出日志。

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        ...
        TypeElement typeElement = (TypeElement) element;
        String qualifiedName = typeElement.getQualifiedName().toString();
        mMessager.printMessage(Diagnostic.Kind.NOTE,"qualifiedName:"+qualifiedName);
        ...
        return false;
    }

输出如下

OK,我们写完了生成文件代码,终于可以测试一波了,新建一个Android工程,该工程依赖注解工程,至于compiler处理器工程,我们要使用apt 的方式依赖,首先在项目目录下的build.gradle中添加
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'这个依赖

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.1'
        // apt 
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

然后在Android 工程中,添加这个插件依赖

然后就可以使用apt依赖处理器工程了

apt project(':xxxCompiler')

运行我们的Android工程,查看build生成文件

我们文件如愿生成啦,接下来要做的就是去调用这个sayHello的方法,我们的思路是通过反射生成的类,调用sayHello的方法。

在注解工程中,新建AnnotationApi类,编码如下

public class MyAnnotationApi {
    
    public static void sayHelloAnnotation(Object pTarget){
        String name = pTarget.getClass().getCanonicalName();
        try {
            Class clazz = Class.forName(name+"$$HelloWorld");
            Method method = clazz.getMethod("sayHello");
            method.invoke(null);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

}

然后在MainActivity中调用sayHelloAnnotation的方法

@MyAnnotation
public class MainActivity extends AppCompatActivity {

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

查看输出



至此,我们的需求就已经全部实现了。

总结

总结一下,这个例子可以看到,注解处理器非常强大,通过编译期生成代码,让注解处理器去生成这些代码,可以省去我们编写重复代码的时间。除此之外,我们应该思考我们使用的注解框架,比如说butterkinef是怎么工作的,查看源码或者自己学习编写一下注解框架,进而加深注解和注解框架的理解。

参考:
Annotation-Processing-Tool详解(想深入理解看这个篇,非常详细)
Android利用APT技术在编译期生成代码

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,565评论 25 707
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,355评论 6 343
  • 很多时候我们都会抱怨为什么自己用心付出了那么多却依然达不到我们想要的结果 你用心爱一个人他最后选择的是别人你用心设...
    Lapin阅读 1,014评论 0 0
  • 寻找温暖和相对的安静。 爱触及不到的地方,孤独无孔不入。 战胜孤独,唯有爱; 驱除孤独,可以转移注意力; 直面孤独...
    真实的人生阅读 186评论 1 0
  • 柴先生为什么你每天都这么暖
    沙漏倒装记忆阅读 322评论 0 0