参考文章
- (掘金) JAVA 注解的基本原理
- Java Language Specification 中的 9.6. Annotation Types
主要结论
- 注解(
annotation
)是一种特殊的接口(interface
) - 注解中可以定义方法,方法被执行时,动态代理机制会生效
项目代码
完整的代码在 basic-proxy-annotation 中
正文
前文 讨论了动态代理的一个小例子。
其实动态代理的应用场景还是有很多的,例如注解中的一些功能就是借助动态代理实现的。
本文会简单讨论动态代理在注解中的使用。
注解是一种特殊的接口
在 java.lang.annotation.Annotation
类的 javadoc
中可以看到这样的描述
The common interface extended by all annotation types. Note that an
interface that manually extends this one does not define
an annotation type. Also note that this interface does not itself
define an annotation type.
大致就是说所有的注解都是 java.lang.annotation.Annotation
的子类,
但是如果直接手写一个 java.lang.annotation.Annotation
的子类,
那么这样的子类并不会被当作注解。
Java Language Specification 中的 9.6. Annotation Types 提到
An annotation type declaration specifies a new annotation type, a special kind of interface type.
可见注解也是接口(interface
)。
不过注解和普通的接口还是有明显不同的。
按照我的理解,注解和普通接口的关系就像一些小说里主角和配角的关系。
在这些小说里,配角遇到倒霉事多半只能认栽,而主角则时常有好运伴随,总能逢凶化吉。
普通接口并不能用来修饰其他元素,而注解就可以(是不是也可以看作一种主角光环呢?)。
引入 juni4 的 jar
包后,
我们可以用 @Test
注解来修饰需要测试的方法。
我在 HelloWorldTest.java 中写了如下代码
package com.study.proxy.test;
import org.junit.Test;
public class HelloWorldTest {
@Test
public void testHelloWorld() {
System.out.println("Hello world");
}
}
在这段代码里,testHelloWorld()
方法是被 @Test
注解修饰的。
但是我们不能用普通的接口(例如 java.util.List
)来修饰 testHelloWorld()
方法
关于注解也是一种接口,
还可以多解释一些(没兴趣的读者可以略过这一段)。
下一小节会有一个注解 Decent.java 出现,
这里我们先征用一下。
在当前目录下执行如下命令
mvn clean compile
然后利用 javap
命令查看 Decent.java 对应的字节码的结构。
具体的命令如下
javap -v -p -cp target/classes/ com.study.proxy.annotation.Decent
在红框部分可以看到,
Decent
确实是一个 interface
(字节码文件的结构又是一个比较大的话题,作者的相关知识也比较有限,这里就不展开了)。
动手写注解
参考 倚天屠龙记 中的设定,
江湖上有些门派以名门正派自居,
而明教则常被这些名门正派视为异类。
我们可以把 名门正派 看成一种标签,
可以用注解模拟这种打标签的逻辑。
我们就写一个名为 @Decent
的注解来描述这种标签吧。
其完整代码在 Decent.java 中。
这个类的主要代码如下
/**
* 用于描述名门正派的相关信息的注解
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Decent {
/**
* 是名门正派吗?
*
* @return 是则返回 true, 否则返回 false (默认值是 true)
*/
boolean yes() default true;
/**
* 活跃地区
*
* @return 活跃地区(默认值是 中华)
*/
String region() default "中华";
}
我们定义三个类,给它们都加上 @Decent
注解
- 峨眉派: Emei.java
- 崆峒派: Kongtong.java
- 明教: Ming.java
以 Emei.java
为例,其主要代码如下
@Decent(region = "四川")
public class Emei {
}
我们再写一个简单的 BasicAnnotation
类,其主要代码如下(BasicAnnotation.java
的完整代码在 BasicAnnotation.java 中)
public class BasicAnnotation {
public static void main(String[] args) {
// 获取 Decent 的实例
Decent decent = Emei.class.getAnnotation(Decent.class);
// 调用这个实例的 region() 方法
System.out.println(decent.region());
}
}
注解是如何被动态代理支持的?
不出意外的话,BasicAnnotation
类中的 main
方法运行完之后,会有
四川
展示出来
上述代码中主要做了两件事
- 获取
Decent
的实例 - 调用这个实例的
region()
方法
既然注解本质上也是接口,那么这里就有个问题,Decent
中定义的 region()
方法是谁实现的呢?
比如我们在某些代码里调用 java.util.List
中的 get(int)
方法时,
总归需要有某个子类来实现这个方法吧。
现在的问题也是类似的。
如果这里涉及动态代理的话,
那么照理说应该会用到 java.lang.reflect.Proxy
这个类(相关内容可以参考 前文)。
我们在 java.lang.reflect.Proxy
中加个断点(简称为 断点A)
然后 debug
BasicAnnotation
类的 main
方法
这时候会遇到 断点A。
我们在这观察一下,会发现 h
的类型是 sun.reflect.annotation.AnnotationInvocationHandler
那我们再到 sun.reflect.annotation.AnnotationInvocationHandler
这个类中观察一下。
我在第 38 行加了 断点B。
我们重新 debug
一次。
第一次运行到 断点B 时,和 @Rentention
注解有关。
在 37 行看到入参的名称分别是 var1
和 var2
。
我们观察一下 var1
,看来它和注解的实际类型对应
再观察一下 var2
,看来它和注解中定义的方法有关
这里有点绕。
var2
是一个 Map
,它的 size
是 1,也就是说其中只有 1 个 kv 对。
这个唯一的 kv 对信息如下。
-
k
: 是一个String
,内容为value
(就是v
a
l
u
e
这 5 个char
组成的字符串) -
v
: 是一个java.lang.annotation.RetentionPolicy
类型的枚举值。
当第 41 行执行过之后
var2
对应的 Map
就被 this.memberValues
引用了。
我们 Resume Program
,
然后会第二次来到断点B,
此时 var1
和 Decent
对应
而 var2
对应的 Map
这次则包含了两个 kv
对
- 其中的一个
kv
对:"region"
->"四川"
- 另一个
kv
对:"yes"
->true
到这里,
可以猜测一下,
注解中的方法名以及对应的值会被保存在 var2
中,
并最终被赋给 this.memberValues
。
然后我们来试试在其他地方打断点。
在 BasicAnnotation.java 的 main
方法中,
Decent
注解的 region
方法会被调用。
既然这里涉及动态代理,
打蛇打七寸,擒贼先擒王,
我们直接去 sun.reflect.annotation.AnnotationInvocationHandler
类的 invoke
方法里加上 断点C。
断点C 的具体位置如下图所示。
再次 debug
BasicAnnotation.java 的 main
方法,
程序会运行到 断点C 这里
在 断点C 这里,会执行如下语句
String var4 = var2.getName();
var2
是一个 java.lang.reflect.Method
,
这个 Method
和 Decent
注解中的 region
方法对应。
所以 var4
的值将会变为 region
。
继续执行,我们会看到程序执行到了 81 行(从 48 行到 81 行其实还是有一些逻辑的,这里跳过了,读者如果有兴趣可以自行打断点观察详情)
这时候 this.memberValues
的作用就体现出来了,
这个 Map
中与 "region"
对应的值就是 "四川"
。
现在我来概括一下整体的逻辑。
注解被动态代理支持的整体逻辑
如果一个 注解A 中定义了 N
个方法(这里先不考虑 toString
hashCode
等特殊方法),
那么这 N
个方法以及这 N
个方法的返回值可以组合成 N
个 kv
对。
在生成与 注解A 对应的 sun.reflect.annotation.AnnotationInvocationHandler
的实例时,
这 N
个 kv
对会被保存到 sun.reflect.annotation.AnnotationInvocationHandler
的
this.memberValues
里。
当我们调用这 N
个方法中的任意一个时,
都会被转化为对 sun.reflect.annotation.AnnotationInvocationHandler
中名为 invoke
方法的调用。
在这个 invoke
方法中,
通过对 this.memberValues
进行查询,
就能得知对应的值,
将这个值返回即可。