什么是注解
java.lang.annotation.Annotation 接口中有这么一句话,用来描述注解。
The common interface extended by all annotation types 所有的注解类型都继承 Annotation
这句话有点抽象,但却说出了注解的本质。我们看看 JDK 内置注解 @Override 的定义:
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
其实本质上是:
java
public interface Override extends Annotation{
}
没错,注解的本质就是一个继承了 Annotation 接口的接口。有关这一点,你可以去反编译任意一个注解类看看结果。
比如新建一个 Teacher.java 文件用于自定义注解:
less
@Target({ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Teacher {
String name();
int age() default 20;
}
然后使用 javac Teacher.java 命令生成 Teacher.class 文件,再使用 javap Teacher.class 反编译后输出如下:
java
Compiled from "Teacher.java"
public interface com.example.test.Teacher extends java.lang.annotation.Annotation {
public abstract java.lang.String name();
public abstract int age();
}
可以看到自定义注解确实是一个继承自 java.lang.annotation.Annotation 接口的接口。
准确意义上来说,注解只不过是代码里的特殊标记而已。这些标记可以在编译、类加载或运行时被读取,并执行相应的处理。通过使用注解,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证、处理或者部署。
元注解
与声明一个类不同的是,注解的声明使用 @interface 关键字,@Override 注解的声明如下:
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
其中的 @Target,@Retention 两个注解是我们所谓的『元注解』,『元注解』是用于修饰注解的注解,通常用在注解的定义上,『元注解』一般用于指定某个注解的保留策略以及作用目标等信息。
Java 中有以下几个『元注解』:
- @Target:注解的作用目标
- @Retention:注解的保留策略
- @Documented:注解是否应当被包含在 JavaDoc 文档中
- @Inherited:是否允许子类继承该注解
其中,@Target 用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的,修饰类的,还是用来修饰字段属性的。
@Target定义如下:
java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Target {
ElementType[] value();
}
我们可以通过以下方式来为这个 value 传值:
@Target(value = {ElementType.FIELD})
上面这段代码表示该注解将只能作用在成员字段上,不能用于修饰方法或者类。其中,ElementType 是一个枚举类型,有以下一些值:
- ElementType.TYPE 允许被修饰的注解作用在类、接口和枚举上
- ElementType.FIELD 允许作用在成员变量上
- ElementType.METHOD:允许作用在方法上
- ElementType.PARAMETER:允许作用在方法参数上
- ElementType.CONSTRUCTOR:允许作用在构造方法上
- ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
- ElementType.ANNOTATION_TYPE:允许作用在注解上
- ElementType.PACKAGE:允许作用在包上
@Retention 用于指明当前注解的保留策略,它的基本定义如下:
java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Retention {
RetentionPolicy value();
}
同样的,它也有一个 value 属性:
@Retention(value = RetentionPolicy.RUNTIME)
这里的 RetentionPolicy 依然是一个枚举类型,它有以下几个枚举值可取:
- RetentionPolicy.SOURCE:源码级注解。注解信息只会保留在 .java 源码中,源码在编译后,注解信息被丢弃,不会保留在 .class 中。
- RetentionPolicy.CLASS:编译时注解。注解信息会保留在 .java 源码以及 .class 中。当运行 Java 程序时, JVM 会丢弃该注解信息,不会保留在 JVM 中。
- RetentionPolicy.RUNTIME:运行时注解。当运行 Java 程序时, JVM 也会保留该注解信息,可以通过反射获取该注解信息。
@Retention 注解指定了被修饰的注解的保留策略,这3个策略的生命周期长度为 SOURCE < CLASS < RUNTIME 。生命周期短的能起作用的地方,生命周期长的也一定能起作用。如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarning ,则可选用 RetentionPolicy.SOURCE 。如果要在编译时进行一些预处理操作,比如生成一些辅助代码(ARouter 的 @Route 注解就是这种情况),就用 RetentionPolicy.CLASS 。如果要在运行时动态获取注解信息,那就只能用 RetentionPolicy.RUNTIME 。
剩下两种类型的元注解我们日常用的不多,也比较简单,这里不再详细介绍了,你只需要知道他们各自的作用即可。@Documented 注解修饰的注解,当我们执行 JavaDoc 文档打包时会被保存进 doc 文档,反之将在打包时丢弃。@Inherited 注解修饰的注解是具有可继承性的,也就说我们的注解修饰了一个类,而该类的子类将自动继承父类的该注解。
下面我们来自定义一个注解,里面有 2 个成员变量:
java
@Target({ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Teacher {
String name(); //无默认值
int age() default 20;
}
注解只有成员变量,成员变量在注解定义中以无形参的方法的形式来声明。在使用注解时,如果定义的注解中的成员变量无默认值,则使用时必须传值。
java
@Teacher(name = "小帅") // name 无默认值,需要传值;age 有默认值,可以省略
@Teacher(name = "小帅", age = 22) // 也可以显式赋值
源码级注解的应用
源码级注解的注解信息只会保留在 .java 源码中,编译后注解信息会被丢弃,不会保留在 .class 中。编译如下代码:
java
@Teacher(name = "小帅", age = 22)
public class Test {
}
然后去 build\intermediates\javac\debug\classes\ 目录下找到 Test.class,打开发现已经找不到注解了。如果将保留策略改成 RetentionPolicy.CLASS ,则在 Test.class 中还可以找到注解:
class
@Teacher(
name = "小帅",
age = 22
)
public class Test {
}
源码级注解主要用来做一些检查性的工作,比如,在 androidx.annotation 中有提供 @IntDef 注解,此注解的定义如下:
java
@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {
int[] value() default {};
boolean flag() default false;
boolean open() default false;
}
此注解能够取代枚举,实现方法入参的限制。
Java 中的 enum (枚举)实质是特殊单例的静态成员变量,在运行期所有枚举类作为单例,全部加载到内存中,比常量多 5 到 10 倍的内存占用。
比如我们定义一个 test() 方法,接收的参数 Student 只能在 Jim 、 Lily 两位学生中选一个,如果使用枚举实现,代码如下:
java
public class Test {
enum Student{
Jim,
Lily
}
public void test(Student student){
}
}
为了优化内存,我们现在不再使用枚举,改成如下代码:
java
public class Test {
public static final int Jim = 1;
public static final int Lily = 2;
public void test(int student){
}
}
这种方式有什么弊端呢?test() 方法由于采用 int 类型,将无法限定为 Jim 和 Lily 。
下面我们使用注解进行优化:
java
public interface Constants {
int Jim = 1;
int Lily = 2;
}
java
@IntDef(value = {Constants.Jim, Constants.Lily})
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.SOURCE)
public @interface Student {
}
java
public class Test {
public static void test(@Student int student){
}
}
此时,我们再去调用 test() 方法,如果传递的参数不是 Constants.Jim 或 Constants.Lily ,Android Studio 会报红。
运行时注解的应用
针对运行时注解会采用反射机制获取注解信息,我们定义一个运行时注解,代码如下:
java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Teacher {
String name();
int age() default 20;
}
接下来在 AnnotationTest 类的方法上应用该注解,代码如下:
java
public class AnnotationTest {
@Teacher(name = "老王")
public String getMathTeacherName(){
return "";
}
@Teacher(name = "老刘")
public String getEnglishTeacherName(){
return "";
}
}
最后写一个简单的注解处理器,通过反射获取注解中的信息:
java
public class AnnotationProcessor {
public static void main(String[] args) {
Method[] methods = AnnotationTest.class.getDeclaredMethods();
for(Method m : methods){
// 通过 getAnnotation() 获取指定类型的注解对象
Teacher teacher = m.getAnnotation(Teacher.class);
System.out.println(teacher.name());
}
}
}
输出结果为:
java
老刘
老王
这样就拿到了注解中的成员变量的值,Retrofit 中的注解也是同样的原理,通过反射拿到注解中的信息。
编译时注解的应用
处理编译时注解相对会比较麻烦,编译时注解一般会结合注解处理器(APT)对注解进行处理,这里模拟一个 ButterKnife 自动绑定布局 id 的例子进行说明。
1. 定义注解
新建一个项目,在项目中新建一个 Java Library 来存放注解,Library 命名为 annotations 。接下来定义注解,如下:
java
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface MyBindView {
// value 值对应要绑定的布局 id
int value();
}
注解定义好了,在 Activity 中就可以使用了。在 app 模块的 MainActivity 中使用该注解,代码如下:
java
public class MainActivity extends AppCompatActivity {
@MyBindView(R.id.tv_text)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
通过注解的 value 值就可以拿到布局 id ,但是怎么把 View 和 id 绑定起来呢?
如果 MyBindView 是 RetentionPolicy.RUNTIME 类型的,可以通过运行时反射绑定 id:
java
public static void bind(Activity activity) {
// 获取成员变量
for (Field field : activity.getClass().getDeclaredFields()) {
// 判断这个成员变量上是否有 @MyBindView 注解
MyBindView myBindView = field.getAnnotation(MyBindView.class);
if (myBindView != null) {
try {
// 将 activity 中成员 field 的值赋值为:activity.findViewById(myBindView.value())
field.set(activity, activity.findViewById(myBindView.value()));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
但是现在注解不是 RetentionPolicy.RUNTIME 类型的,并且每次运行的时候去反射效率比较低,因为反射是比较耗性能的,当然你可以用缓存来优化,还有一种更好的办法是使用 APT 结合 JavaPoet 来实现。
2.编写注解处理器
再新建一个 Java Library 来存放注解处理器,Library 命名为 processor,接下来编写注解处理器:
java
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnvironment.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 遍历所有的类
for (Element element : roundEnvironment.getRootElements()) {
// 获取类的包名
String packageStr = element.getEnclosingElement().toString();
// 获取类的名字
String classStr = element.getSimpleName().toString();
// 需要构建的新类名:原类名 + Binding
ClassName className = ClassName.get(packageStr, classStr + "Binding");
// 构建新的类的构造方法
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get(packageStr, classStr), "activity");
boolean hasBuild = false;
// 获取类里面的元素,比如类的成员变量、方法、内部类等
for (Element enclosedElement : element.getEnclosedElements()) {
// 获取成员变量
if (enclosedElement.getKind() == ElementKind.FIELD) {
// 判断是否被 @MyBindView 注解
MyBindView bindView = enclosedElement.getAnnotation(MyBindView.class);
if (bindView != null) {
// 需要生成类
hasBuild = true;
// 在构造方法中加入代码
constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
enclosedElement.getSimpleName(), bindView.value());
}
}
}
// 判断需要生成
if (hasBuild) {
try {
// 构建新的类
TypeSpec builtClass = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addMethod(constructorBuilder.build())
.build();
// 生成 Java 文件
JavaFile.builder(packageStr, builtClass)
.build().writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
// 只支持 @MyBindView 注解
return Collections.singleton(MyBindView.class.getCanonicalName());
}
}
在其 build.gradle 中添加如下配置:
c
dependencies {
implementation project(":annotations")
// AutoService 是Google提供的
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
compileOnly 'com.google.auto.service:auto-service:1.0-rc7'
// javapoet 是一个代码生成框架,利用它可以优雅的生成我们想要的代码
implementation 'com.squareup:javapoet:1.10.0'
}
其中 AutoService 是 Google 提供的,用来帮助生成 javax.annotation.processing.Processor 文件的,如果你不使用它,你就需要自己去 resources 中添加该文件。
注解处理器里面的几个方法解释一下:
- init: 会被注解处理工具调用,入参为 ProcessingEnvironment,ProcessingEnvironment 提供了很多有用的工具类,比如 Elements、Types、Filter 和 Messager 等。
- process:你要在这里对注解进行处理。
- getSupportedAnnotationTypes:指定这个注解处理器支持的注解类型。
process() 方法中的代码会查找成员变量上有 @MyBindView 注解的类,生成代码,类名为:原类名 + Binding,并通过 findViewById 来绑定 View 和 id 。代码是通过 com.squareup:javapoet:1.10.0 这个框架来生成的,如果没有这个框架,那么需要使用字符串拼接的方式来生成代码, EventBus 就使用了字符串拼接的方式:

给 app 模块的build.gradle添加如下配置:
css
dependencies {
implementation project(path: ':annotations')
annotationProcessor project(':processor')
}
然后在Terminal中输入:./gradlew :app:compileDebugJavaWithJavac,就会在 app 模块下生成MainActivityBind.java文件:

打开 MainActivityBind.java,代码如下:
java
public class MainActivityBinding {
public MainActivityBinding(MainActivity activity) {
activity.textView = activity.findViewById(2131231011);
}
}
此时只是生成了我们想要的代码,还需要调用 MainActivityBinding 的构造函数。
3.应用注解
再新建一个 Android Library,Library 命名为 reflect,在 build.gradle 中添加如下配置:
css
dependencies {
api project(path: ':annotations')
}
代码如下:
java
public class MyButterKnife {
public static void bind(Activity activity) {
try {
// 获取"当前的 activity 类名 + Binding "的 class 对象
Class bindingClass = Class.forName(activity.getClass().getCanonicalName() + "Binding");
// 获取 class 对象的构造方法,该构造方法的参数为当前的 activity 对象
Constructor constructor = bindingClass.getDeclaredConstructor(activity.getClass());
// 创建实例
constructor.newInstance(activity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
最后在 app 模块的 MainActivity 中调用 MyButterKnife 的 bind() 方法,代码如下:
java
public class MainActivity extends AppCompatActivity {
@MyBindView(R.id.tv_text)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyButterKnife.bind(this);
textView.setText("Hello APT!!!!");
}
}
其中 app 模块的 build.gradle 配置需要修改成如下所示:
css
dependencies {
implementation project(path: ':reflect')
annotationProcessor project(':processor')
}
运行 MainActivity ,通过 MyButterKnife 的 bind() 方法就实现了自动绑定布局 id 的功能。可以分析一下 ARouter 框架,也是类似的原理。
Demo地址:github.com/EnzoXRay/An...