安卓使用注解处理器自动生成代码操作详解(AutoService,JavaPoet,AbstractProcessor)

关联文章:Android自定义注解

新手村

先来说说注解处理器(AbstractProcessor)是干嘛的,它主要是用来处理注解的一些内部逻辑,拿butterknife举例,我声明了一个bindView注解,那肯定是要写一些逻辑才能找到控件的id对吧,AbstractProcessor就是注解处理的逻辑入口,出于性能考虑,肯定是不能使用反射来处理找id这个逻辑的,这时,JavaPoet就派上用场了,它的作用是根据特定的规则生成java代码文件,这样,我通过注解来拿到需要的参数,通过JavaPoet来生成模板代码,对性能没有任何的影响,由于ServiceLoader加载Processor需要手动注册配置,框架AutoService就是用来自动注册ServiceLoader的,省去了AbstractProcessor繁琐的配置。理解了这三者的关系,下面开始真正的学习吧

副本之JavaPoet的使用

项目地址:https://github.com/square/javapoet
javapoet的api非常的通俗易懂,我用主页的使用示例来说明一下
例如我们要生成一个这样的代码:

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, 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);

MethodSpec类是用来配置方法的,一个方法包括方法名,修饰符,返回值,参数,内容,配置对应的方法已在注释中标出。
TypeSpec为类的配置,类包括类名,修饰符,方法,字段等
JavaFile用于指定输出位置,生成类,我们传入包名,和类,最后通过writeTo指定输出到控制台。
可以看出,复杂的地方就是在MethodSpec的配置,下面着重介绍MethodSpec的一些常用用法

基本用法

MethodSpec main = MethodSpec.methodBuilder("main")
    .addCode(""
        + "int total = 0;\n"
        + "for (int i = 0; i < 10; i++) {\n"
        + "  total += i;\n"
        + "}\n")
    .build();

效果:

void main() {
  int total = 0;
  for (int i = 0; i < 10; i++) {
    total += i;
  }
}

可以看到里面的分号和换行符混在一起看起来眼花缭乱,丢失一个还不会报错,让人很抓狂,因此JavaPoet很贴心的准备了换行符分号和起始结束括号的api:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addStatement("int total = 0") //这段代码之后会添加一个分号和换行符
    .beginControlFlow("for (int i = 0; i < 10; i++)")//这段代码之后会添加一个起始的括号
    .addStatement("total += i")
    .endControlFlow()//括号结束
    .build();

此外还有一个nextControlFlow是前后都加括号,通常用于if else的逻辑判断中,示例:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addStatement("long now = $T.currentTimeMillis()", System.class)
    .beginControlFlow("if ($T.currentTimeMillis() < now)", System.class)
    .addStatement("$T.out.println($S)", System.class, "Time travelling, woo hoo!")
    .nextControlFlow("else if ($T.currentTimeMillis() == now)", System.class)
    .addStatement("$T.out.println($S)", System.class, "Time stood still!")
    .nextControlFlow("else")
    .addStatement("$T.out.println($S)", System.class, "Ok, time still moving forward")
    .endControlFlow()
    .build();

输出:

void main() {
  long now = System.currentTimeMillis();
  if (System.currentTimeMillis() < now)  {
    System.out.println("Time travelling, woo hoo!");
  } else if (System.currentTimeMillis() == now) {
    System.out.println("Time stood still!");
  } else {
    System.out.println("Ok, time still moving forward");
  }
}

可以看到上面有几个不明觉厉的符号,我们称之为占位符,占位符常用的有以下几种:

  • $T 类占位符,用于替换代码中的类
  • $L 姑且叫它变量占位符吧,用法和String.format中的%s差不多,按照顺序依次替换里面的变量值
  • $S 字符串占位符,当我们需要在代码中使用字符串时,用这个替换
  • $N 名称占位符,比方说需要在一个方法里使用另一个方法,可以用这个替换

$L演示示例,后面的变量按照顺序对号入座:

private MethodSpec computeRange(String name, int from, int to, String op) {
  return MethodSpec.methodBuilder(name)
      .returns(int.class)
      .addStatement("int result = 0")
      .beginControlFlow("for (int i = $L; i < $L; i++)", from, to)
      .addStatement("result = result $L i", op)
      .endControlFlow()
      .addStatement("return result")
      .build();
}

$N

MethodSpec hexDigit = MethodSpec.methodBuilder("hexDigit")
    .addParameter(int.class, "i")
    .returns(char.class)
    .addStatement("return (char) (i < 10 ? i + '0' : i - 10 + 'a')")
    .build();

MethodSpec byteToHex = MethodSpec.methodBuilder("byteToHex")
    .addParameter(int.class, "b")
    .returns(String.class)
    .addStatement("char[] result = new char[2]")
    .addStatement("result[0] = $N((b >>> 4) & 0xf)", hexDigit)
    .addStatement("result[1] = $N(b & 0xf)", hexDigit)
    .addStatement("return new String(result)")
    .build();

输出:
public String byteToHex(int b) {
  char[] result = new char[2];
  result[0] = hexDigit((b >>> 4) & 0xf);
  result[1] = hexDigit(b & 0xf);
  return new String(result);
}

public char hexDigit(int i) {
  return (char) (i < 10 ? i + '0' : i - 10 + 'a');
}

其余两个前面的示例中已经使用过了,T传入类,S传入字符串,注意顺序:

 addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")

类的获取与使用
java中的源码类我们在使用的时候都会自动导入,但是我们自定义的类是不会的,所以我们需要使用ClassName来获取我们想要的类

ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
MethodSpec today = MethodSpec.methodBuilder("tomorrow")
    .returns(hoverboard)
    .addStatement("return new $T()", hoverboard)
    .build();

输出:
package com.example.helloworld;
import com.mattel.Hoverboard;

public final class HelloWorld {
  Hoverboard tomorrow() {
    return new Hoverboard();
  }
}

传入包名前半段和后半段的类名就能获取到我们想要的类了,但是参数化类型我们要怎么定义呢,比如List<Hoverboard>,这时TypeName就上场了,TypeName有多个子类,包括上面的ClassName也是它的子类,每个子类都承担着不同的职责:

  • ArrayTypeName 用于生成数组类,例如Hoverboard []
  • ClassName 获取普通的类,例如Hoverboard
  • Parameterized 获取参数化类,例如List<Hoverboard>
  • TypeVariableName 获取类型变量,例如泛型T
  • WildcardTypeName 获取通配符,例如? extends Hoverboard
    它们的用法差不多,以Parameterized举例:
ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
ClassName list = ClassName.get("java.util", "List");
ClassName arrayList = ClassName.get("java.util", "ArrayList");
TypeName listOfHoverboards = ParameterizedTypeName.get(list, hoverboard);

MethodSpec beyond = MethodSpec.methodBuilder("beyond")
    .returns(listOfHoverboards)
    .addStatement("$T result = new $T<>()", listOfHoverboards, arrayList)
    .addStatement("result.add(new $T())", hoverboard)
    .addStatement("result.add(new $T())", hoverboard)
    .addStatement("result.add(new $T())", hoverboard)
    .addStatement("return result")
    .build();

输出:
package com.example.helloworld;

import com.mattel.Hoverboard;
import java.util.ArrayList;
import java.util.List;

public final class HelloWorld {
  List<Hoverboard> beyond() {
    List<Hoverboard> result = new ArrayList<>();
    result.add(new Hoverboard());
    result.add(new Hoverboard());
    result.add(new Hoverboard());
    return result;
  }
}

其他的例如字段(FieldSpec),注解(AnnotationSpec),参数(ParameterSpec)等api用起来都大同小异,由于篇幅有限,javaPoet的介绍就讲到这里,如果还有不太明白的地方或有想进一步了解的可以参考这个比较全面的介绍:JavaPoet使用详解

副本之AutoService的使用

项目地址:https://github.com/google/auto
使用非常的简单:

package foo.bar;

import javax.annotation.processing.Processor;

@AutoService(Processor.class)
final class MyProcessor implements Processor {
  // …
}

编译后,则会在META-INF文件夹下生成Processor配置信息文件,而当外部程序装配这个模块的时候,
就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

副本之AbstractProcessor的使用

AbstractProcessor继承自Processor,是一个抽象处理器,它的作用是在编译时扫描注解并处理一些逻辑,例如生成代码等,一般我们继承它需要实现4个方法:

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

    //文件相关辅助类
    private Filer mFiler;
    //元素
    private Elements elements;
    //日志信息
    private Messager messager;

    /**
     * 入口,相当于java的main入口
     *
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        elements = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return true;
    }


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

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

注解类:

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

init方法是一个入口,ProcessingEnvironment类主要提供了一些工具类给我们使用,我们可以在init方法中获取我们需要的工具类。
getSupportedAnnotationTypes用于获取我们自定义的注解,写法可以固定
getSupportedSourceVersion用于获取java版本,写法可以固定
process方法是我们处理逻辑的核心方法,返回true,代表注解已申明,并要求Processor后期不用再处理了它们

参数Set<? extends TypeElement> set是请求处理的类型的集合,RoundEnvironment 是当前或之前的请求处理类型的环境,可以通过它获取当前需要处理请求的元素,例如我需要获取BindView注解的元素的类并获取其中的内容可以这样写:

//先拿到所有使用了BindView注解的元素集合
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);
 for (Element element:elementsAnnotatedWith){
          //从元素中拿到这个注解实例
            BindView annotation = element.getAnnotation(BindView.class);
          //从这个注解实例中获取到注解中包含的值
            int value = annotation.value();
  }

这样我们就获取到了注解中的值,思考下butterknife中bindView注解中的那个id的获取,是不是有点豁然开朗了呢。
我们获取信息都是基于Element这个类展开来,所以了解下这个类很有必要,Element表示一个程序元素,比如包、类或者方法,主要包括以下几种方法:

  public interface Element extends AnnotatedConstruct {
    TypeMirror asType();

    ElementKind getKind();

    Set<Modifier> getModifiers();

    Name getSimpleName();

    Element getEnclosingElement();

    List<? extends Element> getEnclosedElements();

    boolean equals(Object var1);

    int hashCode();

    List<? extends AnnotationMirror> getAnnotationMirrors();

    <A extends Annotation> A getAnnotation(Class<A> var1);

    <R, P> R accept(ElementVisitor<R, P> var1, P var2);
}
public interface AnnotatedConstruct {
    List<? extends AnnotationMirror> getAnnotationMirrors();

    <A extends Annotation> A getAnnotation(Class<A> var1);

    <A extends Annotation> A[] getAnnotationsByType(Class<A> var1);
}
  • asType
    获取元素的类型信息,包括包名,类名等,配合javapoet的ClassName可以直接获取到该TypeName
    TypeName typeName = ClassName.get(element.asType());
  • getKind 用于判断是哪种element
  • getModifiers 用于获取元素的关键字public static等
  • getEnclosingElement 返回包含该element的父element
  • getAnnotation 获取元素上的注解
  • accept是一个判断方法,用于判断如果是某一个元素就执行某一个方法,用的很少,不细讲了

可能会遇到的问题

加入AutoService发现配置都正确,但就是不能生成代码,原因可能是你的Gradle过高,把版本降到4.10.1或以下就可以了,原因不详,如果有知道原因的朋友可以在留言区说一下
另外一个就是你的注解器的lib包需要使用annotationProcessor来使用,而不是implementation

文章到这就要到我们的实战环节了,下一篇我将带领大家仿butterknife简单实现一个findviewbyid的功能,帮助大家更好的运用和消化这些知识,加油。关注我不迷茫,支持我的就点个赞呗

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

推荐阅读更多精彩内容