第一部分:前言
本文目标:
- 了解字节码中注解部分的结构和它们的含义
- 了解注解运行的原理
- 追踪注解定义时候的方法为什么可以像属性一样使用,它怎么做到的
- 追踪注解中代理类的样子和生成时机
阅读本文前请先理解注解的相关知识,前置文章:初始Java中的注解
java中注解的保留时期
-
保留到运行时时期。行时时期的注解可以通过反射进行分析和使用,
-
保留到字节码时期。
-
保留到源文件时期。源文件时期的代码是给编译器看的。
注解在类文件结构里的定义
在Class类的字节码文件结构的中,jdk1.5在引入四种关于注解的属性表,它们分别是:
RuntimeVisibleAnnotations属性表
- 使用位置:类、方法表、字段表
- 说明:jdk1.5新增。指明哪些注解运行时可见,即运行时可以被反射并使用
RuntimeInvisibleAnnotations属性表
- 使用位置:类、方法表、字段表
- 说明:jdk1.5新增。指明哪些注解运行时不可见。
RuntimeVisibleParameterAnnotations属性表
- 使用位置:方法表
- 说明:该注释作用于方法参数,表明哪些注释运行时可见。
RuntimeInvisibleParameterAnnotations属性表
- 使用位置:方法表
- 说明:该注释作用于方法参数,表明哪些注释运行时不可见。
本文中我将结合RuntimeVisibleAnnotations属性表和具体代码,通过对注解底层源码进行跟踪和分析得出前文中提出的问题的解答。
- 为什么注解是接口,但是使用的时候它的方法却称作注解的属性或者元素?
- 使用的方式也很特别,为什么是方法名对应赋值而不是像普通类一样使用属性赋值
- 基于问题2,为什么注解是个接口却能当做对象使用呢?
阅读本文需要你具备以下知识
- 反射
- 动态代理
- 了解Class类文件结构
如果没有探究过Class类文件结构也可以不用在意,我在解读注解的源码时,会对注解属性表解析的部分进行对应说明。
第二部分:RuntimeVisibleAnnotations属性表
下面我将对RuntimeVisibleAnnotations
属性表结构做个简单解读,为了不要被扰乱思路,可以先不看这部分内容,等后面源码解读的时候再结合这部分的内容一起阅读更好。
RuntimeVisibleAnnotations
属性表是一个可变长度的属性表,它在字节码中的定义的标准结构如下:
RuntimeVisibleAnnotations_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 num_annotations;
annotation annotations[num_annotations];// annotation表的解析在
本文中使用到的RuntimeVisibleAnnotations
属性表来源JAVA虚拟机规范:https://docs.oracle.com/javase/specs/index.html
1 attribute_name_index
:
这是一个指向字节码内常量池(constant_pool table)的索引,该索引指向的内容表示这个RuntimeVisibleAnnotations
2 attribute_length
:
表示这个注解属性表的长度
3 num_annotations
:
表示当前注解属性表内一共有多少个注解,比如:类上使用了3个注解,那么这个值就是3
4 annotations[]
:
表示一个注解表,它内部有num_annotations
个注解,每注解的结构如下:
annotation
:
annotation {
u2 type_index;
u2 num_element_value_pairs;
{
u2 element_name_index;
element_value value;
} element_value_pairs[num_element_value_pairs];
}
4.1 type_index
:
是一个field描述符,它表示上面annotation结构
所表示的注解它的类型。
4.2 num_element_value_pairs
:
表示当前注解中存在的元素-值
(键值对)的个数,每一个方法就是一个注解的元素。
- 元素:是注解中定义的方法
- 值:编写Java源代码的过程中,使用注解时赋的值
注意,注解中的方法和代码源文件中对注解的赋值在这里被组织成键值对啦!所以,跟踪代码的时候会发现它们被放进了一个Map中,每个注解都会有一个Map对象来放置它里面的所有元素-值
键值对。
4.3 element_value_pairs[]
:
表示注解中一个元素-值
键值对,它的结构如下:
element_value_pairs{
u2 element_name_index;
element_value value;
}
4.3.1 element_name_index
:
4.3.2 element_value
:
表示注解中的值,它的的结构由element_value
表示:
element_value
:
element_value {
u1 tag;
union {
u2 const_value_index;
{
u2 type_name_index;
u2 const_name_index;
} enum_const_value;
u2 class_info_index;
annotation annotation_value;
{
u2 num_values;
element_value values[num_values];
} array_value;
} value;
}
4.3.2.1 tag
:
使用一个Assic码来表示键值对中值
的类型。tag表示的字符对应一种数据类型,下面是字符和类型的映射表:
value使用一个union来表示。注解属性的值可以是以下几种:
- 基础数据类型和String类型
- 枚举类型
- viod类型
- 注解类型
- 以及以上类型的元素组成的数组
4.3.2.2 const_value_index:
这是一个指向字节码内常量池(constant_pool table)的索引,该索引指向的内容表示注解元素-值
键值对中的值
是String类型还是基础数据类型的,它必须满足tag中提供的类型中的一种。
4.3.2.3 enum_const_value:
表示注解元素-值
键值对中的值
是枚举类型的,它包含如下两个内容:
- type_name_index:这是一个指向字节码内常量池(constant_pool table)的索引,该索引指向的内容是一个属性描述符,它表示enum类型的内部形式的二进制名字。
- const_name_index:这是一个指向字节码内常量池(constant_pool table)的索引,该索引指向的内容表示enum的简单名。
4.3.2.4 class_info_index:
表示注解元素-值
键值对中的值
是class字面量类型的,也就是值是XXX.class
- 如果是基础数据类型,返回的是基础数据类型对应的字符比如:
int.class,返回的就是I
- 如果是Array类型、类或者接口,则返回ObjectType或者ArrayType,比如
int类型的数组,返回的是[I
- 如果是Void,比如:
void.class,则返回的是V
4.3.2.5 annotation_value:
表示注解元素-值
键值对中的值
是注解类型的。如果是注解类型,那么这个annotation_value
的结构就是前面介绍过的的annotation
的结构,见4
。
4.3.2.6 array_value:
表示注解元素-值
键值对中的值
是数组类型的。
- num_values:数组元素的个数
- values[]:数组。它的元素的结构由element_value表示,见上面的
4.3.2
第三部分:进入正题
Step1: 自定义一个注解,并使用它
Step1.1 定义一个StudentDate注解
我们让它的可用范围为TYPE,保留到运行时,保留到运行时的注解才能被反射使用。
@Target({
ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface StudentDate {
String className();
String methodName();
}
Step1.2 创建一个Student类
后面要通过注解和反射生成它的对象并调用它的show方法
public class Student {
void show(){
System.out.println("this is the show method.");
}
}
Step1.3 通过@StudentDate注解,创建Student对象并调用它的show方法
@StudentDate(className = "com.teresa.annotationdemo.Student",methodName = "show")
public class AnnotationDemo {
public static void main(String[] args) throws Exception {
//设置vm参数,让动态生成的代理类能被保存成文件
System.getProperties().setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
//获取StudentDate这个注解的类和方法
StudentDate stu = AnnotationDemo.class.getAnnotation(StudentDate.class);
String className = stu.className();//打个断点
String methodName = stu.mathodName();
Class studentClazz = Class.forName(className);
Method method = studentClazz.getDeclaredMethod(methodName);
method.invoke(new Student());
}
}
//结果:
this is the show method.
OK,使用StudentDate注解的实现部分已完成,我们看一下系统生成的动态代理类是什么样的?
Step2:查看系统生成的动态代理类
查看在代码根目录生成的jdk文件夹中找到系统生成的动态代理类:
public final class $Proxy1 extends Proxy implements StudentDate {
private static final Method m2;
private static final Method m3;
public $Proxy1(InvocationHandler var1) {
super(var1);
}
//略...
public final String className() {
try {
//调用className,m3是Method对象,代表的是className方法
return (String)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String methodName