注解(Annotation)就是一种程序可识别的注释。 本文介绍注解的使用。
文章目录
一、概述
注解:程序可识别的注释
注解的格式:
@xxx(value = yyy)
@xxx(yyy)
@xxx(aaa = bbb, ccc = ddd)
识别的方式:
- 编码期直接识别(@Override)
- 编译期通过注解处理器识别
- 运行期通过反射识别
二、注解的分类
2.1 JDK 自带注解
- @Override 重写, 标识覆盖它的父类的方法
- @Deprecated 已过期,表示方法是不被建议使用的
- @Suppvisewarnings 压制警告,抑制警告
2.2 元注解
元注解用来生成自定义注解
-
@Target
指明注解可以被写在哪里,取值为 ElementType 的枚举值。 注解没有指明 @Target 的话,取值为除了 TYPE_PARAMETER 的所有地方。
类型 代表的地方 TYPE 类、接口(包括注解)、枚举 FIELD 本地变量、枚举值 METHOD 方法(不包括构造方法) PARAMETER 参数 CONSTRUCTOR 构造方法 LOCAL_VARIABLEPE 局部变量 ANNOTATION_TYPE 注解(TYPE 的一部分) PACKAGE 包(用在 package-info.java) TYPE_PARAMETER 类型参数(1.8新加入,泛型中的类型参数) TYPE_USE 类型使用(1.8新加入,任何使用类型的地方) -
@Retention
指明
@xxx的字面值在代码中保留的最后时期,取值为 RetentionPolicy 的枚举值。默认是CLASS。类型 代表的时期 source 源码(编译时丢失) class class文件(JVM 类加载时丢失) runtime 运行时(不会丢失) -
@Documented 指明注解应该被 javadoc 工具记录,注解信息会被包括在生成的文档中。
-
@Inherited 可以让注解被继承,但这并不是真的继承,只是通过使用 @Inherited,可以让子类 Class 对象使用 getAnnotations() 获取父类被 @Inherited 修饰的注解
2.3 自定义注解
使用元注解来生成自定义注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface MyCustom {
int value();
}
三、注解的语法
3.1 定义注解
使用 @interface 来定义一个注解:
public @interface MyCustom {
}
它与接口非常相似,我们查看一下反编译后的注解类:
public interface MyCustom extends Annotation {
}
可以发现,最终实现也是作为一个接口来实现的,但注解与接口不同,它不支持继承,即定义注解时无法使用 extends 关键字来继承其他注解。
3.2 定义元素
注解中可以定义元素(相当于定义一个有返回值的方法):
public @interface MyCustom {
int value()
}
注解元素支持的类型:
- 所有基本类型(byte、short、int、long、float、double、char、boolean,它们的包装类型不行)
- String
- Class
- enum
- Annotation
- 上述类型的数组(List 不行)
元素可以有默认值:
public @interface MyCustom {
int value() default 1;
}
默认值的限制:
- 如果没有默认值,那在使用时必须提供
- 对于非基本类型的元素,无论是默认值,还是在使用时提供的值,都不能以null作为值。为了绕开这个限制,只能定义一些特殊的值,例如空字符串或负数,表示某个元素不存在。
3.3 使用注解
在注解可放置的地方放置注解,并提供元素值:
@MyCustom(value = 2)
class MyClass {
}
当注解只有一个名为 value 的元素时,赋值时 value = 可以省略:
@MyCustom(2)
class MyClass {
}
四、注解的处理
4.1 注解处理器
在编译时,可以通过注解处理器获取注解的信息。
4.1.1 定义注解
创建一个注解定义模块 annotation。定义一个注解 @BindView。
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
4.1.2 定义注解处理器
创建一个注解处理模块 processor,它依赖于 annotation。
implementation project(':annotation')
注解处理器继承于 AbstractProcessor。一般来说,要实现四个方法(更多见这里):
- init(processingEnvironment)
初始化注解处理器,processingEnvironment 中会提供一些实用工具,如用来报告错误、警报和其他通知的 Messager;用来创建新源、类或辅助文件的 Filer 等。 - getSupportedSourceVersion
设置该处理器支持的最新的 jdk 版本。一般返回 SourceVersion.latestSupported()。 - getSupportedAnnotationTypes
设置该处理器要处理的注解类型,要提供包名+类名。 - process(annotationSet, roundEnvironment)
注解的处理逻辑。
注解处理分为多个轮次,每一轮次该方法都会调用 process 方法。
它的两个参数 annotationSet 是要处理的注解类型,roundEnvironment 是上一轮次的环境信息,可以拿到注解修饰的语句信息。
如果返回 true,则后续 Processor 不会再处理 annotationSet 中的注解;如果返回 false,则后续 Processor 还会处理它们。
@AutoService(Processor.class)
public class MyAnnotationProcessor extends AbstractProcessor {
private Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mMessager = processingEnvironment.getMessager();
mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor init");
}
@Override
public SourceVersion getSupportedSourceVersion() {
mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor getSupportedSourceVersion");
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor getSupportedAnnotationTypes");
Set<String> strings = new HashSet<>();
strings.add(BindView.class.getCanonicalName());
return strings;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor process " + roundEnvironment.hashCode());
Set<? extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(BindView.class);
mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor 要处理的注解 " + set);
mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor 注解修饰的语句 " + elementSet);
if (elementSet != null) {
for (Element element : elementSet) {
VariableElement variableElement = (VariableElement) element;
Name name = variableElement.getSimpleName();
BindView bindView = variableElement.getAnnotation(BindView.class);
mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor 注解修饰的语句信息和注解元素值 " + name
+ ", " + bindView.value());
}
}
return true;
}
}
4.1.3 使用注解
主模块 app,它依赖于 annotation、processor。
implementation project(':annotation')
annotationProcessor project(':processor')
添加注解
public class MainActivity extends AppCompatActivity {
@BindView(1)
private TextView mTextView1;
@BindView(2)
private TextView mTextView2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
4.1.4 注解处理器执行
make project,输出:
> Task :app:compileDebugJavaWithJavac
注: MyAnnotationProcessor init
注: MyAnnotationProcessor getSupportedSourceVersion
注: MyAnnotationProcessor getSupportedAnnotationTypes
注: MyAnnotationProcessor getSupportedAnnotationTypes // 第一轮
注: MyAnnotationProcessor process 1845469711
注: MyAnnotationProcessor 要处理的注解 [com.gdeer.annotation.BindView]
注: MyAnnotationProcessor 注解修饰的语句 [mTextView1, mTextView2]
注: MyAnnotationProcessor 注解修饰的语句信息和注解元素值 mTextView1, 1
注: MyAnnotationProcessor 注解修饰的语句信息和注解元素值 mTextView2, 2
注: MyAnnotationProcessor getSupportedAnnotationTypes // 第二轮
注: MyAnnotationProcessor process 977562649
注: MyAnnotationProcessor 要处理的注解 []
注: MyAnnotationProcessor 注解修饰的语句 []
注: MyAnnotationProcessor getSupportedAnnotationTypes // 第三轮
注: MyAnnotationProcessor process 2034412128
注: MyAnnotationProcessor 要处理的注解 []
注: MyAnnotationProcessor 注解修饰的语句 []
可以看到进行了三轮注解处理,每一轮都会调到 process 方法,每次 process 方法中的 roundEnvironment 都不相同。因为第一次 process 返回了 true,所以之后两轮中都没有要处理的注解,所以都没有进行处理。
4.1.4 生成 java 文件
从注解中获取信息后,我们就可以通过这些信息生成新的 java 文件,这里可以借助 JavaPoet 工具,将编辑好的文件信息写入 init 方法的 processingEnvironment 中提供的 Filer 中。这样,就可以在 /app/build/generated/source/apt/debug 目录下看到生成的文件。
JavaPoet 相关操作可参考这里:JavaPoet 的使用。
4.2 反射
在运行时,可以通过反射获取注解的信息。
定义一个 UseCase 注解(Retention 为 RUNTIME,否则在运行时找不到该注解):
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
int id();
String description() default "no description";
}
使用 UseCase 注解:
public class PasswordUtils {
@UseCase(id = 47, description = "password must contain at least one numeric")
public boolean validatePassword(String password) {
return (password.matches("sdfsfd"));
}
@UseCase(id = 48)
public String encryptPassword(String password) {
return new StringBuilder(password).reverse().toString();
}
@UseCase(id = 49, description = "New passwords can't equal previously used ones")
public boolean checkForNewPassword(List<String> prevPassword, String password) {
return !prevPassword.contains(password);
}
}
处理 UseCase 注解:
使用 declaredMethod.getAnnotation(UseCase.class) 获取 declaredMethod 方法上的 @UseCase 注解。
public class UseCaseTracker {
public static void trackUseCases(List<Integer> useCases, Class<?> cl) {
for (Method declaredMethod : cl.getDeclaredMethods()) {
UseCase useCase = declaredMethod.getAnnotation(UseCase.class);
if (useCase != null) {
System.out.println(useCase.id() + " " + useCase.description());
useCases.remove(Integer.valueOf(useCase.id()));
}
}
for (Integer useCase : useCases) {
System.out.println("missing " + useCase);
}
}
public static void main(String[] args) {
List<Integer> useCases = new ArrayList<>();
Collections.addAll(useCases, 47, 48, 49, 50);
trackUseCases(useCases, PasswordUtils.class);
}
}
输出:
47 password must contain at least one numeric
48 no description
49 New passwords can't equal previously used ones
missing 50