作者:小牛呼噜噜 | 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{
}

自定义注解

我们可以根据自己的需求定义注解,一般分为以下几步:

  1. 新建注解文件, @interface定义注解
public @interface MyReport { }
  1. 添加参数、默认值
public @interface MyReport {
String name() default "";
int value() default 0;
}
  1. 用元注解配置注解
@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 注解及其底层原理的更多相关文章

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

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

  2. java面试-CAS底层原理

    一.CAS是什么? 比较并交换,它是一条CPU并发原语. CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B.当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什 ...

  3. [Java] I/O底层原理之一:字符流、字节流及其源码分析

    关于 I/O 的类可以分为四种: 关于字节的操作:InputStream 和 OutPutStream: 关于字符的操作:Writer 和 Reader: 关于磁盘的操作:File: 关于网络的操作: ...

  4. 基于JAVA Socket的底层原理分析及工具实现

    前言 在工作开始之前,我们先来了解一下Socket 所谓Socket,又被称作套接字,它是一个抽象层,简单来说就是存在于不同平台(os)的公共接口.学过网络的同学可以把它理解为基于传输TCP/IP协议 ...

  5. [Java] I/O底层原理之三:NIO

    本篇文章参考自并发编程网 一.NIO 的概述 NIO 由以下几个核心组成 Channels Buffers Selectors 选择器用于监听多个通道的事件(如:链接打开.数据达到),单个线程可以监听 ...

  6. [Java] I/O底层原理之二:网络IO及网络编程

    首先我们来看一下当访问一个域名时它的过程 查找 DNS 首先,浏览器检查缓存中有没有 浏览器缓存中没有,则查找操作系统中有没有配置这个对应关系 如果操作系统中也没有,则去 DNS 查找,即发送DNS报 ...

  7. Java 总结 数据底层原理 【包括 ArrayList、LinkedList、hash table、HashMap、Hashtable、ConcurrentHashMap、hash code、HashSet、LinkedHashMap、LinkedHashSet】

    1.ArrayList (1)底层是由动态数组实现的[使用了List接口]. (2)动态数组是长度不固定,随着数据的增多而变长. (3)如果不指定,默认长度为10,当添加的元素超过当前数组的长度时,会 ...

  8. JDK中注解的底层实现

    前提 用Java快三年了,注解算是一个常用的类型,特别是在一些框架里面会大量使用注解做组件标识.配置或者策略.但是一直没有深入去探究JDK中的注解到底是什么,底层是怎么实现了?于是参考了一些资料,做了 ...

  9. 基础篇:深入解析JAVA注解机制

    目录 java实现注解的底层原理和概念 五种元注解详解 使用动态代理机制处理注解 spring.AOP和注解机制 (题外)@FunctionalInterface原理介绍 欢迎指正文中错误 关注公众号 ...

随机推荐

  1. 降维、特征提取与流形学习--非负矩阵分解(NMF)

    非负矩阵分解(NMF)是一种无监督学习算法,目的在于提取有用的特征(可以识别出组合成数据的原始分量),也可以用于降维,通常不用于对数据进行重建或者编码. NMF将每个数据点写成一些分量的加权求和(与P ...

  2. 关于基础RMQ——ST算法

    RMQ,Range Maximum/Minimum Query,顾名思义,就是询问某个区间内的最大值或最小值,今天我主要记录的是其求解方法--ST算法 相对于线段树,它的运行速度会快很多,可以做到O( ...

  3. WC2021 题目清单

    Day2 上午 <IOI题型与趣题分析> 来源 题目 完成情况 备注 IOI2002 Day1T1 Frog 已完成 IOI2002 Day1T2 Utopia IOI2002 Day1T ...

  4. 《HALCON数字图像处理》第一、二章笔记

    目录 第一章 绪论 1.1 图像和图像处理 1.1.1 图像 1.1.2 数字图像 1.1.3 图像处理及其发展过程 1.2 数字图像处理的步骤和方法 1.3 数字图像处理系统的硬件组成 1.4 数字 ...

  5. Java 多线程共享模型之管程(上)

    主线程与守护线程 默认情况下,Java 进程需要等待所有线程都运行结束,才会结束.有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束. packag ...

  6. rabbitMq急速安装教程

    背景 我们在工作中很多时候其实也用过mq.但是仅仅只是会用,所以老猫在此想完整地归纳一下mq相关的知识点,在此分享给大家.首先给大家带来的是mq的单机急速安装.操作系统时centos7.(本来想记录到 ...

  7. PyTorch保存模型、冻结参数等

    此外可以参考PyTorch模型保存.https://zhuanlan.zhihu.com/p/73893187 查看模型每层输出详情 Keras有一个简洁的API来查看模型的每一层输出尺寸,这在调试网 ...

  8. flink-执行模式

    flink的执行模式 flink既能处理离线数据,也能处理实时数据,在1.12.0版本以前,批数据返回的数据集合是dataSet,对应一套dataSet的api,从1.12.0版本以后,flink实现 ...

  9. Java创建数据库新建表及初始化表

    方法一 package com.crt.openapi; import java.sql.DriverManager;import java.sql.ResultSet;import java.io. ...

  10. SAP SD-Invoice 销售发票

    针对销售订单的发票流程: 1. 事务码:VF01(个别生成系统发票) 创建开票凭证(发票)/  VF04 开具系统发票(可把多个item 合并成一张系统发票) 2. 事务码:VF02 修改发票, 释放 ...