自定义注解框架

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

在我们平时的Android开发中,常会用到一个框架ButterKnife来代替我们平时的findViewById(),先在根目录下的build中导入

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' 

然后在具体的项目目录下的build.gradle中导入依赖:

implementation  'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

最后在我们的Activity中使用该框架,使用方式如下:

@BindView(R.id.textview)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   ButterKnife.bind(this);
}

如上面所示,使用该方式可以方便的起到代替findViewById的效果,但是ButterKnife框架的功能远远不止于此,比如Resource注入,单事件注入,多事件回调等等。当然今天我们主要讲如何自己实现类似butterknife的findViewById的效果。

实现思路

ButterKnife的原理是通过注解来实现的,主要步骤可分为以下四步:

1.定义注解,编译时注解的使用方式

2.注解处理器处理注解。

3.在解析注解的时候生成Java文件(JavaPoet)

4.引入

上面是大致的步骤,具体的过程可以解释为先自定义注解,通过编译时注解来实现,因为编译时注解可以采用APT来解析,通过AbstractProcess来自定义注解处理逻辑。如果采用运行时注解,则采用反射来获取。同时如果使用运行时注解的话,在运行时反射调用效率也是个问题。同时我们还需要向编译器注册注解处理器,需要在META-INFO目录下手动注册,一般是通过依赖Google的AutoService库来解决。通过APT解析时我们可以使用JavaPoet来生成我们需要的findViewById的功能文件。生成文件后,再把该文件引入我们的页面中进行Bind(Activity,Fragment,Adapter等),比如ButterKnife.bind(this)。

为了实现上面的效果,我们先介绍一下注解,APT,AutoService和JavaPoet等内容。其实实现的思路并不难,问题在于把这些东西串起来,考虑细节问题和熟练使用。

注解

注解是代码里特殊标记,可以在编译,类加载,运行时被读取,并执行相应的处理。我们可以在不改变元原有的逻辑之下,在源文件中嵌入一些补充的信息。注解的图解如下:

自定义注解框架

简单介绍一下上面的图片内容,元注解可以理解为注解上的注解,有五个,我们主要介绍Retention和Target和Repetable。

Rentention用于定义Annotation的生命周期

  • RetentionPoicy.SOURCE

    注解只保留在源文件中,当 Java 文件被编译成 class 文件的时候,注解被遗弃。常用于做一些检查性的操作,比如 @Override、@SuppressWarnings 。

  • RetentionPoicy.CLASS

    注解被保留在 class 文件,但 JVM 加载 class 文件时被遗弃,这是默认的生命周期。常用于在编译时进行一些预处理,比如生成一些辅助代码,例如 ButterKnife。

  • RetentionPoicy.RUNTIME

    注解不仅被保存到 class 文件中,JVM 加载 class 文件之后,仍然存在。常用于在运行时动态获取注解信息,大多会与反射一起使用。

Target定义了 Annotation 所修饰的对象结构。

  • ElementType.CONSTRUCTOR 用于描述构造器

  • ElementType.FIELD 用户描述属性

  • ElementType.LOCAL_VARIABLE 用于描述局部变量

  • ElementType.METHOD 用于描述方法

  • ElementType.PACKAGE 用于描述包

  • ElementType.TYPE 用于描述类、接口或枚举

如果未标注,则表示可修饰所有。

@Repeatable

JDK1.8新加的,表明当自定义注解的属性值有多个时,自定义注解可以多次使用。

自定义注解

自定义注解可以根据有无成员变量分为标记型注解元数据注解。定义了注解之后还要解析注解。解析注解也可以分为两种,它是根据Retentaion来划分的,如果是编译时注解,可以采用APT来解析,则采用反射来获取。

运行时注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD})
public @interface BiaoJi {
   String value();
}

通过反射处理该注解:

@BiaoJi("class focus")
public class AnnotationTest2 {

   @BiaoJi("field foucs")
   private int count;

   @BiaoJi("method foucs")
   public int getCount(){
       return 100;
  }

   public static void main(String[] args) throws NoSuchMethodException, NoSuchFieldException {
       if (AnnotationTest2.class.isAnnotationPresent(BiaoJi.class)){
           BiaoJi biaoJi = AnnotationTest2.class.getAnnotation(BiaoJi.class);
               System.out.println(biaoJi.value());
      }
       //解析成员变量的注解
       BiaoJi methodBiaoJi = AnnotationTest2.class.getMethod("getCount").getAnnotation(BiaoJi.class);
       System.out.println(methodBiaoJi.value());
       //解析方法上的注解
       Field field = AnnotationTest2.class.getDeclaredField("count");
       field.setAccessible(true);
       BiaoJi fieldBiaoJi = field.getAnnotation(BiaoJi.class);
       System.out.println(fieldBiaoJi.value());


  }

这边涉及到的就是反射的知识,反射解析注解主要有一下三个方法:

  1. boolean isAnnotationPresent(Class<? extends Annotation> annotation)

    判断是否使用了某个注解。

  2. <A extends Annotation> A getAnnotation(Class<A> annotation)

    获得指定的注解元素。

  3. Annotation[] getAnnotations()

    返回对应元素上的所有注解。

编译时注解

编译时注解的解析通过APT来实现,通过继承AbstractProcress来自定义注解处理逻辑,同时我们需要向编译器注册注解处理器,需要在META-INFO目录下手动注册,一般是通过依赖Google的AutoService库来解决。

对于APT,我们先通过如下的思维导入了解一下:

自定义注解框架

APT即注解处理器,它有三个主要用途:

1.定义编译规则,并检查被编译的源文件

2.修改已有的源代码(涉及Java编译器的内部API,可能会存在兼容问题)

3.生成新的源代码


Java 源代码的编译流程分为三个步骤:

  1. 将源文件解析成抽象语法树

  2. 调用已注册的注解处理器

  3. 生成字节码

如果第二步调用注解处理器过程中生成了新的源文件,那么编译器将重复第一二步骤,解析并处理新生成的源文件。

所以可以这样理解,我们写的自定义注解处理器是给编译器写的,让它按照我们的逻辑来处理注解,所以也得向编译器注册注解处理器。

APT即注解处理器,Java API已经提供了扫描源码并解析注解的框架,我们可以通过AbstractProcessor类来实现自己的注解处理逻辑。APT的原理是在注解了某些代码元素(字段,函数,类等等)后,在编译期编译器会检查AbstractProcessor的子类,并且自动调用其process()方法,然后将添加了指定注解的所有代码元素作为参数传递给该方法,开发者在根据注解元素再编译期输出对应的Java代码。

所有的注解处理器都需要实现Processor接口

public interface Processor {

 void init(ProcessingEnvironment processingEnv);
 
 Set<String> getSupportedAnnotationTypes();
 
 SourceVersion getSupportedSourceVersion();
 
 boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);
 
...
}

它有四个重要方法,其中 init 方法用于存放注解处理器的初始化代码,之所以不用构造器,是因为在 Java 编译器中,注解处理器的实例是通过反射 API 生成的,也正是因为使用反射 API,每个注解处理器类都需要定义一个无参构造器。

通常来说,当编写注解处理器时,我们不声明任何构造器,并依赖于 Java 编译器,而具体的初始化代码,则放入 init 方法之中。

而剩下的三个方法中,getSupportedAnnotationTypes 方法将返回注解处理器所支持的注解类型,这些注解类型只需要用字符串形式表示即可。

getSupportedSourceVersion 方法将返回该处理器所支持的 Java 版本,通常直接返回 SourceVersion.latestSupported(),而 process 方法则是最为关键的注解处理方法。

process 方法接收两个参数,分别代表该注解处理器所能处理的注解类型,以及囊括当前轮生成的抽象语法树的 RoundEnvironment。

通常我们这样使用 RoundEnvironment:

for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
//todo
}

这个 Element 表示一个程序元素,可以是包、类、或者方法,所有通过注解取得的元素都将以 Element 类型处理,准确来说是 Element 对象的子类处理。

Element 的子类:

  • ExecutableElement

    表示某个类或接口的方法、构造方法或初始化程序,包括注释类型元素。

    对应注解是 ElementType.METHOD 和 ElementType.CONSTRUCTOR。

  • PackageElement

    表示一个包程序元素,提供对有关包及其成员的信息访问。

    对应注解是 ElementType.PACKAGE。

  • TypeElement

    表示一个类或接口程序元素,提供对有关类型及其成员的信息访问。

    对应注解是 ElementType.TYPE。

    注意:枚举类型是一种类,而注解类型是一种接口。

  • TypeParameterElement

    表示类、接口、方法元素的类型参数。

    对应注解是 ElementType.PARAMETER。

  • VariableElement

    表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。

    对应注解是 ElementType.FIELD 和 ElementType.LOCAL_VARIABLE。

不同类型的Element的信息获取方式不同

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

   private Elements mElementsUtils;

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

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

       //解析类上的注解
       for (Element element : roundEnvironment.getElementsAnnotatedWith(Info.class)) {
           TypeElement classElement = (TypeElement) element;
           PackageElement packageElement = (PackageElement) element.getEnclosingElement();
           //全类名
           System.out.println(classElement.getQualifiedName().toString());
           //类名
           System.out.println(classElement.getSimpleName().toString());
           //包名
           System.out.println(packageElement.getQualifiedName().toString());
           //父类名
           System.out.println(classElement.getSuperclass().toString());
      }

       //解析方法上的注解
       for (Element element : roundEnvironment.getElementsAnnotatedWith(Info.class)) {
           ExecutableElement executableElement = (ExecutableElement) element;
           TypeElement classElement = (TypeElement) executableElement.getEnclosingElement();
           PackageElement packageElement = mElementsUtils.getPackageOf(classElement);
           //全类名
           String fullClassName = classElement.getQualifiedName().toString();
           //与上面一致
           //...
           //方法名
           String methodName = executableElement.getSimpleName().toString();

           //方法参数列表
           List<? extends VariableElement> methodParameters = executableElement.getParameters();
           List<String> types = new ArrayList<>();
           for (VariableElement variableElement : methodParameters) {
               TypeMirror methodParameterType = variableElement.asType();
               if (methodParameterType != null) {
                   TypeVariable typeVariable = (TypeVariable) methodParameterType;
                   methodParameterType = typeVariable.getUpperBound();
              }
               //参数名
               String parameterName = variableElement.getSimpleName().toString();
               //参数类型
               String parameteKind = methodParameterType.toString();
               types.add(methodParameterType.toString());
          }
      }

       //解析属性上的注解
       for (Element element : roundEnvironment.getElementsAnnotatedWith(Info.class)) {
           VariableElement variableElement = (VariableElement) element;
           TypeElement classElement = (TypeElement) element.getEnclosingElement();
           PackageElement packageElement = mElementsUtils.getPackageOf(classElement);
           //类名
           String className = classElement.getSimpleName().toString();
           //与上面一致
           //...

           //类成员类型
           TypeMirror typeMirror = variableElement.asType();
           String type = typeMirror.toString();
      }
       return true;
  }
}
AbstractProcessor

实现一个注解处理器,需要继承 AbstractProcessor ,如下:

public class BindViewProcessor extends AbstractProcessor {

  private Elements mElementsUtils;
  private Types mTypesUtils;
  private Filter mFilter;
  private Messager mMessager;

  /**
    * 初始化方法
    * 可以初始化一些给注解处理器使用的工具类
    */
  @Override
  public synchronized void init(ProcessingEnvironment processingEnvironment) {
      super.init(processingEnvironment);
      mElementsUtils = processingEnvironment.getElementUtils();
  }

  /**
    * 指定目标注解对象
    */
  @Override
  public Set<String> getSupportedAnnotationTypes() {
      Set<String> hashSet = new HashSet<>();
      hashSet.add(BindView.class.getCanonicalName());
      return hashSet;
  }

  /**
    * 指定使用的 Java 版本
    */
  @Override
  public SourceVersion getSupportedSourceVersion() {
      return SourceVersion.latestSupported();
  }

  /**
    * 处理注解
    */
  @Override
  public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//...
      return true;
  }
}

对于 APT,其实主要是有很多 API 不熟悉。

Elements:用于处理程序元素的工具类;

Types:用于处理类型数据的工具类;

Filter:用于给注解处理器创建文件;

Messager:用于给注解处理器报告错误、警告、提示等信息。

AutoServcie 注册注解处理器

以前要注册注解处理器要在 module 的 META_INFO 目录新建 services 目录,并创建一个名为 Java.annotation.processing.Processor 的文件,然后在文件中写入要注册的注解处理器的全民。

后来 Google 推出了 AutoService 注解库来实现注册注解处理器的注册,通过在注解处理器上加上 @AutoService(Processor.class) 注解,即可在编译时生成 META_INFO 信息。

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
}

JavaPoet 生成 Java 代码

JavaPoet 中有几个常用的类:

MethodSpec:代表一个构造方法或方法声明;

TypeSpec:代表一个类、接口、或者枚举声明;

FieldSpec:代表一个成员变量、字段声明;

JavaFile:包含一个顶级类的 Java 文件;

关于它的使用,直接看官方文档即可:

https://github.com/square/javapoet

ButterKnife 的实现

分为四步:

  1. 定义注解

  2. 注解处理器处理注解

  3. 生成 Java 文件

  4. 引入

定义注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
  int value();
}
注解处理器处理注解、生成 Java 文件
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {

  private Elements mElementsUtils;
  private Types mTypesUtils;
  private Filter mFilter;
  private Messager mMessager;

  /**
    * 初始化方法
    * 可以初始化一些给注解处理器使用的工具类
    */
  @Override
  public synchronized void init(ProcessingEnvironment processingEnvironment) {
      super.init(processingEnvironment);
      mElementsUtils = processingEnvironment.getElementUtils();
  }

  /**
    * 指定目标注解对象
    */
  @Override
  public Set<String> getSupportedAnnotationTypes() {
      Set<String> hashSet = new HashSet<>();
      hashSet.add(BindView.class.getCanonicalName());
      return hashSet;
  }

  /**
    * 指定使用的 Java 版本
    */
  @Override
  public SourceVersion getSupportedSourceVersion() {
      return SourceVersion.latestSupported();
  }

  /**
    * 处理注解
    */
  @Override
  public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
      //获取所有包含 BindView 注解的元素
      Set<? extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(BindView.class);
      Map<TypeElement, Map<Integer, VariableElement>> typeElementMapHashMap = new HashMap<>();
      for (Element element : elementSet) {
          //因为 BindView 的作用对象是 FIELD,因此 element 可以直接转化为 VariableElement
          VariableElement variableElement = (VariableElement) element;
          //getEnclosingElement 方法返回封装此 Element 的最里层元素
          //如果 Element 直接封装在另一个元素的声明中,则返回该封装元素
          //此处表示的即是 Activity 类对象
          TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
          Map<Integer, VariableElement> variableElementMap = typeElementMapHashMap.get(typeElement);
          if (variableElementMap == null) {
              variableElementMap = new HashMap<>();
              typeElementMapHashMap.put(typeElement, variableElementMap);
          }
          //获取注解的值,即 ViewId
          BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
          int viewId = bindAnnotation.value();
          variableElementMap.put(viewId, variableElement);
      }
      for (TypeElement key : typeElementMapHashMap.keySet()) {
          Map<Integer, VariableElement> elementMap = typeElementMapHashMap.get(key);
          String packageName = ElementUtil.getPackageName(mElementsUtils, key);
          JavaFile javaFile = JavaFile.builder(packageName, generateCodeByPoet(key, elementMap)).build();
          try {
              javaFile.writeTo(processingEnv.getFiler());
          } catch (IOException e) {
              e.printStackTrace();
          }
      }
      return true;
  }

  /**
    * 生成 Java 类
    *
    * @param typeElement       注解对象的上层元素对象,即 Activity 对象
    * @param variableElementMap Activity 包含的注解对象以及注解的目标对象
    * @return
    */
  private TypeSpec generateCodeByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) {
      //自动生成的文件以 Activity 名 + ViewBinding 进行命名
      return TypeSpec.classBuilder(ElementUtil.getEnclosingClassName(typeElement) + "ViewBinding")
              .addModifiers(Modifier.PUBLIC)
              .addMethod(generateMethodByPoet(typeElement, variableElementMap))
              .build();
  }

  /**
    * 生成方法
    *
    * @param typeElement       注解对象上层元素对象,即 Activity 对象
    * @param variableElementMap Activity 包含的注解对象以及注解的目标对象
    * @return
    */
  private MethodSpec generateMethodByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) {
      ClassName className = ClassName.bestGuess(typeElement.getQualifiedName().toString());
      //方法参数名
      String parameter = "_" + StringUtil.toLowerCaseFirstChar(className.simpleName());
      MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
              .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
              .returns(void.class)
              .addParameter(className, parameter);
      for (int viewId : variableElementMap.keySet()) {
          VariableElement element = variableElementMap.get(viewId);
          //被注解的字段名
          String name = element.getSimpleName().toString();
          //被注解的字段的对象类型的全名称
          String type = element.asType().toString();
          String text = "{0}.{1}=({2})({3}.findViewById({4}));n";
          methodBuilder.addCode(MessageFormat.format(text, parameter, name, type, parameter, String.valueOf(viewId)));
      }
      return methodBuilder.build();
  }
}
引入
public class ButterKnife {
   public static void bind(Activity activity) {
       Class clazz = activity.getClass();
       try {
           Class bindViewClass = Class.forName(clazz.getName() + "ViewBinding");
           Method method = bindViewClass.getMethod("bind", clazz);
           method.invoke(null, activity);
      } catch (Exception e) {
           e.printStackTrace();
      }
  }
}

实现过程遇到的问题:注解和注解处理器需分开写在两个不同的Library,注册注解处理器是通过annotationProcessor,而不是implementation。

annotationProcessor project(':apt_processor')
implementation project(path: ':apt_annotations')

参考:

Android中常用注解:https://www.flysnow.org/2015/08/13/android-tech-docs-support-annotations.html

Github:https://github.com/Omooo


写在最后

特别感谢Omooo对我的帮助,指导我解决了不少问题,是我Android 学习道路上的良师益友。

原文始发于微信公众号(九局下半大逆转):自定义注解框架