在日常开发中我们会经常使用注解(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.注解中定义参数
注解的属性也称为成员变量,注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。 注解内的可使用的数据类型是有限制的,类型如下:
所有的基本类型(int,float,boolean 等)
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即可执行注解处理器的操作
###参考链接