本文记录笔者对Java注解的相关学习,了解注解的相关分类;并学识如何书写注解的相关知识
先说结论:
针对运行阶段,注解可分为
针对本身属性,注解可分为
保留阶段不同。运行时注解保留到运行时,可在运行时访问。而编译时注解保留到编译时,运行时无法访问。
原理不同。运行时注解是Java反射机制,Retrofit运行时注解,需要用的时候才用到,而编译时注解通过APT、AbstractProcessor。
性能不同。运行时注解由于使用Java反射,因此对性能上有影响。编译时注解对性能没影响。这也是为什么ButterKnife从运行时切换到了编译时的原因。
产物不同。运行时注解只需自定义注解处理器即可,不会产生其他文件。而编译时注解通常会产生新的Java源文件。
需要注意的是,注解虽然有作用,但注解永远是一个被动的行为
使用到运行时注解的比较有代表的框架就是Android的Retrofit框架
声明运行时注解,需要用到RetentionPolicy.RUNTIME该元注解
使用运行时注解,可以完成默认值设定等一系列效果,从而提高开发效率,具体可看下方的例子
首先我们设定两个相关注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindPort {
String value() default "8080";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindAddress {
String value() default "127.0.0.0";
}
通过设定元注解可以使其成为运行时注解并且具有默认值,具体的大家不懂可以看后文的例子
运行时使用
定义相关类
public class TestClass {
@BindAddress()
String address;
@BindPort()
private String port;
private int number;
public void printInfo() {
System.out.println("info is " + address + ":" + port);
}
}
使用反射获取注解信息,并进行使用
//获取类
Class c = Class.forName(className);
//实例化一个TestClass对象
TestClass tc= (TestClass) c.newInstance();
// 获取所有的属性
Field[] fields = c.getDeclaredFields();
for (Field field : fields) {
if(field.isAnnotationPresent(BindPort.class)){
BindPort port = field.getAnnotation(BindPort.class);
field.setAccessible(true);
field.set(tc,port.value());
}
if (field.isAnnotationPresent(BindAddress.class)) {
BindAddress address = field.getAnnotation(BindAddress.class);
field.setAccessible(true);
field.set(tc,address.value());
}
}
tc.printInfo();
定义一个相关注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BindGet {
String value() default "";
}
定义相关类
public class TestClass {
@BindAddress("http://www.google.com.cn")
String address;
@BindPort("8888")
private String port;
private int number;
@BindGet("mike")
void getHttp(String param){
String url="http://www.baidu.com/?username"+param;
System.err.println("get------->"+url);
}
}
运行时使用
//获取类
Class c = Class.forName(className);
TestClass tc= (TestClass) c.newInstance();
// 获取所有的方法
Method[] ms = c.getDeclaredMethods();
for (Method method : ms) {
if(method.isAnnotationPresent(BindGet.class)){
BindGet bindGet = method.getAnnotation(BindGet.class);
String param=bindGet.value();
method.invoke(tc, param);
}
}
编译时注解应用一样十分广泛,除了之前提到ButterKnife,还有ARouter是通过编译时注解生成路由表,Tinker通过编译时注解生成Application的代理类。编译时注解和运行时注解定义的方式是完全一样的,不同的是它们对于注解的处理方式,运行时注解是在程序运行时通过反射获取注解然后处理的,编译时注解是程序在编译期间通过注解处理器处理的。
相关编译时注解处理流程如下

从上面的流程图我们也可以看出,编译时注解的处理过程是递归的,先扫描原文件,再扫描生成的文件,直到所有文件中都没有待处理的注解才会结束。其中扫描注解并不需要我们处理,我们需要关心的就是如何处理注解以及如何将处理后的结果写入到文件中。
注解处理器早在JDK1.5的时候就有这个功能了,只不过当时的注解处理器是apt,相关的api是在com.sun.mirror包下的。从JDK1.6开始,apt相关的功能已经包含在了javac中,并提供了新的api在javax.annotation.processing和javax.lang.model to process annotations这两个包中。旧版的注解处理器api在JDK1.7已经被标记为deprecated,并在JDK1.8中移除了apt和相关api。
JDK中Processor类图如下,Processor就是用于处理编译器注解的类。通过继承AbstractProcessor就可以自定义处理注解。

下文我们以模拟ARouter处理过程进行说明其中的方法
@Override
public Set<String> getSupportedOptions() {
HashSet<String> set = new HashSet<>();
set.add("MODULE_NAME");
return set;
}
在gralde文件中的传入参数
javaCompileOptions {
annotationProcessorOptions {
arguments = [MODULE_NAME: "this module name is " + project.getName()]
}
}
最后在Processor的init方法中获取到相应参数即可
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
System.out.println(processingEnvironment.getOptions().get("MODULE_NAME"));
}
getSupportedSourceVersion:返回你目前使用的JDK版本,通常返回SourceVersion.latestSupported(),当然如果你没使用最新的JDK版本的话,也可以返回指定版本。
getSupportedAnnotationTypes: 这个方法用于注解的注册,只有在这个方法中注册过的注解才会被注解处理器所处理
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(BindView.class.getCanonicalName());
return types;
}
Processor接口中定义了注解处理器中必要的方法,AbstractProcessor是实现Processor接口的一个抽象类,它在Processor的基础上提供了三个个注解功能,分别对应上面的三个方法。从下图中的名字也很容易看出对应的哪些方法。
添加链接描述

所有被注解标注的部分都会被解析成element,在上面介绍的process方法中,通过roundEnvironment的getElementsAnnotatedWith方法就可以获取到element的set,element既可能是类,也可能是类属性,还可能是方法,所以接下来我们还需要将element转换成对应的子类。
具体到element相关子类如下
相关使用注解进行实战可以查看这篇博客
运行时注解
标准注解有以下几种:
元注解是用来标注注解的注解,在注解定义时使用。有以下几种:
下面重点介绍下@Targe注解及@Retention注解:
@Targe注解
其中@Targe注解的取值是一个ElementType类型的数值。这里有以下几种取值,对应不用的对象范围。
@Retention注解
@Retention注解有3种类型,分别表示不同级别的保留策略。
定义新的注解类型使用@interface关键字,这与定义一个接口很像:
public @interface MyAnnotation {
}
定义完成后,就可以在程序中使用注解
@MyAnnotation
public class AnnotationTest {
}
注解只有成员变量,没有方法。注解的成员变量在注解定义中以“无参的方法”形式来声明,其“方法名”定义了该成员变量的名字,其返回值定义了该成员变量的类型:
public @interface MyAnnotation {
String name();
int age();
}
上面的代码定义了name和age两个成员变量,使用该注解时就要给两个成员变量指定值:
@MyAnnotation(name = "droidYu",age = 0)
public class AnnotationTest {
}
也可以在定义成员变量时,用default关键字为其指定默认值:
public @interface MyAnnotation {
String name() default "droidYu";
int age() default 0;
}
在使用时就可以不进行赋值操作:
@MyAnnotation()
public class AnnotationTest {
}
定义成员变量时,有个一特殊的成员变量value,在使用时可以不用写 value = ,而直接传入value的值即可:
public @interface MyAnnotation {
String name() default "droidYu";
int age() default 0;
String value();
}
@MyAnnotation(value = "no value")
public class AnnotationTest {
}
此时value=可以省略:
@MyAnnotation("no value")
public class AnnotationTest {
}
定义注解时,还可以为注解添加元注解,例如使用@Target和@Retention元注解来定义只能用来注解方法的运行时注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyMethod {
}
此时的MyMethod注解就只能标注在方法上,如果标注在类上,编译器就会提示错误警告,编译不能通过。
@MyAnnotation("no value")
@MyMethod //这里会报错,编译不能通过
public class AnnotationTest {
@MyMethod //正确的使用位置
public void method() {
}
}