什么是注解
-
所有写过Java的都应该见过见过@Override、@Deprecated这种“神奇的”语法吧,比如
@Override public String toString() { ... }
再比如说Android里面的ButterKnife也是通过这种方式来“自动”的完成View的查找,再比如说Retrofit也用到了这种语法,当我们用一些优雅的第三方库的时候,往往会用到注解来很方便的实现一些功能,模糊的去讲,注解是可以用到parameters、field、method、class、annotation上的一种特殊的语法,比如说在Retrofit里面就体现的淋漓尽致
@GET("/get/") Call
> getSomething(@Query String param);
注解之元注解
- 元注解就是Java提供的为我们自定义注解而提供的内置注解,其实就相当于Java中的关键字,他们是内置的,有了这些关键字我们才能写自己的代码
- 元注解有四个:
- @Target
- @Retention
- @Inherited
- @Documented
@Target
Target也就是注解可以修饰的类型,所有的类型都在ElementType下面,比如ElementType.CLASS代表可以修饰类、ElementType.TYPE代表可以修饰任何类型、ElementType.ANNOTATION_TYPE代表这个注解可以修饰注解,其他的可以自行去查看文档
-
提前先说一下,每一个注解都可以定义一个value的属性,作为默认的注解值,另外在注解中如果使用数组类型,我们可以仅使用一个值,Java会自动把单个值包装成一个数组对象,例如
@Target(ElementType.CLASS) public @interface MyAnnotation {}
或者多个值
@Target({ElementType.CLASS, ElementType.Field}) public @interface MyAnnotation {}
下面自定义还会细讲一些
@Retention
- Retention表示了这个注解存在的时间,其所有类型在RetentionPolicy下,只有三种类型
- SOURCE: 表示只在源码中存在,也就是说这个注解会在javac编译时抹去,它的作用就是在编译的时候做一些静态检查,比如@Override
- CLASS: 表示会在class文件中存在,但是会被VM忽略,也就是说无法通过反射动态获取注解的值,这也是默认行为,我们可以从class文件中找到这些信息,也就意味着可以自定义类加载器,然后可以在原始IO中找到对应信息(ClassLoader.findClass),只是通过class文件在永生区/元空间生成class对象的时候,classLoader对应的jni方法defineClass(注:在Java8上响应的方法是defineClass0、defineClass1、defineClass2,在Android上是defineClass)会忽略这样的Annotation;同时,我们可以通过decompile获得这些注解信息
- RUNTIME: 表示在运行时也会存在,也就是我们可以在代码中动态获取到这个注解和注解的值,当然获取的方式是通过反射
@Inherited
- 如果定义注解时使用了Inherited,那么当我们使用这个注解去修饰类A的时候,即使我们不对A的子类使用注解,但是我们依旧能通过子类获取到这个注解,也就是说@Inherited意味着自定义的注解是可以继承的,当我们尝试从某个类拿Inherited的注解时,会不停的去它的superClass中寻找。但是记住这种可以继承的特性只对类生效,也就是说Override的方法是不生效的
@Documented
- 如果使用@Documented修饰了某个自定义注解,那这个注解在生成Javadoc时会自动保留,即添加在对应的方法/变量/…上
使用注解
基础知识
- 在注解中只能使用方法,但是注解的方法在使用的时候和普通字段一样
- 注解中的value字段有特殊含义,是默认赋值字段
- 定义注解时可以使用数组类型,而且当使用时可以只穿入单个值,java会自动把它包装成一个长度为1的数组
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface MyAnnotation {
int[] value();
String name() default "";
}
public class Person {
@MyAnnotation(1)
int age;
@MyAnnotation(name="John")
String name;
}
编译时注解
- 可以使用RetentionPolicy.SOURCE或者RetentionPolicy.CLASS来达到编译时注解的目的
- 这种编译期的注解主要在于自动生成代码,比如Android上的ButterKnife
- 编译期注解的优点在于消耗的时间完全在编译期,在运行时不需要额外的时间,但是缺点也是很明显的,首先因为要自动生成代码(元编程),编写这种代码其实还不如反射写起来舒服(因为反射的时候好歹可以直接获取到某个变量的值),其次就是很多情况下要求字段必须不能是private的,或者更确切的说生成的代码的位置必须是可以访问到注解所修饰的字段的,(参见ButterKnife),当然你也可以把生成的代码插入字段所在的源码文件中,就不用管它是不是private了
- 栗子(我直接在Android Studio上做的实验,Intellij Idea没有找到怎么让AbstractProcessor生效):
//MyProcessor: 继承AbstractProcessor, 都是接口类型Processor, 这个类其实很好理解, 是java为我们提供
//的接口,当javac编译的时候会调用你配置好的Processor类去进行编译期注解的处理, 不然你只定义Annotation谁
//知道应该怎么处理它呢?所以我们必须要自己实现Processor,也叫注解处理器
//这个类做的事情其实是生成StaticAnnotation修饰的<变量名字, 类名>的json
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyProcessor extends AbstractProcessor {
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
File file = new File("output.json");
System.out.println("------------");
try {
for (TypeElement element : annotations) {
System.out.print(element.getSimpleName() + " ");
}
Set extends Element> elements = roundEnv.getElementsAnnotatedWith(StaticAnnotation.class);
System.out.println(elements.size());
if (elements.size() == 0) return false;
Writer writer = new BufferedWriter(new FileWriter(file));
writer.append("{\n");
for (Element element : elements) {
System.out.println(element.getSimpleName());
System.out.println(element.getKind() == ElementKind.FIELD);
if (element.getKind() == ElementKind.FIELD) {
writer.append(element.getSimpleName());
writer.append(": ");
writer.append(element.getEnclosingElement().getSimpleName());
writer.append("\n");
}
}
writer.append("}\n");
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
//声明可以处理的注解的类型,添加到set里的必须是全名
@Override
public Set getSupportedAnnotationTypes() {
Set set = new HashSet<>();
set.add(StaticAnnotation.class.getCanonicalName());
return set;
}
}
//自定义的注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface StaticAnnotation {
int value();
}
//测试类
public class Student {
@StaticAnnotation(3)
String name;
@StaticAnnotation(2)
boolean isMale;
}
注:
- 在Android上使用时,AbstractProcessor在Android module下是获取不到的,要建立一个java module,然后在main下面建立META-INF/services/javax.annotation.processing.Processor,再向里面添加对应的Processor的全名
- @SupportedAnnotationTypes就是对getSupportAnnotationTypes()的简写,@SupportedSourceVersion对应getSupportedSourceVersion()
- 我个人认为很多情况下使用RetentionPolicy.SOURCE就已经足够了,毕竟很多情况下我们只是生成完代码之后就用不着注解了
运行时注解
- Retention对应的就是RUNTIME,Retrofit就是使用的这种注解,运行时注解在VM中也会保留,所以我们可以通过反射方法拿到Annotation(Method、Field、Class都有getAnnotation方法)
- 运行时的注解就不像编译时的那么麻烦了,因为它不需要Processor这种"驱动",它的优点就是灵活,缺点就是反射导致效率低,不过在网络请求中,反射效率再怎么低都是要比网络延迟要好很多的
- 栗子
public class TestClass {
@MyAnnotation(1)
private int value;
public static void main(String[] args) {
TestClass instance = new TestClass();
Class clazz = instance.getClass();
try {
Field field = clazz.getDeclaredField("value");
MyAnnotation annotation = field.getAnnotation(MyAnnotation.class);
Object o = field.get(instance);
o = annotation.name();
field.setInt(instance, annotation.name()[0]);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
System.out.println(instance.value);
}
}
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
int[] name() default 0;
int[] value();
}
上述例子利用注解对相应的实例变量进行了赋值,虽然反射也很烦,但是还是要比编译时生成代码写起来要更舒服一些的(也许是我对反射比元编程更习惯一些吧)