注解的本质

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 参数这个构造函数(m1m2m3m4 都是 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 中通过 memberValuesMap 结构来存储相关的映射关系

参考:

[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 注解的实现原理的更多相关文章

  1. 认识下java注解的实现原理

    1,什么是注解 注解也叫元数据,例如常见的@Override和@Deprecated,注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包.类.接口.字段.方法参数.局部变量等进行注 ...

  2. Java 注解及其底层原理

    目录 什么是注解? 注解的分类 Java自带的标准注解 元注解 @Retention @Documented @Target @Inherited @Repeatable 自定义注解 自定义注解的读取 ...

  3. Java注解及应用原理

    视频地址:https://www.bilibili.com/video/BV1Py4y1Y77P/?spm_id_from=333.337.search-card.all.click&vd_s ...

  4. Java自定义注解源码+原理解释(使用Java自定义注解校验bean传入参数合法性)

    Java自定义注解源码+原理解释(使用Java自定义注解校验bean传入参数合法性) 前言:由于前段时间忙于写接口,在接口中需要做很多的参数校验,本着简洁.高效的原则,便写了这个小工具供自己使用(内容 ...

  5. java@ 注解原理与使用

    Java反射 java反射机制的定义: 在运行转态时(动态的)时. 对于任意一个类,都能够知道这个类的所有属性和方法 对于任意一个对象,都能够知道调用它的任意属性和方法 Class对象 java中用对 ...

  6. java注解(Annotation)解析

    注解(Annotation)在java中应用非常广泛.它既能帮助我们在编码中减少错误,(比如最常见的Override注解),还可以帮助我们减少各种xml文件的配置,比如定义AOP切面用@AspectJ ...

  7. Java注解Annotation学习

    学习注解Annotation的原理,这篇讲的不错:http://blog.csdn.net/lylwo317/article/details/52163304 先自定义一个运行时注解 @Target( ...

  8. java自定义注解知识实例及SSH框架下,拦截器中无法获得java注解属性值的问题

    一.java自定义注解相关知识 注解这东西是java语言本身就带有的功能特点,于struts,hibernate,spring这三个框架无关.使用得当特别方便.基于注解的xml文件配置方式也受到人们的 ...

  9. Java Spring Boot VS .NetCore (八) Java 注解 vs .NetCore Attribute

    Java Spring Boot VS .NetCore (一)来一个简单的 Hello World Java Spring Boot VS .NetCore (二)实现一个过滤器Filter Jav ...

  10. [2]朝花夕拾-JAVA注解、PHP注解?

    一.Java注解概述 注解,也被称为元数据,为我们在代码中添加信息提供了一种形式化的方法,是我们可以在稍后某个时刻非常方便地使用这些数据. 注解在一定程度上是把元数据与源代码文件结合在一起,而不是保存 ...

随机推荐

  1. MySQL系列之备份恢复——运维在备份恢复方面、备份类型、备份方式及工具、逻辑备份和物理备份、备份策略、备份工具使用-mysqldump、企业故障恢复案例、备份时优化参数、MySQL物理备份工具

    文章目录 1. 运维在数据库备份恢复方面的职责 1.1 设计备份策略 1.2 日常备份检查 1.3 定期恢复演练(测试库) 1.4 故障恢复 1.5 迁移 2. 备份类型 2.1 热备 2.2 温备 ...

  2. 服务链路追踪 —— SpringCloud Sleuth

    Sleuth 简介 随着业务的发展,系统规模变得越来越大,微服务拆分越来越细,各微服务间的调用关系也越来越复杂.客户端请求在后端系统中会经过多个不同的微服务调用来协同产生最后的请求结果,几平每一个请求 ...

  3. C#学习笔记——变量、常量和转义字符

    变量 变量是存储数值的容器,是一门程序语言的最基础的部分. 不同的变量类型可以存储不同类型的数值. 种类: 在C#种一共有14种变量: 有符号类型4种 无符号类型4种 浮点数3种 特殊类型(char ...

  4. Oracle11g安装教程(带安装包)

    找了半天没在官网上找到Oracle11g的安装包下载,又找了半天,终于在网上的一个教程里找到安装包的网盘链接.现在在这记一下防止以后重新找麻烦. 网盘链接 百度云盘链接:[https://pan.ba ...

  5. eclipse使用技巧和插件

    eclipse使用技巧和插件 本篇文章只列举了一部分技巧和插件,并没有包括大家都知道的快捷键和技巧,而是一些不经常用但又很方便的功能. 一,技巧 给Eclipse添加更方便的提示功能:Windows– ...

  6. 唱衰这么多年,PHP 仍然还是你大爷!

    PHP 是个庞然大物. 尽管有人不断宣称 PHP "即将消亡". 但无法改变的事实是:互联网依然大量依赖 PHP.本文将通过大量的数据和事实告诉你为何 PHP 仍然在统治着互联网, ...

  7. 数据库系列:MySQL引擎MyISAM和InnoDB的比较

    1.数据库核心知识点 数据库系列:MySQL慢查询分析和性能优化 数据库系列:MySQL索引优化总结(综合版) 数据库系列:高并发下的数据字段变更 数据库系列:覆盖索引和规避回表 数据库系列:数据库高 ...

  8. 【web实验报告】实验二

    一.实验目的 通过一个小型网站的开发,掌握JSP基础知识,加深对session,request,response,cookie等对象的理解,掌握其使用方法,进一步深入掌握HTML.CSS和JavaSc ...

  9. 【LOJ NOI Round#2 Day1 T1】单枪匹马(矩阵乘法)

    题目传送门 操作二要求的东西是一个循环迭代的东西,手推相邻两项找下规律,发现相邻两项的分子分母间含有线性关系,考虑用矩阵乘法求解.对于 \([1,n]\)的询问,从后往前倒推, \(x_{n-1}=a ...

  10. SQL Server 自动增长清零的方法

    方法一: truncate table TableName 删除表中的所有的数据的同时,将自动增长清零.如果有外键参考这个表,这个方法会报错(即便主键表和外键表都已经没有数据),请参考方法2. 方法二 ...