安卓使用注解处理器自动生成代码操作详解(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,这时TypeName就上场了,TypeName有多个子类,包括上面的ClassName也是它的子类,每个子类都承担着不同的职责:

  • ArrayTypeName 用于生成数组类,例如Hoverboard []
  • ClassName 获取普通的类,例如Hoverboard
  • Parameterized 获取参数化类,例如List
  • 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 beyond() {
    List 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 set, RoundEnvironment roundEnvironment) {
        return true;
    }


    @Override
    public Set getSupportedAnnotationTypes() {
        Set 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 set是请求处理的类型的集合,RoundEnvironment 是当前或之前的请求处理类型的环境,可以通过它获取当前需要处理请求的元素,例如我需要获取BindView注解的元素的类并获取其中的内容可以这样写:

//先拿到所有使用了BindView注解的元素集合
Set 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 getModifiers();

    Name getSimpleName();

    Element getEnclosingElement();

    List getEnclosedElements();

    boolean equals(Object var1);

    int hashCode();

    List getAnnotationMirrors();

     A getAnnotation(Class var1);

     R accept(ElementVisitor var1, P var2);
}
public interface AnnotatedConstruct {
    List getAnnotationMirrors();

     A getAnnotation(Class var1);

     A[] getAnnotationsByType(Class 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的功能,帮助大家更好的运用和消化这些知识,加油。关注我不迷茫,支持我的就点个赞呗

你可能感兴趣的:(安卓使用注解处理器自动生成代码操作详解(AutoService,JavaPoet,AbstractProcessor))