编译时注解处理方

编译时注解处理

若希望对编译时的注解进行处理需要做

  1. 自定义类集成自AbstractProcessor
  2. 重写其中的process函数

这块很多同学不理解,实际是编译器在编译时自动查找所有继承自AbstractProcessor的类,然后调用他们的process方法去处理

原始处理方式

Example

  1. 创建项目AnnotationTest工程
  2. 创建annotation模块(这个模块中用来放注解类型的类)
  3. 创建complier模块(这个模块用来放处理注解的类)

需要注意,AbstractProcessor是在javax包中,而android核心库中不存在该包,因此在选择创建annotationcomplier模块时需要选择java Library

工程结构如下:

14863104472100.jpg

接下来我们在annotation模块编写一个运行时注解@Test:该注解会生成一个指定格式的类,先看看该注解的定义:

@Retention(CLASS)
@Target(METHOD)
public @interface Test {
    String value();
}

接下来,我们在complier模块需要为其编写注解处理器,代码比较简单,直接来看:

public class TestProcessor extends AbstractProcessor {

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

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

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

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("------ process -----");
        //MethodSpec这个类是要引入'com.squareup:javapoet:1.8.0'包,方便通过代码创建java文件
        MethodSpec main = MethodSpec.methodBuilder("main")
                //.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(void.class)
                .addParameter(String[].class, "args")
                .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                .build();

        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                //netstat -an | grep .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(main)
                .build();

        JavaFile javaFile = JavaFile.builder("com.francis.helloworld", helloWorld)
                .build();

        try {
            //这里的输出要在Gradle Console中看
            javaFile.writeTo(System.out);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

}

若不知道其中的方法和其中涉及的类的相关含义移步AbstractProcessor介绍

到现在为止,我们已经编写好了注解类及其对应的处理器.现在我们仅需要对其进行配置.

  1. main文件夹下创建resources文件夹
  2. resources资源文件夹下创建META-INF文件夹
  3. 然后在META-INF文件夹中创建services文件夹
  4. 然后在services文件夹下创建名为javax.annotation.processing.Processor的文件,在该文件中配置需要启用的注解处理器,即写上处理器的完整路径,有几个处理器就写几个,分行写幺,比如我们这里是:
com.francis.example.TestProcessor

到现在我们已经做好打包之前的准备了,此时项目结构如下:

14863111455430.jpg

现在简单的在项目中使用一下我们自己定义的注解:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Test("test")
    public void test(){

    }
}

现在编译app模块,在编译过程中你可以在Gradle Console(不要在Android Monitor中的logcat中去找,眼镜找瞎了都找不到)看到输出的信息,不出意外的话,你讲看到一下信息:

Paste_Image.png

高效处理方式

原始的处理方式有如下几个问题:

  1. 注解处理类不应该被打包到APK中来增加apk的大小,它只有在编译时被用到
  2. 要在指定的目录下建立文件,并且还要把注解处理类,写入其中,这个导致很容易出错

什么是Android-apt

我们知道APT是集成在javac当中的工具,这个Android-apt又是什么鬼呢? 对于从事Android开发的同学来说,ButterKnife这个开源工具可是非常熟悉。在使用该工具之前,你需要进行配置:

compile 'com.jakewharton:butterknife:8.4.0'
apt 'com.jakewharton:butterknife-compiler:8.4.0'

这里的有什么用?上面例子中我们没有配置apt插件照样可以用,这是怎么一回事?
Anroid-apt是用在Android Studio中处理注解处理的插件。它有两方面的作用:

  1. 只允许配置编译时注解处理器依赖,但在最终APK或者Library中不包含注解处理器的代码。
  2. 这个插件可以自动的帮你为生成的代码创建目录,使注解处理器生成的代码能被Android Studio正确的引用,让生成的代码编译到APK里面去(若你的项目依赖于注解处理器模块的源代码,貌似就不用apt插件,也可以引用生成的代码)

对与在一个jar包中的注解处理器(API和处理器)而言,我们不需要进行特殊的配置,它照样可以工作。如果我们需要在项目当中引用注解处理器生成的代码,那么就需要使用Android-apt插件来帮助解决。

对于Butter Knife,EventBus 3.0这类工具,最终我们都需要在自己项目引用注解处理器生成的代码,因此需要为其配置Android-apt。EventBus 3.0的注解处理器还接受两个参数,当然也要借助Android-apt插件了,以EventBus在工程的配置为例:

Paste_Image.png

到现在,我们恍然大雾,原来Android-apt是这么用的啊。知其然更知其所以然啊,现在无论你遇到什么样的配置问题,那都是小菜一碟。

另外,一些注解处理器可以接受外部的参数,在IDEA当中我们可以直接配置,但是基于IDEA而来的Android Studio反而无法直接配置,借助Android-apt插件我们可以实现该功能,其用法如下:

apt{
    arguments{
        配置参数名称 参数值
    }

}

你正在为自己明白了Android-apt的用途的时候,无意间听到了消息:android-apt插件作者近期已经发表声明表示后续不会再继续维护该插件。

噩耗就是来的这么随心所欲啊!!!好不容易懂了,结果发现这玩意不再更新,这是什么梗什么怨啊。

用annotationProcessor代替Android-apt

Gradle从2.2版本开始支持annotationProcessor功能来代替Android-apt。另外,和android-apt只支持javac编译器相比,annotationProcessor同时支持javac和jack编译器。

修改Project的build.gradle配置

android-apt方式

dependencies {
   classpath 'com.android.tools.build:gradle:2.2.3'
   classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}

annotationProcessor方式

dependencies {
   classpath 'com.android.tools.build:gradle:2.2.3'
}

修改module的build.gradle配置

android-apt方式

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}
apply plugin: 'com.neenbedankt.android-apt'
dependencies {
    compile 'org.greenrobot:eventbus:3.0.0'
    apt'org.greenrobot:eventbus-annotation-processor:3.0.1'//apt
}

annotationProcessor方式

保留dependencies 里面的引用并且把apt 换成annotationProcessor就可以了

dependencies {
    compile 'org.greenrobot:eventbus:3.0.0'
    annotationProcessor  'org.greenrobot:eventbus-annotation-processor:3.0.1'
}

不难看出,使用annotationProcessor更为简单。如果你现在的Gradle版本是2.2.X以上,可以考虑替换掉Android-apt了。

参数配置修改

android-apt方式

apt  {
    arguments {
        eventBusIndex "org.greenrobot.eventbusperf.MyEventBusIndex"
    }
}

annotationProcessor方式

defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ eventBusIndex : 'org.greenrobot.eventbusperf.MyEventBusIndex' ]
            }
        }
    }

这里的参数配置是干嘛用的?

我们可以在AbstractProcessor类中的init(ProcessingEnvironment processingEnv)方法里面获取配置

public synchronized void init(ProcessingEnvironment processingEnv) {
    Map<String, String> options = processingEnv.getOptions();
    if (MapUtils.isNotEmpty(options)) {
        moduleName = options.get('eventBusIndex');
    }
}

借助AutoService处理方式

这样就无需再关注 META-INF/services/的创建以注解处理器的注册了

@AutoService(Processor.class)
//对应getSupportedSourceVersion方法
@SupportedSourceVersion(SourceVersion.lastestSupported())
//对应getSupportedAnnotationTypes方法
@SupportedAnnotationTypes({ "cn.trinea.java.test.annotation.MethodInfo" })
public class MethodInfoProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        HashMap<String, String> map = new HashMap<String, String>();
        for (TypeElement te : annotations) {
            for (Element element : env.getElementsAnnotatedWith(te)) {
                MethodInfo methodInfo = element.getAnnotation(MethodInfo.class);
                map.put(element.getEnclosingElement().toString(), methodInfo.author());
            }
        }
        return false;
    }
    
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
    }
}

SupportedAnnotationTypes 表示这个 Processor 要处理的 Annotation 名字。

process中的annotations参数 表示待处理的 Annotations
process中的env参数表示当前或是之前的运行环境

process 函数返回值表示这组annotations是否被这个Processor 接受,如果接受后续子的 processor 不会再对这个Annotations进行处理,简单点说就是当一个方法被2个注解修饰后,若第一个注解处理器的process方法返回了true,那么第二个注解处理器就不会处理该方法了

gradle引用方式:
com.google.auto.service:auto-service:1.0-rc2

这里我踩了个坑,爬了半天没有爬上来......我把"Processor"作为注解处理器的类名,然后总是失败,调试也不能调试

借助javapoet库方面生成代码

MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)
    .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

会生成如下代码---->

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

自己编写注解框架时如何划分项目结构

由于编译时注解处理器只在编译过程中使用,因此我们不希望注解处理器相关的代码在最终的APK中存在,这样能够有效的较少方法数。比如我通常在编写注解Annotation Processor的时候会引用javapoet和Guava,如果将这些代码也打进最终的APK中会造成方法数的暴增,因此建议将注解处理器相关代码单独成为一个模块

另外为了方面注解被其他工程引用,通常我也建议将注解的定义单独划分成一个模块。

综上,我们最终的项目结构如下:

  • xxx/xxx-api:主工程/提供api,Android Library类型
  • xxx-compiler:注解处理器模块,Java Library类型,打包apk时可以不要
  • xxx-annotations:自定义注解,Java Library类型,打包apk时可以不要

xxx/xxx-api依赖xxx-annotations,xxx-compiler依赖xxx-annotations。这点Butter Knife给我们一个非常好的示范:

Paste_Image.png

如何调试注解处理器

对于编译时注解处理器的调试显得略微麻烦些,不同构建方式(Maven,Ant,Gradle等)会稍微有些区别,但也只是形势上的区别,本质上还是离不开JPDA。

编译时注解处理器是运行在一个单独的JVM当中,因此我们想要对它进行调试可以使用Remote Debug。无论是是Eclipse中还是,IDEA当中,对Remote Debug功能都提供了良好的支持,作为IDEA二次开发出的Android Studio同样也不例外。先来看一下如何开启JVM的远程调试功能,在启动JVM的时候加上以下参数即可:

在Android Studio中建立Remote Debugger,操作步骤如下:

这里的端口号要保持一致,不然Remote Debugger是连不上gradle的守护线程的

准备工作完成,下面就可以来调试了(别忘记加断点)。具体怎么做呢?很简单,就是重新编译即可。这里为了方便演示,直接图形化操作:

Paste_Image.png

在构建过程中,Remote Debugger将会触发断点并挂起构建过程,接下来就可以像往常一样调试了.

看不到gif动态图:http://pyonhu.com/2017/02/10/bian-yi-shi-zhu-jie-chu-li-fang/

参考:拓展篇:注解处理器最佳实践

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,567评论 25 707
  • 在基础篇中,我们介绍了什么是注解以及如何开发注解处理器,今天就来说说在开发编译时注解处理器中的那些最佳实践。 什么...
    涅槃1992阅读 1,325评论 3 21
  • 金英翠萼带春寒,黄色花中有几般? 凭君语向游人道,莫作蔓菁花眼看。 --...
    iceYing阅读 338评论 3 0
  • 听了第11次课了,慢慢秋水老师开始讲解知识管理的定义了。说实话,以前自己接触过知识管理的一些知识,但是从来没有系统...
    better小葵阅读 375评论 0 0
  • arguments这个对象主要用来存放函数的参数,但是他还有一个特殊的属性,callee,他是一个指针,指向拥有这...
    issac_宝华阅读 415评论 0 2