Java 注解及其底层原理
作者:小牛呼噜噜 | https://xiaoniuhululu.com
计算机内功、JAVA底层、面试相关资料等更多精彩文章在公众号「小牛呼噜噜 」
什么是注解?
当我们开发SpringBoot项目,我们只需对启动类加上@SpringBootApplication,就能自动装配,不需要编写冗余的xml配置。当我们为项目添加lombok依赖,使用@Data来修饰实体类,我们就不需要编写getter和setter方法,构造函数等等。@SpringBootApplication,@Data等像这种以**"@"**开头的代码 就是注解,只需简简单单几个注解,就能帮助我们省略大量冗余的代码,这是一个非常不可思议的事情!
但我们往往知道在哪些地方加上合适的注解,不然IDE会报错,却不知道其中的原理,那究竟什么是注解呢?
注解(Annotation ), 是 Java5 开始引入的新特性,是放在Java源码的类、方法、字段、参数前的一种特殊“注释”,是一种标记、标签。注释往往会被编译器直接忽略,能够被编译器打包进入class文件,并执行相应的处理。
按照惯例我们去看下注解的源码:
先新建一个注解文件:MyAnnotation.java
public @interface MyAnnotation {
}
发现MyAnnotation 是被@interface修饰的,感觉和接口interface很像。
我们再通过idea来看下其的类继承:

MyAnnotation 是继承Annotation接口的。
我们再反编译一下:
$ javac MyAnnotation.java
$ javap -c MyAnnotation
Compiled from "MyAnnotation.java"
public interface com.zj.ideaprojects.test3.MyAnnotation extends java.lang.annotation.Annotation {
}
发现生成的字节码中 @interface变成了interface,MyAnnotation而且自动继承了Annotation
我们由此可以明白:注解本质是一个继承了Annotation 的特殊接口,所以注解也叫声明式接口
注解的分类
一般常用的注解可以分为三大类:
Java自带的标准注解
例如:
- @Override:让编译器检查该方法是否正确地实现了覆写;
- @SuppressWarnings:告诉编译器忽略此处代码产生的警告。
- @Deprecated:标记过时的元素,这个我们经常在日常开发中经常碰到。
- @FunctionalInterface:表明函数式接口注解
元注解
元注解是能够用于定义注解的注解,或者说元注解是一种基本注解,包括@Retention、@Target、@Inherited、@Documented、@Repeatable 等
元注解也是Java自带的标准注解,只不过用于修饰注解,比较特殊。
@Retention
注解的保留策略, @Retention 定义了Annotation的生命周期。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。
它的参数:
| RetentionPolicy.SOURCE | 注解只在源码阶段保留,在编译器进行编译时它将被丢掉 |
|---|---|
| RetentionPolicy.CLASS | 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中 |
| RetentionPolicy.RUNTIME | 注解可以保留到程序运行中的时候,它会被加载进 JVM 中,在程序运行中也可以获取到它们 |
如果@Retention不存在,则该Annotation默认为RetentionPolicy.CLASS
示例:
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}
我们自定义的TestAnnotation 可以在程序运行中被获取到
@Documented
它的作用是 用于制作文档,将注解中的元素包含到 doc 中
一般不怎么用到,了解即可
@Target
@Target 指定了注解可以修饰哪些地方, 比如方法、成员变量、还是包等等
当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。
常用的参数如下:
| ElementType.ANNOTATION_TYPE | 给一个注解进行注解 |
|---|---|
| ElementType.CONSTRUCTOR | 给构造方法进行注解 |
| ElementType.FIELD | 给属性进行注解 |
| ElementType.LOCAL_VARIABLE | 给局部变量进行注解 |
| ElementType.METHOD | 给方法进行注解 |
| ElementType.PACKAGE | 给包进行注解 |
| ElementType.PARAMETER | 给一个方法内的参数进行注解 |
| ElementType.TYPE | 给一个类型进行注解,比如类、接口、枚举 |
@Inherited
@Inherited 修饰一个类时,表明它的注解可以被其子类继承,缺省情况默认是不继承的。
换句话说:如果一个子类想获取到父类上的注解信息,那么必须在父类上使用的注解上面 加上@Inherit关键字
注意:
- @Inherited仅针对@Target(ElementType.TYPE)类型的annotation有效
- @Inherited 不是表明 注解可以继承,而是子类可以继承父类的注解
我们来看一个示例:
定义一个注解:
@Inherited
@Target(ElementType.TYPE)
public @interface MyReport {
String name() default "";
int value() default 0;
}
使用这个注解:
@MyReport(value=1)
public class Teacher {
}
则它的子类默认继承了该注解:
public class Student extends Teacher{
}
idea 查看类的继承关系:

@Repeatable
使用@Repeatable这个元注解来申明注解,表示这个声明的注解是可重复的
@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。
比如:一个人他既会下棋又会做饭,他还会唱歌。
@Repeatable(MyReport.class)
@Target(ElementType.TYPE)
public @interface MyReport {
String name() default "";
int value() default 0;
}
@MyReport(value=0)
@MyReport(value=1)
@MyReport(value=2)
public class Man{
}
自定义注解
我们可以根据自己的需求定义注解,一般分为以下几步:
- 新建注解文件, @interface定义注解
public @interface MyReport { }
- 添加参数、默认值
public @interface MyReport {
String name() default "";
int value() default 0;
}
- 用元注解配置注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyReport {
String name() default "";
int value() default 0;
}
我们一般设置 @Target和@Retention就够了,其中@Retention一般设置为RUNTIME,因为我们自定义的注解通常需要在程序运行中读取。
自定义注解的读取
读到这里,相信大家已经明白了 如何定义和使用注解,我们接下来 就需要如何将注解利用起来。
我们知道读取注解, 需要用到java的反射
推荐阅读笔者之前写过关于反射的文章:https://mp.weixin.qq.com/s/_n8HTIjkw7Emcunpb4-Iwg
我们先来写一个简单的示例--反射获取注解:
通过前文的了解,先来改造一下MyAnnotation.java
@Retention(RetentionPolicy.RUNTIME)//确保程序运行中,能够读取到该注解!!!
public @interface MyAnnotation {
String msg() default "no msg";
}
我们再用@MyAnnotation来修饰Person类的类名、属性、和方法
@MyAnnotation(msg = "this person class")//注解 修饰类
public class Person {
private String name;//姓名
private String sex;//性别
@MyAnnotation(msg = "this person field public")//注解 修饰 public属性
public int height;//身高
@MyAnnotation(msg = "this person field private")//注解 修饰 private属性
private int weight;//体重
public void sleep(){
System.out.println(this.name+"--"+ "睡觉");
}
public void eat(){
System.out.println("吃饭");
}
@MyAnnotation(msg = "this person method")//注解 修饰方法
public void dance(){
System.out.println("跳舞");
}
}
最后我们写一个测试类
public class TestAn {
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
//获取Person class 实例
Class<Person> c1 = Person.class;
//反射获取 类上的注解
MyAnnotation classAnnotation = c1.getAnnotation(MyAnnotation.class);
System.out.println(classAnnotation.msg());
//反射获取 private属性上的注解
Field we = c1.getDeclaredField("weight");
MyAnnotation fieldAnnotation = we.getAnnotation(MyAnnotation.class);
System.out.println(fieldAnnotation.msg());
//反射获取 public属性上的注解
Field he = c1.getDeclaredField("height");
MyAnnotation field2Annotation = he.getAnnotation(MyAnnotation.class);
System.out.println(field2Annotation.msg());
//反射获取 方法上的注解
Method me = c1.getMethod("dance",null);
MyAnnotation methodAnnotation = me.getAnnotation(MyAnnotation.class);
System.out.println(methodAnnotation.msg());
}
}
结果:
this person class
this person field private
this person field public
this person method
我们通过反射读取api时,一般会先去校验这个注解存不存在:
if(c1.isAnnotationPresent(MyAnnotation.class)) {
//存在 MyAnnotation 注解
}else {
//不存在 MyAnnotation 注解
}
我们发现反射真的很强大,不仅可以读取类的属性、方法、构造器等信息,还可以读取类的注解相关信息。
那反射是如何实现工作的?
我们来看下源码:
从 c1.getAnnotation(MyAnnotation.class);通过idea点进去查看源码,把重点的给贴出来,其他的就省略了
Map<Class<? extends Annotation>, Annotation> declaredAnnotations =
AnnotationParser.parseAnnotations(getRawAnnotations(), getConstantPool(), this);
parseAnnotations()去分析注解,其第一个参数是 获取原始注解,第二个参数是获取常量池内容
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));
}
});
}
Proxy._newProxyInstance_(var0.getClassLoader(), new Class[]{var0}, new AnnotationInvocationHandler(var0, var1)创建动态代理,此处var0参数是由常量池获取的数据转换而来。
我们监听此处的var0:

可以推断出注解相关的信息 是存放在常量池中的
我们来总结一下,反射调用getAnnotations(MyAnnotation.class)方法的背后主要操作:
解析注解parseAnnotations()的时候 从该注解类的常量池中取出注解相关的信息,将其转换格式后,通过newProxyInstance(注解的类加载器,注解的class实例 ,AnotationInvocationHandler实例)来创建代理对象,作为参数传进去,最后返回一个代理实例。
其中AnotationInvocationHandler类是一个典型的动态代理类, 这边先挖个坑,暂不展开,不然这篇文章是写不完了

关于动态代理类我们只需先知道: 对象的执行方法,交给代理来负责
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
...
private final Map<String, Object> memberValues;//存放该注解所有属性的值
private transient volatile Method[] memberMethods = null;
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
...
}
public Object invoke(Object var1, Method var2, Object[] var3) {
...
//调用委托类对象的方法,具体等等一些操作
}
...
}
反射调用getAnnotations(MyAnnotation.class),返回一个代理实例,我们可以通过这个实例来操作该注解
示例:注解 模拟访问权限控制
当我们引入springsecurity来做安全框架,然后只需添加@PreAuthorize("hasRole('Admin')")注解,就能实现权限的控制,简简单单地一行代码,就优雅地实现了权限控制,觉不觉得很神奇?让我们一起模拟一个出来吧
@Retention(RetentionPolicy.RUNTIME)
public @interface MyPreVer {
String value() default "no role";
}
public class ResourceLogin {
private String name;
@MyPreVer(value = "User")
private void rsA() {
System.out.println("资源A");
}
@MyPreVer(value = "Admin")
private void rsB() {
System.out.println("资源B");
}
}
public class TestLogin {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
//模拟 用户的权限
String role = "User";
//模拟 需要的权限
final String RoleNeeded = "Admin";
//获取Class实例
Class<ResourceLogin> c1 = ResourceLogin.class;
//访问资源A
Method meA = c1.getDeclaredMethod("rsA",null);
MyPreVer meAPre = meA.getDeclaredAnnotation(MyPreVer.class);
if(meAPre.value().equals(RoleNeeded)) {//模拟拦截器
meA.setAccessible(true);
meA.invoke(c1.newInstance(),null);//模拟访问资源
}else {
System.out.println("骚瑞,你无权访问该资源");
}
//访问资源B
Method meB = c1.getDeclaredMethod("rsB",null);
MyPreVer meBPre = meB.getDeclaredAnnotation(MyPreVer.class);
if(meBPre.value().equals(RoleNeeded)) {//模拟拦截器
meB.setAccessible(true);
meB.invoke(c1.newInstance());//模拟访问资源
}else {
System.out.println("骚瑞,你无权访问该资源");
}
}
}
结果:
骚瑞,你无权访问该资源
资源B
尾语
注解 是一种标记、标签 来修饰代码,但它不是代码本身的一部分,即注解本身对代码逻辑没有任何影响,如何使用注解完全取决于我们开发者用Java反射来读取和使用。
我们发现反射真的很强大,不仅可以读取类的属性、方法、构造器等信息,还可以读取类的注解相关信息,以后还会经常遇到它。
注解一般用于
- 编译器可以利用注解来探测错误和检查信息,像
@override检查是否重写 - 适合工具类型的软件用的,避免繁琐的代码,生成代码配置,比如jpa自动生成sql,日志注解,权限控制
- 程序运行时的处理: 某些注解可以在程序运行的时候接受代码的读取,比如我们可以自定义注解
平时我们只知道如何使用注解,却不知道其是如何起作用的,理所当然的往往是我们所忽视的。
参考资料:
《Java核心技术 卷一》
https://blog.csdn.net/qq_20009015/article/details/106038023
https://zhuanlan.zhihu.com/p/258429599
本篇文章到这里就结束啦,很感谢你能看到最后,如果觉得文章对你有帮助,别忘记关注我!

Java 注解及其底层原理的更多相关文章
- 认识下java注解的实现原理
1,什么是注解 注解也叫元数据,例如常见的@Override和@Deprecated,注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包.类.接口.字段.方法参数.局部变量等进行注 ...
- java面试-CAS底层原理
一.CAS是什么? 比较并交换,它是一条CPU并发原语. CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B.当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什 ...
- [Java] I/O底层原理之一:字符流、字节流及其源码分析
关于 I/O 的类可以分为四种: 关于字节的操作:InputStream 和 OutPutStream: 关于字符的操作:Writer 和 Reader: 关于磁盘的操作:File: 关于网络的操作: ...
- 基于JAVA Socket的底层原理分析及工具实现
前言 在工作开始之前,我们先来了解一下Socket 所谓Socket,又被称作套接字,它是一个抽象层,简单来说就是存在于不同平台(os)的公共接口.学过网络的同学可以把它理解为基于传输TCP/IP协议 ...
- [Java] I/O底层原理之三:NIO
本篇文章参考自并发编程网 一.NIO 的概述 NIO 由以下几个核心组成 Channels Buffers Selectors 选择器用于监听多个通道的事件(如:链接打开.数据达到),单个线程可以监听 ...
- [Java] I/O底层原理之二:网络IO及网络编程
首先我们来看一下当访问一个域名时它的过程 查找 DNS 首先,浏览器检查缓存中有没有 浏览器缓存中没有,则查找操作系统中有没有配置这个对应关系 如果操作系统中也没有,则去 DNS 查找,即发送DNS报 ...
- Java 总结 数据底层原理 【包括 ArrayList、LinkedList、hash table、HashMap、Hashtable、ConcurrentHashMap、hash code、HashSet、LinkedHashMap、LinkedHashSet】
1.ArrayList (1)底层是由动态数组实现的[使用了List接口]. (2)动态数组是长度不固定,随着数据的增多而变长. (3)如果不指定,默认长度为10,当添加的元素超过当前数组的长度时,会 ...
- JDK中注解的底层实现
前提 用Java快三年了,注解算是一个常用的类型,特别是在一些框架里面会大量使用注解做组件标识.配置或者策略.但是一直没有深入去探究JDK中的注解到底是什么,底层是怎么实现了?于是参考了一些资料,做了 ...
- 基础篇:深入解析JAVA注解机制
目录 java实现注解的底层原理和概念 五种元注解详解 使用动态代理机制处理注解 spring.AOP和注解机制 (题外)@FunctionalInterface原理介绍 欢迎指正文中错误 关注公众号 ...
随机推荐
- BZOJ4713 迷失的字符串 解题报告
BZOJ4713 题目大意:有 \(n\) 个点 \(n-1\) 条边,每条边有一个字符.给你 \(m\) 个字符串 \(s_i\),问每个字符串是否可以通过树上的一条简单路径表示. \(n,m\le ...
- [算法学习] dsu on tree
简介 dsu on tree跟dsu没有关系,但是dsu on tree借鉴了dsu的启发式合并的思想. 它是用来解决一类树上的询问问题,一般这种问题有以下特征: \(1.\)只有对子树的查询: \( ...
- Redis概述及基本数据结构
SQL vs NoSQL 结构化 SQL 是结构化的,一旦定义了表结构,以后在维护数据的时候必须严格遵守定义的结构. NoSQL 是非结构化的,常见的形式有 Redis 的 Key-Value 存储形 ...
- SPFA 最短路算法
SPFA算法 1.什么是spfa算法? SPFA 算法是 Bellman-Ford算法 的队列优化算法的别称,通常用于求含负权边的单源最短路径,以及判负权环.SPFA一般情况复杂度是O(m)O(m) ...
- Python3 collections模块
https://www.cnblogs.com/zhangxinqi/p/7921941.html http://www.wjhsh.net/meng-wei-zhi-p-8259022.html h ...
- conda创建/移除虚拟环境
conda创建python虚拟环境 前言 conda常用的命令: conda list 查看安装了哪些包. conda env list 或 conda info -e 查看当前存在哪些虚拟环境 co ...
- JS:this关键字1
this 代表了当前的对象,哪个对象调用了this所在的函数,this就代表了哪个对象: 例1: function fn(){ var a = 1; console.log(this) } fn() ...
- 【故障公告】取代 memcached 的 redis 出现问题造成网站故障
6月19日开始,我们将博客站点的缓存服务器从 memcached 换成了 redis,稳定运行了3天,今天上午访问高峰突然出现问题,在 11:00-12:30 期间影响了网站的正常访问,由此给您带来麻 ...
- HDLBits->Circuits->Arithmetic Circuitd->3-bit binary adder
Verilog实例数组 对于一个定义好的简单module,例如加法器之类,如果我们要对其进行几十次几百次的例化,并且这些例化基本都是相同的形式,那么我们肯定不能一个个的单独对其进行例化,此时我们就可以 ...
- 为什么新的5G标准将为技术栈带来更低的 TCO
摘要 新5G标准和边缘计算对低延迟的要求,给那些试图将一堆不同组件组装成一个不会出现故障且仍具有低延迟的高成本效益应用程序公司带来了严峻的挑战.事实上,这个问题非常严重,以至于需要重新考虑架构. ...