- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
转载:JDK中注解的底层实现
用Java快三年了,注解算是一个常用的类型,特别是在一些框架里面会大量使用注解做组件标识、配置或者策略。但是一直没有深入去探究JDK中的注解到底是什么,底层是怎么实现了?于是参考了一些资料,做了一次稍微详细的分析。
参考JavaSE-8里面的JLS-9.6对注解的描述如下:
注解的声明如下:
SHELL
{InterfaceModifier} @ interface Identifier AnnotationTypeBody
接口修饰符 @ interface
注解标识符 注解类型的内容
其中:
如果注解类型与它的任何封闭类或接口具有相同的简单名称
,则编译时会出现错误。java.lang.annotation.Annotation
。既然所有注解类型的父接口都是java.lang.annotation.Annotation
,那么我们可以看一下Annotation接口的文档:
public interface Annotation
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. More information about annotation types can be found in section 9.6 of The Java™ Language Specification. The AnnotatedElement interface discusses compatibility concerns when evolving an annotation type from being non-repeatable to being repeatable.
JavaSE-8中的文档对Annotation的描述和JLS-9.6中差不多,不过最后指明了可重复注解的兼容性考虑的问题,可重复注解在JDK1.8中由元注解@Repeatable
实现。下面基于JDK8的最后一个版本java version 1.8.0_181
探究一下注解在JDK中的底层实现。
注解实现探究
我们先定义一个十分简单的Counter注解如下:
package club.throwable.annotation;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
public @interface Counter {
int count() default 0;
}
我们先从直接使用@Counter
注解,从直观上观察@Counter
实例的类型:
@Counter(count = 1)
public class Main {
public static void main(String[] args) throws Exception{
Counter counter = Main.class.getAnnotation(Counter.class);
System.out.println(counter.count());
}
}
@Counter
实例从Debug过程中观察发现是JDK的一个代理类(并且InvocationHandler
的实例是sun.reflect.annotation.AnnotationInvocationHandler
,它是一个修饰符为default的sun包内可用的类),为了验证这一点我们使用JDK的反编译命令查看@Counter
的字节码:
javap -c -v ...club\throwable\annotation\Counter.class
@Counter反编译后的字节码如下:
Classfile /D:/Projects/rxjava-seed/target/classes/club/throwable/annotation/Counter.class
Last modified 2018-10-6; size 487 bytes
MD5 checksum 83cee23f426e5b51a096281068d8b555
Compiled from "Counter.java"
public interface club.throwable.annotation.Counter extends java.lang.annotation.Annotation
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
#1 = Class #19 // club/throwable/annotation/Counter
#2 = Class #20 // java/lang/Object
#3 = Class #21 // java/lang/annotation/Annotation
#4 = Utf8 count
#5 = Utf8 ()I
#6 = Utf8 AnnotationDefault
#7 = Integer 0
#8 = Utf8 SourceFile
#9 = Utf8 Counter.java
#10 = Utf8 RuntimeVisibleAnnotations
#11 = Utf8 Ljava/lang/annotation/Retention;
#12 = Utf8 value
#13 = Utf8 Ljava/lang/annotation/RetentionPolicy;
#14 = Utf8 RUNTIME
#15 = Utf8 Ljava/lang/annotation/Documented;
#16 = Utf8 Ljava/lang/annotation/Target;
#17 = Utf8 Ljava/lang/annotation/ElementType;
#18 = Utf8 TYPE
#19 = Utf8 club/throwable/annotation/Counter
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/annotation/Annotation
{
public abstract int count();
descriptor: ()I
flags: ACC_PUBLIC, ACC_ABSTRACT
AnnotationDefault:
default_value: I#7}
SourceFile: "Counter.java"
RuntimeVisibleAnnotations:
0: #11(#12=e#13.#14)
1: #15()
2: #16(#12=[e#17.#18])
如果熟悉字节码,从直观上可以得到下面的信息:
注解是一个接口,它继承自java.lang.annotation.Annotation
父接口。@Counter
对应的接口接口除了继承了java.lang.annotation.Annotation
中的抽象方法,自身定义了一个抽象方法public abstract int count();
。
既然注解最后转化为一个接口,注解中定义的注解成员属性会转化为抽象方法,那么最后这些注解成员属性怎么进行赋值的呢?答案就是:为注解对应的接口生成一个实现该接口的动态代理类
。直接点说就是:Java通过动态代理的方式生成了一个实现了”注解对应接口
”的实例,该代理类实例实现了”注解成员属性对应的方法
”,这个步骤类似于”注解成员属性
”的赋值过程,这样子就可以在程序运行的时候通过反射获取到注解的成员属性(这里注解必须是运行时可见的,也就是使用了@Retention(RetentionPolicy.RUNTIME)
,另外需要理解JDK原生动态代理和反射相关内容)。
上面一些已经指出了,注解的最底层实现就是一个JDK的动态代理类,而这个动态代理类的生成过程我们完全可以通过Debug跟踪,这里列举一下笔者跟踪整个过程的流水账:
1、Class<?>#getAnnotation(Class<A> annotationClass)
,通过类型获取注解实例。
2、Class<?>#annotationData()
,获取注解的数据。
3、Class<?>#createAnnotationData(int classRedefinedCount)
,构建注解的数据。
4、AnnotationParser#parseAnnotations(byte[] var0, ConstantPool var1, Class<?> var2)
,这里已经是sun包下的类,无法看到源码,这个方法用于解析注解,这一步使用到字节码中常量池的索引解析,常量解析完毕会生成一个成员属性键值对作为下一个环节的入参,常量池的解析可以看AnnotationParser#parseMemberValue
方法。
5、AnnotationParser#annotationForMap(final Class<? extends Annotation> var0, final Map<String, Object> var1)
,同样是sun.reflect.annotation.AnnotationParser
中的方法,用于生成注解的动态代理类。
注意第5步,贴出它的源码:
JAVA
public static Annotation annotationForMap(final Class<? extends Annotation> var0, final Map<String, Object> var1) {
return (Annotation)AccessController.doPrivileged(new PrivilegedAction<Annotation>() {
public Annotation run() {
return (Annotation)Proxy.newProxyInstance(var0.getClassLoader(), new Class[]{var0}, new AnnotationInvocationHandler(var0, var1));
}
});
}
熟悉JDK动态代理的这里的代码应该看起来很简单,就是生成一个标准的JDK动态代理类,而InvocationHandler
的实例是AnnotationInvocationHandler
,可以看它的成员变量、构造方法和实现InvocationHandler
接口的invoke
()方法:
JAVA
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
//保存了当前注解的类型
private final Class<? extends Annotation> type;
//保存了注解的成员属性的名称和值的映射,注解成员属性的名称实际上就对应着接口中抽象方法的名称
private final Map<String, Object> memberValues;
private transient volatile Method[] memberMethods = null;
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
public Object invoke(Object var1, Method var2, Object[] var3) {
//获取当前执行的方法名称
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
byte var7 = -1;
switch(var4.hashCode()) {
case -1776922004:
if (var4.equals("toString")) {
var7 = 0;
}
break;
case 147696667:
if (var4.equals("hashCode")) {
var7 = 1;
}
break;
case 1444986633:
if (var4.equals("annotationType")) {
var7 = 2;
}
}
switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
//利用方法名称从memberValues获取成员属性的赋值
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
//这一步就是注解成员属性返回值获取的实际逻辑
//需要判断是否数据,如果是数据需要克隆一个数组
//不是数组直接返回
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}
return var6;
}
}
}
}
//忽略其他方法
这里需要重点注意一下的是:AnnotationInvocationHandler
的成员变量Map<String, Object> memberValues
存放着注解的成员属性的名称和值的映射,注解成员属性的名称实际上就对应着接口中抽象方法的名称,例如上面我们定义的@Counter
注解生成代理类后,它的AnnotationInvocationHandler
实例中的memberValues
属性存放着键值对count=1
。
既然知道了注解底层使用了JDK原生的Proxy,那么我们可以直接输出代理类到指定目录去分析代理类的源码,有两种方式可以输出Proxy类的源码:
1、通过Java系统属性设置System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");。
2、通过-D参数指定,其实跟1差不多,参数是:-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
。
这里使用方式1,修改一下上面用到的main方法:
JAVA
public static void main(String[] args) throws Exception {
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
Counter counter = Main.class.getAnnotation(Counter.class);
System.out.println(counter.count());
}
执行完毕之后,项目中多了一个目录:
其中$Proxy0是@Retention
注解对应的动态代理类,而$Proxy1
才是我们的@Counter
对应的动态代理类,当然如果有更多的注解,那么有可能生成$ProxyN
。接着我们直接看$Proxy1
的源码:
JAVA
public final class $Proxy1 extends Proxy implements Counter {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m4;
private static Method m0;
public $Proxy1(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int count() throws {
try {
return (Integer)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final Class annotationType() throws {
try {
return (Class)super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("club.throwable.annotation.Counter").getMethod("count");
m4 = Class.forName("club.throwable.annotation.Counter").getMethod("annotationType");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
显然,$Proxy1
实现了Counter
接口,它在代码的最后部分使用了静态代码块实例化了成员方法的Method
实例,在前面的代码对这些Method
进行了缓存,在调用成员方法的时候都是直接委托到InvocationHandler(AnnotationInvocationHandler)
实例完成调用。我们在分析AnnotationInvocationHandler
的时候看到,它只用到了Method
的名称从Map从匹配出成员方法的结果,效率近似于通过Key从Map实例中获取Value一样,是十分高效的。
既然知道了注解的底层原理,我们可以编写一个”注解接口”和InvocationHandler
实现来简单模拟整个过程。先定义一个接口:
JAVA
public interface CounterAnnotation extends Annotation {
int count();
}
InvocationHandler的简单实现:
JAVA
public class CounterAnnotationInvocationHandler implements InvocationHandler {
private final Map<String, Object> memberValues;
private final Class<? extends Annotation> clazz;
public CounterAnnotationInvocationHandler(Map<String, Object> memberValues, Class<? extends Annotation> clazz) {
this.memberValues = memberValues;
this.clazz = clazz;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Object value;
switch (methodName) {
case "toString":
value = super.toString();
break;
case "hashCode":
value = super.hashCode();
break;
case "equals":
value = super.equals(args[0]);
break;
case "annotationType":
value = clazz;
break;
default:
value = memberValues.get(methodName);
}
return value;
}
}
编写一个main方法:
JAVA
public class CounterAnnotationMain {
public static void main(String[] args) throws Exception{
//这里模拟了注解成员属性从常量池解析的过程
Map<String,Object> values = new HashMap<>(8);
values.put("count", 1);
//生成代理类
CounterAnnotation proxy = (CounterAnnotation)Proxy.newProxyInstance(CounterAnnotationMain.class.getClassLoader(),
new Class[]{CounterAnnotation.class},
new CounterAnnotationInvocationHandler(values, CounterAnnotation.class));
System.out.println(proxy.count());
}
}
//运行后控制台输出:1
注解@CrossOrigin 出于安全原因,浏览器禁止Ajax调用驻留在当前原点之外的资源。例如,当你在一个标签中检查你的银行账户时,你可以在另一个选项卡上拥有EVILL网站。来自EVILL的脚本
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界. 这篇CFSDN的博客文章深入理解Java高级特性——注解由作者收集整理,如果你对这篇文章有兴趣,
概述 在这个快速教程中,我们将探索 Spring 的 @RequestParam 注解。 简单地说,我们可以使用 @RequestParam 从请求中提取查询参数、表单参数甚至文件。我们将讨论如何使用
我有一个关于 Spring @Async 注释的问题。我有一个 Controller 自动连接了一个服务(GnInsuranceDetailsService) @RequestMapping(va
我在使用注释来调用注释所属的方法时遇到了一些麻烦......我将举一个例子: class MyEventHandlers { @handler(id=“1”) public doSom
我是.Net程序员,但是这次我正在从事Java项目,并且遇到了一些困难。这个 java 项目不是我的,它是由其他开发人员开发的,并且使用 Hibernate。 当我运行 Ant 构建器时,我收到此错误
我在 Grails 文档(第 9 章:测试)中读到过这个注解。但是我不明白这是什么... 问题是我需要模拟 GORM 的动态方法,有一种方法可以自动模拟它们,而不必编写我需要的所有方法吗? 最佳答案
这个问题在这里已经有了答案: How to get annotation class name, attribute values using reflection (2 个答案) 关闭 5 年前。
如何了解 Java EE 6 JMS 注释规范支持的所有有效属性集@ActivationConfigProperty Java EE 6 Documentation for @ActivationCo
我认为这是不可能的,但也许我错了。所以我问你,如果可能的话。 ;-) 如果我定义了一个注释,它只接受类引用,它扩展了一些可能的接口(interface)或类: Class serviceIFProv
我正在尝试使用 javax.validation 验证一些 DTO,但似乎注释 @NotEmpty 没有检查参数是否为 null。 这是我的类(class): Person.class public
我是 hibernate 新手,我正在尝试学习它,但在尝试使一对多关系正常工作时遇到了问题。我尝试了几个例子,但似乎没有一个起作用。 有人可以看一下下面的代码并告诉我哪里出了问题吗?我尝试了很多不同的
这个问题已经有答案了: Why doesn't Java offer operator overloading? (17 个回答) 已关闭 9 年前。 每个人都知道 Java 中的简单算术如何用于基元
有人知道如何用 Python 处理这种 XML 注释,这是我第一次看到。 <?link id="752760" resource-uuid="UUID-9f0575a3-1847-1cde-fd
我遇到了这个link这解释了如何继承 bean。假设此示例中的 HelloWorld 类使用 @Component 注释作为 bean 公开,如何创建另一个继承此 bean 的 bean?我可以使用
谁能告诉我这段代码是否: public class OvTester { @Override public int hashCode() { return toStri
我有一个实体,它有一个非键列,我已将其设置为在我的数据库中自动生成。 我不能使用 @GeneratedValue,因为据我所知,它仅适用于关键字段。 在这种情况下,如何指示非键列是自动生成的? 最佳答
所以可能像很多人一样,我通常会临时注释掉代码,主要是为了调试目的。我目前放了类似 **DEBUG** 或任何容易搜索的内容,但我认为让编译器在发现临时注释掉的代码时输出警告(甚至错误)可能很有用。我想
此组件解决的问题是: 「谁」在「什么时间」对「什么」做了「什么事」 本组件目前针对 Spring-boot 做了 Autoconfig,如果是 SpringMVC,也可自己在 xml 初始化 b
配置全局乱码过滤器 参数绑定注解@RequestParam 注解@RequestParam的参数使用说明 获得Restful风格的参数 自定义类型转换器 自定义转换器的开发步骤: 获得Servlet相
我是一名优秀的程序员,十分优秀!