• Java 注解及其底层原理


    作者:小牛呼噜噜 | https://xiaoniuhululu.com
    计算机内功、JAVA底层、面试相关资料等更多精彩文章在公众号「小牛呼噜噜 」

    什么是注解?

    当我们开发SpringBoot项目,我们只需对启动类加上@SpringBootApplication,就能自动装配,不需要编写冗余的xml配置。当我们为项目添加lombok依赖,使用@Data来修饰实体类,我们就不需要编写getter和setter方法,构造函数等等。@SpringBootApplication,@Data等像这种以@开头的代码 就是注解只需简简单单几个注解,就能帮助我们省略大量冗余的代码,这是一个非常不可思议的事情!
    但我们往往知道在哪些地方加上合适的注解,不然IDE会报错,却不知道其中的原理,那究竟什么是注解呢?

    注解(Annotation), 是 Java5 开始引入的新特性,是放在Java源码的类、方法、字段、参数前的一种特殊“注释”,是一种标记、标签。注释往往会被编译器直接忽略,能够被编译器打包进入class文件,并执行相应的处理。

    按照惯例我们去看下注解的源码:

    先新建一个注解文件:MyAnnotation.java

    public @interface MyAnnotation {
    }
    
    • 1
    • 2

    发现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 {
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    发现生成的字节码中 @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 { 
    } 
    
    • 1
    • 2
    • 3

    我们自定义的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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    使用这个注解:

    @MyReport(value=1)
    public class Teacher {
    }
    
    • 1
    • 2
    • 3

    则它的子类默认继承了该注解:

    public class Student extends Teacher{
        
    }
    
    • 1
    • 2
    • 3

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    自定义注解

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

    1. 新建注解文件, @interface定义注解
    public @interface MyReport { } 
    
    • 1
    1. 添加参数、默认值
    public @interface MyReport {
        String name() default "";
        int value() default 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    1. 用元注解配置注解
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface MyReport {
        String name() default "";
        int value() default 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们一般设置 @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";
    }
    
    • 1
    • 2
    • 3
    • 4

    我们再用@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("跳舞");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    最后我们写一个测试类

    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());
            
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    结果:

    this person class
    this person field private
    this person field public
    this person method

    我们通过反射读取api时,一般会先去校验这个注解存不存在:

    if(c1.isAnnotationPresent(MyAnnotation.class)) {
        //存在 MyAnnotation 注解
    }else {
        //不存在 MyAnnotation 注解
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们发现反射真的很强大,不仅可以读取类的属性、方法、构造器等信息,还可以读取类的注解相关信息。

    那反射是如何实现工作的?
    我们来看下源码:
    c1.getAnnotation(MyAnnotation.class);通过idea点进去查看源码,把重点的给贴出来,其他的就省略了

    Map, Annotation> declaredAnnotations =
                AnnotationParser.parseAnnotations(getRawAnnotations(), getConstantPool(), this);
    
    • 1
    • 2

    parseAnnotations()去分析注解,其第一个参数是 获取原始注解,第二个参数是获取常量池内容

    public static Annotation annotationForMap(final Class var0, final Map var1) {
            return (Annotation)AccessController.doPrivileged(new PrivilegedAction() {
                public Annotation run() {
                    return (Annotation)Proxy.newProxyInstance(var0.getClassLoader(), new Class[]{var0}, new AnnotationInvocationHandler(var0, var1));
                }
            });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Proxy._newProxyInstance_(var0.getClassLoader(), new Class[]{var0}, new AnnotationInvocationHandler(var0, var1)创建动态代理,此处var0参数是由常量池获取的数据转换而来。
    我们监听此处的var0:
    image.png
    可以推断出注解相关的信息 是存放在常量池中的

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

    请添加图片描述

    关于动态代理类我们只需先知道: 对象的执行方法,交给代理来负责

    class AnnotationInvocationHandler implements InvocationHandler, Serializable {
        ...
        private final Map memberValues;//存放该注解所有属性的值
        private transient volatile Method[] memberMethods = null;
    
        AnnotationInvocationHandler(Class var1, Map var2) {
        ...
        }
    
        public Object invoke(Object var1, Method var2, Object[] var3) {
        ...
         //调用委托类对象的方法,具体等等一些操作
        }
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    反射调用getAnnotations(MyAnnotation.class),返回一个代理实例,我们可以通过这个实例来操作该注解

    示例:注解 模拟访问权限控制

    当我们引入springsecurity来做安全框架,然后只需添加@PreAuthorize("hasRole('Admin')")注解,就能实现权限的控制,简简单单地一行代码,就优雅地实现了权限控制,觉不觉得很神奇?让我们一起模拟一个出来吧

    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyPreVer {
        String value() default "no role";
    }
    
    • 1
    • 2
    • 3
    • 4
    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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    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("骚瑞,你无权访问该资源");
            }
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    结果:

    骚瑞,你无权访问该资源

    资源B

    尾语

    注解 是一种标记、标签 来修饰代码,但它不是代码本身的一部分,即注解本身对代码逻辑没有任何影响,如何使用注解完全取决于我们开发者用Java反射来读取和使用。
    我们发现反射真的很强大,不仅可以读取类的属性、方法、构造器等信息,还可以读取类的注解相关信息,以后还会经常遇到它。
    注解一般用于

    • 编译器可以利用注解来探测错误和检查信息,像@override检查是否重写
    • 适合工具类型的软件用的,避免繁琐的代码,生成代码配置,比如jpa自动生成sql,日志注解,权限控制
    • 程序运行时的处理: 某些注解可以在程序运行的时候接受代码的读取,比如我们可以自定义注解

    平时我们只知道如何使用注解,却不知道其是如何起作用的,理所当然的往往是我们所忽视的。


    参考资料:
    《Java核心技术 卷一》
    https://blog.csdn.net/qq_20009015/article/details/106038023
    https://zhuanlan.zhihu.com/p/258429599


    本篇文章到这里就结束啦,很感谢你能看到最后,如果觉得文章对你有帮助,别忘记关注我!

  • 相关阅读:
    观测云产品更新 | 优化 SSO 管理、新增监控器类型、优化场景等
    Docker学习大纲
    YOLOv7改进:全网原创首发 | 多尺度空洞注意力(MSDA) | 中科院一区顶刊 DilateFormer 2023.9
    【Linux基础】3.2 磁盘分区机制,增加一个硬盘操作,硬盘/文件夹查询操作
    基于spring boot 的学生科研项目共享平台 毕业设计-附源码271611
    【异步与线程池】串讲&优化详情页加载
    LeetCode_回溯_中等_1774.最接近目标价格的甜点成本
    YOLOv7训练数据集
    C#线程发展历史
    基于神经网络的推荐算法,协同过滤推荐算法python
  • 原文地址:https://blog.csdn.net/qq_41603102/article/details/126284014