注解进阶篇(注解原理及解析)

3,049 阅读5分钟

注解原理及解析

所有的注解类型都继承自这个普通的接口(Annotation)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

本质上就是:

public interface Override extends Annotation{
    
}

注解只是一种注释,需要解析它的代码,才会起作用

解析一个注解有两种形式,一种是运行期反射解析,一种是编译期直接的直接扫描

典型的就是注解 @Override,一旦编译器检测到某个方法被修饰了 @Override 注解,编译器就会检查当前方法的方法签名是否真正重写了父类的某个方法,也就是比较父类中是否具有一个同样的方法签名。如果不是那就报错

解析注解

形象的比喻就是你把这些注解标签在合适的时候撕下来,然后检阅上面的内容信息,并做出某些行为。

运行期注解的解析

运行时 Annotation 指 @Retention 为 RUNTIME 的 Annotation

运行期注解的解析通过是反射的方式,Class相关方法如下:

public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}
判断它是否应用了某个注解

public A getAnnotation(Class annotationClass) {}
获取 Annotation 对象,返回注解到这个元素上的所有注解。

public Annotation[] getAnnotations() {} 获取 Annotation 对象,返回注解到这个元素上的所有注解。

举个例子:

定义Test注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
	
	int id() default -1;
	
	String msg() default "";

}

使用注解,并通过反射解析注解

@Test(msg="hhh")
public class AnnotationTest {
	
	public static void main(String[] args) {
		
		boolean hasAnnotation = AnnotationTest.class.isAnnotationPresent(Test.class);
		
		if ( hasAnnotation ) {
			Test testAnnotation = AnnotationTest.class.getAnnotation(Test.class);
			
			System.out.println("id:"+testAnnotation.id());
			System.out.println("msg:"+testAnnotation.msg());
		}

	}

}

它们的结果如下:

id:-1
msg:hhh

原理分析

AnnotationTest.class.getAnnotation(Test.class)获取注解声明的值,从上面的句子就可以看出,它是从class结构中获取出AnnotationTest注解的,所以肯定是在某个时候注解被加入到class结构中去了。

@TestAnnotation()
public class AnnotationTest {
}

从java源码到class字节码是由编译器完成的,编译器会对java源码进行解析并生成class文件,而注解也是在编译时由编译器进行处理,编译器会对注解符号处理并附加到class结构中,根据jvm规范,class文件结构是严格有序的格式,唯一可以附加信息到class结构中的方式就是保存到class结构的attributes属性中。我们知道对于类、字段、方法,在class结构中都有自己特定的表结构,而且各自都有自己的属性,而对于注解,作用的范围也可以不同,可以作用在类上,也可以作用在字段或方法上,这时编译器会对应将注解信息存放到类、字段、方法自己的属性上。

在我们的AnnotationTest类被编译后,在对应的AnnotationTest.class文件中会包含一个RuntimeVisibleAnnotations属性,由于这个注解是作用在类上,所以此属性被添加到类的属性集上。即Test注解的键值对id=-1、msg="hhh"会被记录起来。而当JVM加载AnnotationTest.class文件字节码时,就会将RuntimeVisibleAnnotations属性值保存到AnnotationTest的Class对象中,于是就可以通过AnnotationTest.class.getAnnotation(Test.class)获取到Test注解对象,进而再通过Test注解对象获取到Test里面的属性值。

这里可能会有疑问,Test注解对象是什么?其实注解被编译后的本质就是一个继承Annotation接口的接口,所以@Test其实就是“public interface Test extends Annotation”,当我们通过AnnotationTest.class.getAnnotation(Test.class)调用时,JDK会通过动态代理生成一个实现了Test接口的对象,并把将RuntimeVisibleAnnotations属性值设置进此对象中,此对象即为Test注解对象,通过它的id()方法就可以获取到注解值。

参考:注解机制及其原理

那么通过动态代理生成实现了Test接口的对象具体说怎么样的呢?

动态代理

  • $Proxy0就是通过 Proxy 动态生成的。
  • $Proxy0实现了要代理的接口。
  • $Proxy0通过调用 InvocationHandler来执行任务。

通过Proxy.newProxyInstance反射拿到动态代理,因为动态代理实现了代理接口方法,所以可以调用代理接口调用代理接口的方法,(内部是调用InvocationHandler.invate()反射调用代理接口的方法。)

详情见轻松学,Java 中的代理模式及动态代理

注解生成的动态代理

注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。

通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。

memberValues是一个hashmap,这个map以key(注解方法名)—value(注解方法对应的值)

分析见Java注解(Annotation)原理详解

编译期注解的解析

APT

APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。 简单来说就是在编译期,通过注解生成.java文件。

所以说编译期注解的解析,由Annotation Processor 注解处理器完成

编译时注解的核心依赖APT(Annotation Processing Tools)实现,对应的处理流程为:

  • 在某些代码元素上(如类型、函数、字段等)添加注解;
  • 编译时编译器会检查AbstractProcessor的子类,
  • 然后将添加了注解的所有元素都传递到该类的process函数中;
  • 使得开发人员可以在编译器进行相应的处理。

所以我们需要写一个AbstractProcessor的实现类,由于所有被注解过的元素经过注解处理器扫描之后被封装为Element元素,所以通过Elemenet,使用StringBuilder来生成对应的Java代码。也可以使用javapoet生成代码。

由于篇幅问题,独立开一篇文章,详情见APT详解

扩展

动态代理和静态代理

注解在class字节码里的结构

www.cnblogs.com/chanshuyi/p…