Java 注解的实现原理
注解的本质
在 java.lang.annotation.Annotation
接口中有这样的描述:
The common interface extended by all annotation interfaces.
大致意思就是所有的注解接口都继承自该 Annotaion
接口
假设现在我们编写了一个新的注解 ReadAuth
,该注解的目的是标记那些读取数据需要权限的操作,如下所示:
public @interface ReadAuth {
}
现在,编译这个注解类,然后通过 javap
命令查看反编译之后的结果:
Compiled from "ReadAuth.java"
public interface com.example.eamples.annotations.ReadAuth extends java.lang.annotation.Annotation {
}
可以看到,注解的本质是一个继承了 java.lang.annotation.Annotation
接口的接口类
注解是元数据的一种提供形式,提供不属于程序本身的数据,相当与给某个程序区域打上标签。
然而,如果使用 Spring 开发项目的话,经常会见到使用注解就能完成许多任务的情况,如:通过 @Controller
定义控制器、@RequestMapping
定义请求 url
等。这些注解本质上也只是一个标记的作用,具体功能的实现是通过 Spring 来解析这些注解来实现
解析注解有两种方式:一是在编译阶段扫描注解,二是在运行期间通过反射的方式来获取相关的注解信息。第一种方式要求编译器能够检测到合法的注解,由于编译器一般情况下没有办法修改它们的行为,因此对于用户或者框架自定义的注解,都需要通过反射的方式来获取注解的元数据信息
元注解
“元注解” 是 JDK
中内置的几种用于修饰注解的注解。通常在注解的定义上能够看到这些注解,如常见的方法重写注解 @Override
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
其中,@Target
和 @Retention
注解就是 JDK
中内置的元注解,表示自定义的注解应该作用的代码范围和保留时间段
JDK
中存在以下几个元注解:
@Retention
:@Retention
注解指定标记的注解的存储方式,有以下三种存储方式:RetentionPolicy.SOURCE
:标记的注解仅保留在源代码级别,并被编译器忽略RetentionPolicy.CLASS
:标记的注释在编译时由编译器保留,但被 Java 虚拟机忽略(即类加载阶段忽略)RetentionPolicy.RUNTIME
:标记的注解由 JVM 保留,因此它可以被运行时环境使用
@Documented
:@Documented
注解表示无论注解的存储方式如何,这些注解都能够使用javadoc
工具生成到文档中(默认情况下,注解将不会被包括到javadoc
生成的文档中)@Target
:@Target
注解标记另一个注解,以限制该注解可以应用于哪些 Java 元素。@Target
可以指定以下元素类型的一个或多个作为其值:ElementType.ANNOTATION_TYPE
表示该注解的作用范围为注解ElementType.CONSTRUCTOR
作用于构造函数ElementType.FIELD
作用于字段或者属性ElementType.LOCAL_VARIABLE
作用于局部变量ElementType.METHOD
作用于方法级别ElementType.PACKAGE
作用于包声明ElementType.PARAMETER
作用于一个方法的参数ElementType.TYPE
作用于一个类的任意元素(该类可以是一般类、接口或枚举)
@Inherited
:@Inherited 注解表示注解类型可以继承自父类(默认情况下不可以继承)。当用户查询注解类型并且类没有该类型的注解时,查询该类的父类的注解类型。 该注解仅适用于类声明。@Repeatable
:@Repeatable
注解,在 Java SE 8 中引入,表示标记的注解可以多次应用于同一个声明或类型使用。
JDK 预定义注解
在 JDK 1.8 中,预先定义了以下几种注解:
@Deprecated
:@Deprecated
注解表示标记的元素已被弃用,不应再使用。每当程序使用带有 @Deprecated 注释的方法、类或字段时,编译器都会生成警告。@Override
:@Override
注释通知编译器该元素将要重写在父类中声明的元素。虽然重写方法时不需要使用此注释,但它有助于防止错误。 如果标有@Override
的方法未能正确覆盖其父类之一中的方法,则编译器会生成错误。@SuppressWarnings
:@SuppressWarnings
注释告诉编译器抑制它将生成的警告。每个编译器警告都属于一个类别。 Java 语言规范列出了两个类别:弃用和未选中。@SafeVarargs
:@SafeVarargs
注释,当应用于方法或构造函数时,断言代码不会对其 varargs 参数执行潜在的不安全操作。 使用此注释类型时,与可变参数使用相关的未经检查的警告将被禁止。@FunctionalInterface
:@FunctionalInterface
注解,在 Java SE 8 中引入,表示类型声明旨在成为 Java 语言规范所定义的功能接口
注解与反射
在 Java 虚拟机规范中,定义了一系列和注解相关的属性表,也就是说,无论是字段、方法还是类,如果被注解修饰了,那么就可以写入到对应的字节码文件。对应的属性表有以下几种:
RuntimeVisibleAnnotations
:运行时可见的注解RuntimeInVisibleAnnotations
:运行时不可见的注解RuntimeVisibleParameterAnnotations
:运行时可见的方法参数注解RuntimeInvisibleParameterAnnotations
:运行时不可见的方法参数注解AnnotationDefault
:注解类元素的默认值
由于在 Class
文件中存在这些属性,因此对于一个类或者接口来说,相关类的Class
对象能够提供以下几种和注解交互的方法:
getAnnotation
:返回指定的注解isAnnotationPresent
:判断当前的元素是否被指定的注解修饰过getAnnotations
:返回该元素上的所有注解getDeclaredAnnotation
:返回本元素的指定注解getDeclaredAnnotations
:返回本元素的所有注解,不包括从父注解继承来的注解
接下来,让我们看看 JDK 是如何获取到相关的注解的
依旧以前面提到的 @ReadAuth
为例,下面是自定义的 @ReadAuth
的定义:
import java.lang.annotation.*;
@Inherited
@Documented
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadAuth {
}
然后编写下面的示例来获取方法的注解:
import java.lang.reflect.Method;
public class TestReadAuth {
@ReadAuth
static void readTest() {
System.out.println("Read Auth Test");
}
static {
/*
JDK 8 及其i之前的版本需要设置 sun.misc.ProxyGenerator.saveGeneratedFiles 属性为 true,JDK 8 之后版本
则需要设置 jdk.proxy.ProxyGenerator.saveGeneratedFiles 属性为 true,具体可以查看 ProxyGenerator 的saveGeneratedFiles 定义的属性
配置这个属性的目的在于保存在程序运行过程中生成的 Proxy 对象,
假设获取注解的过程是通过代理的方式来实现的,通过配置该属性就能够保存中间的代理对象
*/
// System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
}
public static void main(String[] args) throws NoSuchMethodException {
Class<?> cls = TestReadAuth.class;
Method method = cls.getDeclaredMethod("readTest"); // 通过反射获取类的方法
ReadAuth readAuth = method.getAnnotation(ReadAuth.class); // 获取方法上的注解
}
}
运行这段代码,会发现在项目的根目录下看到类似下图所示的代理类:
如果没有看到这些,那么请尝试移除当前项目中的其它依赖(如 Spring),这些依赖项目的存在很有可能会导致相关属性的配置失效
通过发现这些 Proxy,可以大致推断注解的获取极有可能是通过代理的方式来实现的,反编译查看生成的 Proxy 类,关键的 Proxy 是实现 ReadAuth
接口的 Proxy,构造函数部分如下:
关键的部分就是使用 InvocationHandler
参数这个构造函数(m1
、m2
、m3
、m4
都是 Annotation
接口定义的方法,因为所有的注解都继承自 Annotation
)。InvocationHandler
是使用 JDK 动态代理时需要实现的接口,因此可以判断这里的代理类型为 JDK 动态代理
查看 InvocationHandler
的具体实现,可以发现在 AnnotationInvocationHandler
中有一段这样的描述:
InvocationHandler for dynamic proxy implementation of Annotation.
大致意思就是:用于注解的动态代理实现的 InvocationHandler
也就是说,生成的代理类的 InvocationHandler
参数的具体实现就是 AnnotationInvocationHandler
按照 JDK 动态代理的基本使用,关键的部分是 invoke
方法的实现,具体在 AnnotationInvocationHandler
的实现如下:
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
int parameterCount = method.getParameterCount();
// Handle Object and Annotation methods
if (parameterCount == 1 && member == "equals" &&
method.getParameterTypes()[0] == Object.class) {
return equalsImpl(proxy, args[0]);
}
if (parameterCount != 0) {
throw new AssertionError("Too many parameters for an annotation method");
}
// 如果 是 Annotation 中定义的方法,那么则调用 AnnotationInvocationHandler 中的具体实现
if (member == "toString") {
return toStringImpl();
} else if (member == "hashCode") {
return hashCodeImpl();
} else if (member == "annotationType") {
return type;
}
// Handle annotation member accessors
/*
走到这说明是自定义的方法(属性),尝试获取属性值
这里的 memberValues 在构造 AnnotationInvocationHandler 时就已经完成初始化了,这是一个
Map 字段,存储的时注解中配置的属性名 ——> 属性值的映射
*/
Object result = memberValues.get(member);
if (result == null)
throw new IncompleteAnnotationException(type, member);
if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
return result;
}
总结
- 注解本质上是继承了
Annotation
接口的接口类,用于提供相关元素的元数据信息 - Java 虚拟机中会按照注解的存储方法存储在类的不同时间段,如果保留时间为
RUNTIME
,那么在 Java 虚拟机中将会保存这个注解,同时有相关的属性表来存储这些注解,因此通过反射获取注解在理论上具有可行性 - 实际获取注解时是通过代理的方式来实现的,
AnnotationInvocationHandler
是实际方法调用所有者。对于注解参数的获取,AnnotationInvocationHandler
中通过memberValues
的Map
结构来存储相关的映射关系
参考:
[1] https://juejin.cn/post/6844903636733001741#heading-0
[2] https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-4.html#jvms-4.7
Java 注解的实现原理的更多相关文章
- 认识下java注解的实现原理
1,什么是注解 注解也叫元数据,例如常见的@Override和@Deprecated,注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包.类.接口.字段.方法参数.局部变量等进行注 ...
- Java 注解及其底层原理
目录 什么是注解? 注解的分类 Java自带的标准注解 元注解 @Retention @Documented @Target @Inherited @Repeatable 自定义注解 自定义注解的读取 ...
- Java注解及应用原理
视频地址:https://www.bilibili.com/video/BV1Py4y1Y77P/?spm_id_from=333.337.search-card.all.click&vd_s ...
- Java自定义注解源码+原理解释(使用Java自定义注解校验bean传入参数合法性)
Java自定义注解源码+原理解释(使用Java自定义注解校验bean传入参数合法性) 前言:由于前段时间忙于写接口,在接口中需要做很多的参数校验,本着简洁.高效的原则,便写了这个小工具供自己使用(内容 ...
- java@ 注解原理与使用
Java反射 java反射机制的定义: 在运行转态时(动态的)时. 对于任意一个类,都能够知道这个类的所有属性和方法 对于任意一个对象,都能够知道调用它的任意属性和方法 Class对象 java中用对 ...
- java注解(Annotation)解析
注解(Annotation)在java中应用非常广泛.它既能帮助我们在编码中减少错误,(比如最常见的Override注解),还可以帮助我们减少各种xml文件的配置,比如定义AOP切面用@AspectJ ...
- Java注解Annotation学习
学习注解Annotation的原理,这篇讲的不错:http://blog.csdn.net/lylwo317/article/details/52163304 先自定义一个运行时注解 @Target( ...
- java自定义注解知识实例及SSH框架下,拦截器中无法获得java注解属性值的问题
一.java自定义注解相关知识 注解这东西是java语言本身就带有的功能特点,于struts,hibernate,spring这三个框架无关.使用得当特别方便.基于注解的xml文件配置方式也受到人们的 ...
- Java Spring Boot VS .NetCore (八) Java 注解 vs .NetCore Attribute
Java Spring Boot VS .NetCore (一)来一个简单的 Hello World Java Spring Boot VS .NetCore (二)实现一个过滤器Filter Jav ...
- [2]朝花夕拾-JAVA注解、PHP注解?
一.Java注解概述 注解,也被称为元数据,为我们在代码中添加信息提供了一种形式化的方法,是我们可以在稍后某个时刻非常方便地使用这些数据. 注解在一定程度上是把元数据与源代码文件结合在一起,而不是保存 ...
随机推荐
- 6. 用Rust手把手编写一个wmproxy(代理,内网穿透等), 通讯协议源码解读篇
用Rust手把手编写一个wmproxy(代理,内网穿透等), 通讯协议源码解读篇 项目 ++wmproxy++ gite: https://gitee.com/tickbh/wmproxy githu ...
- SQL 语句 增删改查、边学习边增加中..... 这一部分为select
SQL语句按照最大的类别分为 1.增加 insert 2.删除 delete https://www.cnblogs.com/kuangmeng/p/17756654.html 3.修改update ...
- 虹科案例|Redis企业版数据库:金融行业客户案例解读
传统银行无法提供无缝的全渠道客户体验.无法实时检测欺诈.无法获得业务洞察力.用户体验感较差.品牌声誉受损和业务损失?虹科提供的Redis企业版数据库具有低延迟.高吞吐和可用性性能,实施Redis企业版 ...
- Java线程安全详解
并发与多线程 blog:https://devonmusa.github.io 1 常见概念 1.1 操作系统线程运行状态 NEW RUNNABLE RUNNING BLOCKED 1.2 Java虚 ...
- 文件 inode 与 no space left on device 异常
转载请注明出处: 文件inode 在 Linux 文件系统中,每一个文件或目录都会有一个 inode,它是一个数据结构,用于存储文件的元数据,比如文件的权限.所有者.大小.创建和修改的时间等.inod ...
- Wampserver搭建DVWA和sqli-labs问题总结
Wampserver 搭建 DVWA 和 sqli-labs 问题总结 遇到问题解决的思路方法 百度,博客去搜索相关的问题,人工智能 chatgpt 查看官方文档,查看注释. 本次解决方法就是在文档的 ...
- STL multimap容器
multimap容器 multimap容器保存的是有序的键/值对,但是可以保存重复的元素.multimap中会出现具有相同键值的元素序列.multimap大部分成员函数的使用方式和map相同.因为重复 ...
- “技能兴鲁”职业技能大赛-网络安全赛项-学生组初赛 Crypto WP
babyRSA 查看代码 from gmpy2 import * from Crypto.Util.number import * flag = 'flag{I\'m not gonna tell y ...
- DP:三角形的最小路径和
给定一个三角形,找出自顶向下的最小路径和.每一步只能移动到下一行中相邻的结点上. 例如,给定三角形: [ [2], [3,4], [6,5,7], [4,1,8,3]] 自顶向下的 ...
- 2018CCPC桂林 A(贪心,思维)
题目 分析:首先发现将大的数放在小的数前面结果更优,于是想到通过比较元素大小的方式将两个数组合并,大的放前面小的放后面,但很容易就能想到比这样合并更优的方案.一开始我是想先按这种方式进行合并,然后将最 ...