关于Spring的两三事:神奇的注解

news2024/12/26 13:50:31

一、前言

  在之前的学习中我们介绍了注解实际上起到的是标记注释的作用,其本身并不提供任何的逻辑处理能力。也就是说如果想让注解能够实现预期的作用,就必须给注解搭配一个能够读取并处理该注解的方法,这里为了方便描述我将这样一个方法定义为注解处理器。基于这样一个认知,我们学习的重心就需要放在理解Spring中不同注解的注解处理器是如何生效的以及Spring提供了哪些自定义注解处理器实现的扩展点上面。

二、基本概念

  首先,让我们先了解一下Spring注解的基本情况,具体我们需要了解以下几个方面:

  在Spring当中注解可以分为两类,一类是Spring本身提供的原生注解(包含JDK提供的注解),一类是由开发人员自行开发的自定义注解。由于不同的注解需要特定的注解处理器才能够识别和处理,这也就意味着注解处理器也会分为原生注解处理器自定义注解处理器两种。

  那么如何在Spring中实现一个注解处理器呢?为了实现这一个需求,我们需要关注两个方面:

  • 基于Spring框架
  • 识别和处理目标注解

  在之前的文章中我们讲解了在Java程序中实现注解处理器的两种方式,分别是基于反射机制的方式使用插入式注解处理器的方式。而在加了需要基于Spring框架来实现这样一个前提之后,使用插入式注解处理器的方式就不是非常合适,因为这种方式依赖的是JDK提供的用于在编译阶段进行注解识别和处理的API而并非是依赖Spring框架提供的能力。

注意,这里并不是说不可以使用插入式注解,只是我们讨论的前提是基于Spring提供的能力来实现注解处理器。

  如此一来,我们能够实现注解处理器的方式就少了一种,但正所谓:失之东隅,收之桑榆。虽然我们不能使用插入式注解,但Spring框架提供了另外一种方式来作为替代——基于BeanDefinition的方式。让我们重新回顾一下注解处理器的作用:识别处理注解。可以看到,这里的第一步就是识别注解。所谓识别注解,实际上是识别被注解标识的类、属性或者方法,而这些信息都保存在类型信息当中。这也是为什么我们可以使用反射机制来去识别注解的原因,因为我们的目标就是去获取这些信息。让我们将目光重新转移到Spring框架中,此时我们会发现这些信息Spring已经收集并保存在BeanDefinition当中了。由此我们得到两种在Spring框架中实现注解处理器的方式:

  • 基于BeanDefinition方式:通过ApplicationContext的getBeansWithAnnotation方法获取被注解的Bean;
  • 基于反射机制方式:通过JDK提供的反射相关API获取类、属性以及方法维度的信息;

  那么应该如何使用这两种方式呢?其实在我们实际的开发过程中,这两种方式的使用并不是完全独立的,两者既可以分开使用,也可以搭配使用,具体的使用方式还需要结合Spring本身提供的扩展点进行具体讨论。

  除此以外我们还需要根据实际的执行时机判断如何实现注解处理器的执行逻辑,基于Spring应用的生命周期可以分为以下三种执行时机:

  • 初始化阶段:在应用初始化阶段执行的注解执行器一般只能够执行一次,一般情况是为了在Spring的通用初始化流程之外对特定Bean进行一些额外初始化操作,具体可以参考@Service@Resource等原生注解处理器的执行逻辑,有兴趣的同学也可以阅读笔者之前的一篇文章关于Spring的两三事:万物之始—BeanDefinition。
  • 应用运行阶段:在应用运行阶段执行的注解处理器可以针对每一次请求进行拦截执行。
  • 应用销毁阶段:和第一种执行时机一样,在应用销毁阶段执行的注解执行器也执行执行一次。

三、自定义注解的几个小例子

  由于目前已经有太多的文章来讲解Spring原生注解以及注解处理器的相关原理,笔者就不做过多的阐述,想要了解的同学可以自行百度或者翻阅源码。这里笔者想要用几个比较常见的例子来说明一下如何实现一个自定义注解及其注解处理器。

测试注解类

@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}
​
​
@Inherited
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogMonitor {
​
    /**
     * 方法名称
     *
     * @return 方法名称
     */
    String methodName() default "";
​
    /**
     * 日志事件
     *
     * @return 日志事件
     */
    String logEvent() default "";
​
}
复制代码

1. 基于ApplicationContextAware和InitializingBean接口

@Component
public class TestApplicationAware implements ApplicationContextAware, InitializingBean {
​
    private ApplicationContext applicationContext;
​
    @Override
    public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
​
    @Override
    public void afterPropertiesSet() throws Exception {
        // 在所有的属性被设置之后进行一次处理
        this.handleClass();
        this.handleMethod();
    }
​
    private void handleMethod() {
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(RestController.class);
        for (Object bean : beans.values()) {
            Class<?> clazz = bean.getClass();
            System.out.println(clazz.getName());
            for (Method method : clazz.getDeclaredMethods()) {
              // 这里需要思考一个问题:如果从应用上下文中取出的代理对象会发生什么?
                LogMonitor logMonitor = method.getAnnotation(LogMonitor.class);
                if (Objects.nonNull(logMonitor)) {
                    System.out.println("被注解的方法: " + method.getName());
                }
                TestAnnotation testAnnotation = method.getAnnotation(TestAnnotation.class);
                if (Objects.nonNull(testAnnotation)) {
                    System.out.println("被测试注解注解的方法: " + method.getName());
                }
            }
​
        }
    }
​
    private void handleClass() {
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(LogMonitor.class);
        System.out.println(beans.keySet());
    }
​
}
复制代码

  通过ApplicationContextAware接口我们可以获取到应用上下文applicationConetext对象,而通过InitalizingBean接口提供的afterPropertiesSet方法,我们可以将注解处理逻辑嵌入到Bean初始化流程当中。从上面的代码可以看到,我们既使用了基于BeanDefinition的方式,也使用了反射机制来获取方法维度的信息。

  但是在这里我们需要注意一个问题,即如果从应用上下中获取到的Bean是一个代理对象,那么上面关于方法级别的处理逻辑还能够生效吗?这里的答案是否定的,因为Spring中使用CGlib生成的代理对象实际是原有对象的子类,而在子类重写父类方法的同时标记在方法维度的注解时不会被继承的,这也就导致了上面关于方法级别的处理逻辑会出现失效的情况。除了方法维度的处理逻辑会遇到这样的问题,类的成员变量维度也会遇到相同的问题。至于如何解决这个问题,笔者的建议是没事就不要混着使用了,还是遵循着Java语法规则来编写代码吧。

2. 基于BeanPostProcessor接口

@Component
public class TestBeanPostProcessor implements BeanPostProcessor {
​
    @Override
    public Object postProcessAfterInitialization(@NotNull Object bean, @NotNull String beanName) throws BeansException {
        Class<?> clazz = bean.getClass();
        Annotation[] annotations = clazz.getAnnotations();
        handleAnnotation(annotations, beanName);
        // 同样,在遇到代理对象的时候这里也会出现同样的问题
        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method method : declaredMethods) {
            handleAnnotation(method.getAnnotations(), method.getName());
        }
        // 同样,在遇到代理对象的时候这里也会出现同样的问题
        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field field : declaredFields) {
            handleAnnotation(field.getAnnotations(), field.getName());
        }
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
​
    private void handleAnnotation(Annotation[] annotations, String name) {
        if (null == annotations) {
            return;
        }
        for (Annotation annotation : annotations) {
            if (annotation.annotationType().equals(LogMonitor.class)) {
                System.out.println("拥有指定注解的测试Bean或方法名称: " + name);
            }
        }
    }
}
复制代码

  通过BeanPostProcessor接口我们可以在Bean的前置/后置处理中进行注解的处理。从上面的代码实现中可以发现,这里只能通过反射机制来完成对应信息的识别和处理。同样,使用这种方式实现的注解处理器也会面临第一种方式遇到的问题。

3. 基于Spring AOP

@Slf4j
@Aspect
@Component
public class LogAspect {
​
    /**
     * 限制时间
     */
    private static final long LIMIT_TIME = 5000;
​
    /**
     * 进行日志切面打印
     *
     * @param joinPoint 切入点
     * @return 处理完成对象
     */
    @Around("@annotation(com.brucebat.hey.push.core.common.annotations.LogMonitor) || @within(com.brucebat.hey.push.core.common.annotations.LogMonitor)")
    public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
        // 针对方法进行拦截
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        LogMonitor logMonitor = method.getAnnotation(LogMonitor.class);
        if (Objects.isNull(logMonitor)) {
            logMonitor = joinPoint.getTarget().getClass().getAnnotation(LogMonitor.class);
        }
        if (Objects.isNull(logMonitor)) {
            return joinPoint.proceed();
        }
        String methodName = logMonitor.methodName();
        if (StringUtils.isBlank(methodName)) {
            String[] classNameArray = method.getDeclaringClass().getName().split("\.");
            methodName = classNameArray[classNameArray.length - 1] + "." + method.getName();
        }
        StopWatch stopWatch = new StopWatch();
        try {
            stopWatch.start(methodName);
            log.info("日志切面打印, method = {}, request = {}", methodName, Objects.isNull(joinPoint.getArgs()) ? "null" : GsonUtils.toJsonString(joinPoint.getArgs()));
            Object object = joinPoint.proceed();
            log.info("日志切面打印, method = {}, response = {}", methodName, Objects.isNull(object) ? "null" : GsonUtils.toJsonString(object));
            return object;
        } finally {
            stopWatch.stop();
            if (stopWatch.getTotalTimeMillis() > LIMIT_TIME) {
                log.info("method = {} 耗时超过限制", methodName);
            }
            log.info("method = {} 耗时 {} 毫秒", methodName, stopWatch.getTotalTimeMillis());
        }
    }
​
}
复制代码

  最后一种方式通过Spring AOP切面编程的方式来完成注解处理。区别于前两种方式是在Spring应用初始化阶段执行并且只能执行一次,使用这种方式实现的注解处理器可以在每次请求执行到被注解标记的类对象或者方法时都会出现处理逻辑。想必这里会有不少同学会问,为什么在上面代码中从方法级别获取注解信息又是可以获取的呢?其实在这里获取到的方法信息是来源于被代理的原始类型信息,不同于上面两种方法从代理对象中直接获取类型信息。原始的类型信息当中一定会包含对应的注解信息,这也是为什么上面关于方法维度的注解识别和处理逻辑是可以生效的。

四、总结

  在本篇文章中,笔者并没有花费笔墨去介绍Spring框架中具体某个原生注解的处理逻辑,而是将目光聚焦于介绍Spring内部实现注解处理的可能方案以及如何依赖Spring提供的能力去实现一个自定义注解处理器。从上面的介绍我们不难发现,实现一个注解处理器的关键就在于抓住如何识别怎样触发,当你明白这两个问题该如何处理之后,即使后面不再使用Spring框架而是转投其他框架的怀抱,也依然能够在对应框架中实现一个符合预期的自定义注解处理器。

  最后,疫情仍在继续,希望大家注意防护,身体健康!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/113534.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

小学生C++编程基础 课程11(共8题)

946.数的数字和(课程A&#xff09; 难度&#xff1a;1 登录 947.数的颠倒 ( 课程A&#xff09; 难度&#xff1a;1 登录 948.求8的个数 (课程A&#xff09; 难度&#xff1a;1 登录 949.删除数字0 (课程A&#xff09; 难度&#xff1a;1 登录 950.垒三角形 (课程A&#xff…

RabbitMQ 第二天 高级 7 RabbitMQ 高级特性 7.6 延迟队列

RabbitMQ 【黑马程序员RabbitMQ全套教程&#xff0c;rabbitmq消息中间件到实战】 文章目录RabbitMQ第二天 高级7 RabbitMQ 高级特性7.6 延迟队列7.6.1 延迟队列概述7.6.2 代码实现7.6.3 小结第二天 高级 7 RabbitMQ 高级特性 7.6 延迟队列 7.6.1 延迟队列概述 【重点】 延…

Leetcode 剑指 Offer II 007. 数组中和为 0 的三个数

题目难度: 中等 原题链接 今天继续更新 Leetcode 的剑指 Offer&#xff08;专项突击版&#xff09;系列, 大家在公众号 算法精选 里回复 剑指offer2 就能看到该系列当前连载的所有文章了, 记得关注哦~ 题目描述 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i…

微信开放平台之第三方平台开发,从哪里入手?

大家好&#xff0c;我是悟空码字 疫情之下&#xff0c;最近有不少兄弟没有挺进决赛&#xff0c;半途成了小羊人&#xff0c;可谓是出师未捷身先死。话说回来&#xff0c;不管怎么样&#xff0c;尽量保护好自己&#xff0c;能越晚变羊越好。 开始说正事&#xff0c;不管是自己…

46_SDIO实验

目录 SDIO相关结构体 SDIO初始化结构体 SDIO命令初始化结构体 SDIO数据初始化结构体 硬件连接 实验源码 SDIO相关结构体 标准库函数对SDIO外设建立了三个初始化结构体&#xff0c;分别为SDIO初始化结构体SDIO_InitTypeDef, SDIO命令初始化结构体SDIO_CmdInitTypeDef和SD…

小技巧2:Python 实现阿拉伯数字转化为中文数字

大家好&#xff0c;我是Kamen Black君&#xff0c;今天给大家介绍一个小技巧&#xff1a;如何用Python 代码实现阿拉伯数字转化为中文数字。 都说光阴似箭&#xff0c;日月如梭&#xff0c;2022年的车轮很快也要驶向了终点。不知道大家在平常的生活中&#xff0c;有没有碰到过…

【Python】Beta分布详解

投硬币&#xff0c;硬币是正还是反&#xff0c;这属于两点分布的问题。 疯狂投硬币&#xff0c;正面出现的次数&#xff0c;服从二项分布&#xff1a;【Python】从二项分布到泊松分布 二项分布中&#xff0c;若特定时间内的伯努利试验次数趋于无穷大&#xff0c;那么在某一时…

【Linux】进程控制(进程创建、进程终止、进程等待、进程替换)

文章目录一、进程创建1.1 认识系统调用 fork1.2 理解 fork 的返回值1.3 写时拷贝策略二、进程终止2.1 main 函数的返回值2.2 进程退出的几种情况(&#x1f31f;)2.3 进程退出码2.4 终止正常进程&#xff1a;return、exit、_exit ⭐2.5 站在 OS 角度&#xff1a;理解进程终止三、…

RV1126笔记二十:吸烟行为检测及部署<七>

若该文为原创文章,转载请注明原文出处。 部署到RV1126,Demo测试 一、介绍 通过训练转换后,得到了RKNN模型,接下来使用rknn_model_zoo里自带的C demo来测试模型是不是可以在RV1126上运行。 C demo直接编译是编译不过的,需要自己移植. 根据C demo提供的README,可以看出…

7段数码管和打印机接口

目录 七段发光二级管显示器接口&#xff08;重点&#xff09; 打印机适配器&#xff08;重点&#xff09; 例题 补充两个芯片(了解&#xff09; 数据输出寄存器 数据输入三态缓冲器 七段发光二级管显示器接口&#xff08;重点&#xff09; 灯泡的题最难就是7段数码管。重点…

【Web开发】Python实现Web服务器(Ubuntu下调试Flask)

&#x1f37a;基于Python的Web服务器系列相关文章编写如下&#x1f37a;&#xff1a; &#x1f388;【Web开发】Python实现Web服务器&#xff08;Flask快速入门&#xff09;&#x1f388;&#x1f388;【Web开发】Python实现Web服务器&#xff08;Flask案例测试&#xff09;&a…

工厂卖家如何借助TikTok突围?

众所周知&#xff0c;TikTok已然成为全球最受欢迎的社交媒体之一&#xff0c;拥有巨大的流量池&#xff0c;对于跨境电商卖家来说&#xff0c;TikTok也是最大的站外流量来源。作为月活跃用户接近16亿的应用程序&#xff0c;TikTok的发展速度让很多社交媒体平台望尘莫及&#xf…

node.js+uni计算机毕设项目基于微信小程序寸金校园租车平台(程序+小程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等…

rabbitmq基础6——交换器和队列web监控基础运维、备份交换机、消息确认机制、消息状态查看

文章目录一、交换器和队列的使用1.1 web监控创建交换器1.1.1 交换器参数1.1.2 备份交换器1.1.2.1 工作原理1.1.2.2 弊端情形1.2 web监控创建队列1.2.1 队列参数1.2.1.1 通用参数1.2.1.2 其他参数1.2.1.2.1 所有队列1.2.1.2.2 主队列1.2.1.2.3 仲裁队列1.2.1.2.4 流队列1.3 web监…

从原理和源码梳理Springboot FatJar 的机制

一、概述 SpringBoot FatJar 的设计&#xff0c;打破了标准 jar 的结构&#xff0c;在 jar 包内携带了其所依赖的 jar 包&#xff0c;通过 jar 中的 main 方法创建自己的类加载器&#xff0c;来识别加载运行其不规范的目录下的代码和依赖。 二、标准的 jar 包结构 打开 Java…

用html实现一个静态登陆页面-可根据需求更改样式

一、创建html文件&#xff0c;vscode下载相关插件 我们先选择一个文件夹创建login.html&#xff0c;.之前的文件命无所谓&#xff0c;.之后就必须为html或者htm。 在编辑改文件输入!且出现提示后按回车或按tab快捷生成基础代码。 然后我们下载一个可以方便我们开发的插件。 …

【计算机网络课程设计】TCP协议包自动生成工具【蒙混过关版】

文章目录引言设计要求分工安排文献查阅总体设计流程具体设计内容&#x1f315;博客x主页&#xff1a;己不由心王道长&#x1f315;! &#x1f30e;文章说明&#xff1a;TCP协议包自动生成工具&#x1f30e; ✅系列专栏&#xff1a;计算机网络 &#x1f334;本篇内容&#xff1a…

Linux系统基础——系统调用

Linux系统调用 特此说明: 刘超的趣谈linux操作系统是比较重要的参考资料&#xff0c;本文大部分内容和所有图片来源于这个专栏。 1 相关概念 程序vs进程vs命令: Linux系统上所有的操作由进程完成&#xff0c;进程的运行是动态的&#xff0c;在此之前是一个静态的程序。用户用一…

2年过去了,有谁还记得曾想取代Node.js的他?

大家好&#xff0c;我卡颂。 22年11月14日&#xff0c;Deno发布了v1.28&#xff0c;距离他第一个稳定版本v1.0.0发布&#xff08;2020年5月13日&#xff09;已过去2年。 作为Node.js的竞争者&#xff0c;Deno似乎并没有达到取代前者的目标。 甚至&#xff0c;他在前端社区的…

离散数学数理逻辑部分【1】

前言 本文创作的起因是&#xff0c;经历了离散数学的学习&#xff0c;深感学习离散之艰辛。所以产生了写一些内容帮助大家期末复习。虽然在csdn发表本文&#xff0c;有些不太合适&#xff0c;但是还是相信本文的质量和内容&#xff0c;可以给正在学习离散数学的大学生提供一些…