注解是什么?怎么用?

1,718 阅读8分钟

什么是注解

注解(Annotation)又称标注,是JDK5.0引入的一种注释机制。java语言中的类、方法、变量、参数、包等都可以被注解修饰。根据注解的保留策略,又可以在不同的时机对注解进行操作,比如编译时通过注解处理器进行处理或者在程序运行时通过反射对注解的信息进行修改。通过注解可以将一些本来重复性的工作自动化处理,比如用于生成java doc,或者编译时进行代码格式检查,编译时自动生成代码等等。注解的使用范围很广,使用频率也是非常高的,很多常用的三方库中都大量的使用了注解,比如ARouter、ButterKnife、Retrofit等等。了解注解的使用和语法对我们将非常有益处。

基础语法

  1. 声明:同类和接口一样,注解也是一种定义类型,我们可以通过@interface声明一个注解
public @interface TestAnnotation {
}
  1. 变量:注解中没有方法,但是可以定义变量属性,但是跟在类中定义变量还是有所区别的。
public @interface TestAnnotation {

    String name();
    int age() default 10;
}
  1. 使用:
public class Test1 {
    @TestAnnotation(age = 10,name = "abc")
    public void testAnn(){
    }
}
  1. 元注解:定义注解的注解,
    • @Retention:用于指定一条注解应该保留多长时间
      • SOURCE:表示源码级别,注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃。
      • CLASS:表示注解被保留到class文件,但jvm加载class文件时候被遗弃。
      • RUNTIME:表示注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在,可以通过反射来获取。
    • @Target:用来限制注解可以应用在哪些项上。
      • EelmentType.PACKAGE:允许作用在包上
      • EelmentType.FIELD:允许作用在字段上
      • EelmentType.METHOD:允许作用在方法上
      • EelmentType.CONSTRUCTOR:允许作用在构造函数上
      • EelmentType.PARAMETER:允许作用在方法参数上
      • EelmentType.LOCAL_VARIABLE:允许作用在本地局部变量上
      • EelmentType.ANNOTATION_TYPE:允许作用在注解上
      • EelmentType.TYPE:允许作用在类、接口和枚举上
    • @Documented:注解是否被包含在JavaDoc文档中
    • @Inherited:是否允许子类继承注解,Inherited声明的注解,在使用时用在类上,可以被子类继承,对属性和方法则无效。
  2. 内置注解
  • @Override:用于标明此方法覆盖了父类的方法。是一个典型的标记式注解,仅被编译器可知,编译器在对java文件进行编译成字节码的过程中,一旦检查到某个方法被该注解修饰,就会去查找父类中是否具有一个同样方法签名的函数,如果没有,则编译失败。

通过查看其源码,可以看到一些上面提到的元注解应用于Override中,@Target和@Retention

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
  • @Deprecated:用于标明已经过时的方法或类。
  • @SuppressWarnings:用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告。

使用注解

上边定义了一个注解TestAnnotation,但是这个注解并不完整,因为它缺少一些元注解,我们将它修改完整

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {

    String name();
    int age() default 10;
}

这是一个叫做 TestAnnotation的注解,它可以作用在方法上,它的声明周期是RUNTIME的,接下来如何应用在代码上呢?

@TestAnnotation(name = "zs",age = 20)
public void test(){
}

我们可以在方法上进行注解标记,并为其属性赋值。我们为方法添加了注解,并进行赋值,但是该如何使用这些值呢?这才是注解最终要解决的问题。单纯的赋值操作是没有用的,只有结合其他方式来操作和使用这些值才能发挥其巨大的能力。

注解&反射

通过反射获取注解信息

  1. 添加注解信息
@TestAnnotation(name = "zs",age = 20)
public void test(){
}
  1. 通过反射获取 注意:如果要通过反射获取注解信息,注解的 @Retention 一定要定义为 RetentionPolicy.RUNTIME,运行时注解,否则无法通过反射获取注解信息。
public static void main(String[] args) throws NoSuchMethodException, SecurityException {

		Class<? extends Test1> class1;
		try {
			class1 = (Class<? extends Test1>) Class.forName("fanshe.Test1");
			Method method = class1.getMethod("testAnn");
			Annotation[] annotations = method.getAnnotations();
			for(int i = 0;i<annotations.length;i++){
				System.out.println("方法注解列表="+annotations[i]+"");
			}
			TestAnnotation annotation = method.getAnnotation(TestAnnotation.class);
			String name = annotation.name();
			int age = annotation.age();
			System.out.println("name="+name+" age="+age);
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}

输出:

image.png

通过反射可以拿到为方法类型定义的注解的值,之后就可以根据拿到的属性进行不同的操作了。为何反射可以拿到这些值呢,反射是如何获取到这些属性的呢?

注解中的动态代理

注解接口

所有的注解类型都继承自一个普通的接口--Annotation,如果我们反编译一个注解,就会验证这一点。

image.png

上图就是我们反编译刚才的 TestAnnotation.class 看到的结果。很明显,我们所定义的注解其实就是一个被 interface 修饰的,继承自java.lang.annotation.Annotation的接口,它有两个抽象方法 name()age(),我们来看看 Annotation 这个接口以及其相关的一些类。 image.png

上面提到的ElementType和RetentionPolicy都在这个包下边。

AnnotationInvocationHandler

会到刚才的反射流程中,我们加一个断点,看看我们获取的到注解列表里面都是些什么。 image.png

通过这张截图,我们看到了一些熟悉的东西,Proxy1,InvocationHandler这些不都是动态代理相关的东西吗?再结合注解就是一个接口这些条件,很自然的就可以将注解和动态代理联系在一起了。

现在来看看AnnotationInvocationHandler是个什么东西。

image.png 它就是一个InvocationHandler的实现类,在动态代理中,通过

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)

传入InvocationHandler 并最终调用其 invoke 方法,并返回一个代理类的实例。这个动态代理的方法是在哪里调用呢?

image.png 就是在AnnotationParse这个类中。

注解中属性值的获取

首先从反射时获取注解信息开始分析,通过调用getAnnotation()方法调用 getDeclaredAnnotations()来获取所有的注解,之后调用 createAnnotationData()方法,

image.png

继续跟下去

image.png 在这个方法中会调用两个重要的方法

getRawAnnotation() 和 getConstantPool(),这两个方法都是native的,getRawAnnotation()获取原始批注,getConstantPool()获取常量池。

之后就会调用 AnnotationParser.parseAnnotations(),

image.png

一路跟下去,最终会调用 annotationForMap()这个方法,

image.png 也就是AnnotationInvocationHandler被调用的地方。

image.png 该方法返回的入参, 里面有整个TestAnnotation这个注解类的信息 ,这一步就获取了注解里面的值,存储在memberDefaults 这个hashmap里面 image.png

注解是将参数信息存储到class文件的常量池里面,在创建实例的时候,通过getConstantPool()获取出来。

编译时注解

除了通过反射进行注解信息的获取和修改,编译器还可以在编译期间进行一些操作,对于编译期的处理之前已经进行过一些分析 编译时注解解析及访问者模式的使用javac编译原理

简单来说,注解处理器可以看作编译器的插件,在编译期间对注解进行处理,可以对语法树进行读取、修改、添加任意元素;但如果有注解处理器修改了语法树,编译器将返回解析及填充符号表的过程,重新处理,直到没有注解处理器修改为止,每一次重新处理循环称为一个Round。

抽象处理器 AbstractProcessor

@AutoService(Processor.class)
public class MyClass extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }
}

创建一个自定义的CustomProcessor 继承 AbstractProcessor,需要重写四个方法,

  1. public synchronized void init(ProcessingEnvironment processingEnv):init方法会被注解处理工具调用,并提供实现ProcessingEnvironment接口的对象作为参数,我们可以使用这个参数获取一些工具类等:

    • Elements getElementUtils(): 返回实现Elements接口的对象,用于操作元素的工具类。
    • Filer getFiler(): 返回实现Filer接口的对象,用于创建文件、类和辅助文件。
    • Messager getMessager(): 返回实现Messager接口的对象,用于报告错误信息、警告提醒。
    • Map<String,String> getOptions() 返回指定的参数选项。
    • Types getTypeUtils(): 返回实现Types接口的对象,用于操作类型的工具类。
  2. process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment):这个方法相当于处理器的主函数,可以在这里对元素进行扫描、处理注解以及生成代码。我们可以从第一个参数 annotations 中获取到此注解处理器所要处理的注解集合,从第二个参数 roundEnvironment 中访问到当前这个Round中的语法树节点,每个节点在这里表示为一个Element。这些Element包含了java代码中最常用的元素,比如:包、枚举、类、注解、接口、字段、参数、异常、本地变量、方法等。

    通过roundEnvironment.getElementsAnnotatedWith可以获取到特定注解对应的Element

    注意在这个方法中最后返回一个boolean值,每一个注解处理器在运行的时候都是一个单例,如果不需要改变或生成语法树的内容,process()方法就可以返回一个false,通知编译器这个round中的代码没有发生改变,无需构造新的JavaCompiler实例。

  3. getSupportedAnnotationTypes():指定注解处理器是注册给哪个注解的,返回值是一个字符串的集合,包含处理器想要处理的注解类型的合法全称。也可以使用 @SupportedAnnotationTypes 注解来代替这个方法。

  4. getSupportedSourceVersion():指定所使用的java的版本,通常返回super.getSupportedSourceVersion()。 跟上边的方法一样,可以使用注解 @SupportedSourceVersion来代替。

  5. 注册注解处理器:新建res文件夹,目录下新建META-INF文件夹,目录下再新建services文件夹,在其中新建 javax.annotation.processing.Processor 文件,最后将我们的处理器的全类名放到这个文件中,这样我们的注解处理器就注册好了。当然这种方式太麻烦,可以通过谷歌的注解处理器来帮助我们生成这个文件,也就是上面的 @AutoService(Processor.class) ,但是想要使用这个插件,需要添加依赖

compile 'com.google.auto.service:auto-service:1.0-rc2'

小结

注解虽然看上去简单,但是它所包含的知识面还是很多的,从代码的编译期到运行期,不同声明周期的注解可以作用在不同的时间点上,通过注解的一些特性,可以帮助我们方便的执行一些操作,省去自己去手动编写的一些繁琐的过程。