注解框架

398 阅读7分钟

在日常开发中我们会经常使用注解(Annotation),很多开源框架中都提供了注解功能,例如:fastjson的 @JSONField,Gson的@SerializedName,ButterKnife的@BindView等,但对于注解的原理我们可能不怎么了解,或者对于如何自定义注解以及写注解处理器(APT)了解不多,下面我们一起重新认识下注解。这样在使用框架中的注解功能时,也能明白其背后的原理,做到心中有数,或者在学习框架源码时能更深入理解注解的用法和好处。

注解(Annotations)

Java注解是JDK5引入的新特性,注解也称为元数据,可以用在类、方法、变量、参数和包上提供标注功能,同时对它们注解的代码的执行没有直接影响。

注解有多种用途:

  • 为编译器提供信息:编译器可以使用注解来检测错误和警告信息,如@Override、@Deprecated
  • 编译或部署时处理(Class): 可以生成代码、XML、文件等,如@param、@return用于生成javadoc文档
  • 运行时处理(Runtime):注解可以在运行时检查、打印日志、反射

Annotation架构

Java内置的注解

java内置的注解一共7个,3个在java.lang中,剩余4个在java.lang.annotation中。

作用在代码的注解:

  • @Override- 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告。

元注解

作用在注解上的注解(元注解):

  • @Retention - 指定注解的存储方式,我们由枚举RetentionPolicy.java来指定。

  • @Documented - 标记这些注解是否包含在javadoc文档中。

  • @Target - 指定注解可以使用的范围,由枚举ElmentType来指定使用范围。

  • @Inherited - 注解类型可以从超类继承。

     public enum RetentionPolicy {
              SOURCE// 标记的注解仅保留在源级别中,并被编译器忽略。    
              CLASS// 标记的注解在编译时由编译器保留,但 Java 虚拟机(JVM)会	忽略。    
              RUNTIME // 标记的注解由 JVM 保留,因此运行时环境可以使用它。
      }
      
      //@Target指定注解使用范围,为ElementType[]  
     public enum ElementType {    
      	TYPE// 类    
     	    FIELD// 字段或属性    
          METHOD// 方法    
          PARAMETER// 参数    
          CONSTRUCTOR// 构造方法    
          LOCAL_VARIABLE// 局部变量    
          ANNOTATION_TYPE// 也可以使用在注解上    
          PACKAGE// 包    
          TYPE_PARAMETER// 类型参数    
          TYPE_USE // 任何类型
      }
    

注解是应用场景

根据 @Retention 元注解定义的存储方式,注解一般可以使用在以下3种场景中,如:

  • 源码(Source): APT 在编译期能获取注解与注解声明的类和类中所有成员信息,一般用于生成额外的辅助类。
  • 字节码(Class): 字节码增强 在编译出Class后,通过修改Class数据以实现修改代码逻辑目的。
  • 运行时(Runtime): 反射 在程序运行时,通过反射技术动态获取注解与其元素。

自定义注解

1.使用@interface定义注解

使用 @interface 定义注解时,意味着它实现了 java.lang.annotation.Annotation 接口,即该注解就是一个Annotation。

定义 Annotation 时,@interface 是必须的。 注意:它和我们通常的 implemented 实现接口的方法不同。Annotation 接口的实现细节都由编译器完成。通过 @interface 定义注解后,该注解不能继承其他的注解或接口。

2.通过@Retention指定注解存储方式

定义该注解的生命周期(有效范围)。可选的参数值在枚举类型RetentionPolicy中包括

SOURCE:注解只存在于Java源代码中,编译生成的字节码文件中就不存在了。
CLASS:注解存在于Java源代码、编译以后的字节码文件中,运行的时候内存中没有,默认值。
RUNTIME:注解存在于Java源代码中、编译以后的字节码文件中、运行时内存中,程序可以通过反射获取该注解。

3.通过@Target指定直接使用范围

指明此注解用在哪个位置,如果不写默认是任何地方都可以使用。 可选的参数值在枚举类ElemenetType中包括:

  TYPE,               /* 类、接口(包括注释类型)或枚举声明  */
  FIELD,              /* 字段声明(包括枚举常量)  */
  METHOD,             /* 方法声明  */
  PARAMETER,          /* 参数声明  */
  CONSTRUCTOR,        /* 构造方法声明  */
  LOCAL_VARIABLE,     /* 局部变量声明  */
  ANNOTATION_TYPE,    /* 注释类型声明  */
  PACKAGE             /* 包声明  */

4.注解中定义参数

注解的属性也称为成员变量,注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。 注解内的可使用的数据类型是有限制的,类型如下:

所有的基本类型(intfloatboolean 等)
String
Class
enum@Retention 中属性的类型为枚举)
Annotation
以上类型的数组(@Target 中属性类型为枚举类型的数组)

编译器对属性的默认值也有约束。首先,属性不能有不确定的的值。也就是说,属性要么具有默认值,要么在使用注解时提供属性的值。对于非基本类型的属性,无论是在源代码中声明时,或是在注解接口中定义默认值时,都不能使用 null 为其值。因此,为了绕开这个约束,我们需要自己定义一些特殊的值,例如空字符串或负数,来表示某个属性不存在。

APT注解处理器

APT(Annotation Processor Tools) 注解处理器,用于处理注解,编写好的 Java 文件,需要经过 Javac 的编译,编译为虚拟机能够加载的字节码(Class)文件,注解处理器是 Javac 自带的一个工具,用来在编译时期处理注解信息;不管是运行时注解还是编译时注解,都会通过处理器在编译时进行扫描和处理注解。 注解处理器将标记了注解的类,变量等作为输入内容,经过注解处理器处理,生成想要生成的java代码。

自定义注解处理器

####1.创建注解相关Module 需要创建两个Module:

  • 1个注解定义的java-library
  • 1个注解处理的java-library

自定义注解

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

####2.继承AbstractProcessor,自定义注解处理

@SupportedAnnotationTypes({"com.xiaomi.shop.annotation.BindView"})
@AutoService(Processor.class)
public class SimpleButterKnifeProcessor extends AbstractProcessor {

    public SimpleButterKnifeProcessor() {}

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

  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      Map<TypeElement, SimpleButterKnifeProcessor.BindingSet> bindingMap = new LinkedHashMap();
      Iterator var4 = roundEnv.getElementsAnnotatedWith(BindView.class).iterator();

      TypeElement typeElement;
      while(var4.hasNext()) {
          Element element = (Element)var4.next();
          typeElement = (TypeElement)element.getEnclosingElement();
          int id = ((BindView)element.getAnnotation(BindView.class)).value();
          Name simpleName = element.getSimpleName();
          String name = simpleName.toString();
          TypeMirror elementType = element.asType();
          TypeName type = TypeName.get(elementType);
          SimpleButterKnifeProcessor.BindingSet bindingSet = (SimpleButterKnifeProcessor.BindingSet)bindingMap.get(typeElement);
          if (bindingSet == null) {
              bindingSet = new SimpleButterKnifeProcessor.BindingSet();
              TypeMirror typeMirror = typeElement.asType();
              TypeName targetType = TypeName.get(typeMirror);
              String packageName = MoreElements.getPackage(typeElement).getQualifiedName().toString();
              String className = typeElement.getQualifiedName().toString().substring(packageName.length() + 1).replace('.', '$');
              ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding", new String[0]);
              bindingSet.targetTypeName = targetType;
              bindingSet.bindingClassName = bindingClassName;
              bindingMap.put(typeElement, bindingSet);
          }

          if (bindingSet.viewBindings == null) {
              bindingSet.viewBindings = new ArrayList();
          }

          SimpleButterKnifeProcessor.ViewBinding viewBinding = new SimpleButterKnifeProcessor.ViewBinding();
          viewBinding.type = type;
          viewBinding.id = id;
          viewBinding.name = name;
          bindingSet.viewBindings.add(viewBinding);
      }

      var4 = bindingMap.entrySet().iterator();

      while(var4.hasNext()) {
          Entry<TypeElement, SimpleButterKnifeProcessor.BindingSet> entry = (Entry)var4.next();
          typeElement = (TypeElement)entry.getKey();
          SimpleButterKnifeProcessor.BindingSet binding = (SimpleButterKnifeProcessor.BindingSet)entry.getValue();
          TypeName targetTypeName = binding.targetTypeName;
          ClassName bindingClassName = binding.bindingClassName;
          List<SimpleButterKnifeProcessor.ViewBinding> viewBindings = binding.viewBindings;
          Builder viewBindingBuilder = TypeSpec.classBuilder(bindingClassName.simpleName()).addModifiers(new Modifier[]{Modifier.PUBLIC});
          viewBindingBuilder.addField(targetTypeName, "target", new Modifier[]{Modifier.PUBLIC});
          com.squareup.javapoet.MethodSpec.Builder activityViewBuilder = MethodSpec.constructorBuilder().addModifiers(new Modifier[]{Modifier.PUBLIC}).addParameter(targetTypeName, "target", new Modifier[0]);
          activityViewBuilder.addStatement("this(target, target.getWindow().getDecorView())", new Object[0]);
          viewBindingBuilder.addMethod(activityViewBuilder.build());
          com.squareup.javapoet.MethodSpec.Builder viewBuilder = MethodSpec.constructorBuilder().addModifiers(new Modifier[]{Modifier.PUBLIC}).addParameter(targetTypeName, "target", new Modifier[0]).addParameter(ClassName.get("android.view", "View", new String[0]), "source", new Modifier[0]);
          viewBuilder.addStatement("this.target = target", new Object[0]);
          viewBuilder.addCode("\n", new Object[0]);
          Iterator var28 = viewBindings.iterator();

          while(var28.hasNext()) {
              SimpleButterKnifeProcessor.ViewBinding viewBinding = (SimpleButterKnifeProcessor.ViewBinding)var28.next();
              com.squareup.javapoet.CodeBlock.Builder builder = CodeBlock.builder().add("target.$L = ", new Object[]{viewBinding.name});
              builder.add("($T) ", new Object[]{viewBinding.type});
              builder.add("source.findViewById($L)", new Object[]{CodeBlock.of("$L", new Object[]{viewBinding.id})});
              viewBuilder.addStatement("$L", new Object[]{builder.build()});
          }

          viewBindingBuilder.addMethod(viewBuilder.build());
          JavaFile javaFile = JavaFile.builder(bindingClassName.packageName(), viewBindingBuilder.build()).build();

          try {
              javaFile.writeTo(this.processingEnv.getFiler());
          } catch (IOException var18) {
              this.processingEnv.getMessager().printMessage(Kind.ERROR, var18.getMessage());
          }
      }

      return true;
  }

  class ViewBinding {
      TypeName type;
      int id;
      String name;

      ViewBinding() {
      }
  }

  class BindingSet {
      TypeName targetTypeName;
      ClassName bindingClassName;
      List<SimpleButterKnifeProcessor.ViewBinding> viewBindings;

      BindingSet() {
      }
  }
}

####3.注册处理器

方式一:手动注册 手动在注解处理器Module下创建目录META-INF/services,创建javax.annotation.processing.Processor文件,在文件中写入自定义的注解处理器名称(包名+类名),例如

	com.leo.annotation.compiler.SimpleButterKnifeProcessor

方式二:自动注册 google提供了一个注册处理器的库AutoService,使用注解@AutoService(Process.class)即可在编译的时候在注解处理器Module的build目录下生成对应注解处理器.class文件和注册配置文件META-INF/services/javax.annotation.processing.Processor

####4.使用注解处理器

1.在app的build.gradle中引入自定义注解处理器

    //依赖注解框架
	implementation project(':miAnnotation')
    annotationProcessor project(':miAnnotationCompiler')

2.然后在需要使用注解的元素上使用注解

    //使用注解
    @BindView(R.id.list)
    public ListView list;

3.重新Make编译app即可执行注解处理器的操作

###参考链接

Android自定义注解处理器