当前位置: 首页 > news >正文

Spring AOP:this 调用当前类方法无法被拦截

问题复现

  • 假设我们正在开发一个宿舍管理系统,这个模块包含一个负责电费充值的类 ElectricService,它含有一个充电方法 charge():

    @Service
    public class ElectricService {public void charge() throws Exception {System.out.println("Electric charging ...");this.pay();}public void pay() throws Exception {System.out.println("Pay with alipay ...");Thread.sleep(1000);}}
    
  • 在这个电费充值方法 charge() 中,我们会使用支付宝进行充值。因此在这个方法中,我加入了 pay() 方法。为了模拟 pay() 方法调用耗时,代码执行了休眠 1 秒,并在 charge() 方法里使用 this.pay() 的方式调用这种支付方法。

  • 但是因为支付宝支付是第三方接口,我们需要记录下接口调用时间。这时候我们就引入了一个 @Around 的增强 ,分别记录在 pay() 方法执行前后的时间,并计算出执行 pay() 方法的耗时。

    @Aspect
    @Service
    @Slf4j
    public class AopConfig {@Around("execution(* com.spring.puzzle.class5.example1.ElectricService.pay()) ")public void recordPayPerformance(ProceedingJoinPoint joinPoint) throws Throwable {long start = System.currentTimeMillis();joinPoint.proceed();long end = System.currentTimeMillis();System.out.println("Pay method time cost(ms): " + (end - start));}
    }
    
  • 最后我们再通过定义一个 Controller 来提供电费充值接口,定义如下:

    @RestController
    public class HelloWorldController {@AutowiredElectricService electricService;@RequestMapping(path = "charge", method = RequestMethod.GET)public void charge() throws Exception{electricService.charge();};
    }
    
  • 完成代码后,我们访问上述接口,会发现这段计算时间的切面并没有执行到,输出日志如下:

    Electric charging ...
    Pay with alipay ...
    
  • 回溯之前的代码可知,在 @Around 的切面类中,我们很清晰地定义了切面对应的方法,但是却没有被执行到。这说明了在类的内部,通过 this 方式调用的方法,是没有被 Spring AOP 增强的。这是为什么呢?我们来分析一下。

案例解析

  • 我们可以从源码中找到真相。首先来设置个断点,调试看看 this 对应的对象是什么样的:
    img
  • 可以看到,this 对应的就是一个普通的 ElectricService 对象,并没有什么特别的地方。再看看在 Controller 层中自动装配的 ElectricService 对象是什么样:
    img
  • 可以看到,这是一个被 Spring 增强过的 Bean,所以执行 charge() 方法时,会执行记录接口调用时间的增强操作。而 this 对应的对象只是一个普通的对象,并没有做任何额外的增强。
  • 为什么 this 引用的对象只是一个普通对象呢?这还要从 Spring AOP 增强对象的过程来看。但在此之前,有些基础我需要在这里强调下。
1. Spring AOP 的实现
  • Spring AOP 的底层是动态代理。而创建代理的方式有两种,JDK 的方式和 CGLIB 的方式。JDK 动态代理只能对实现了接口的类生成代理,而不能针对普通类。而 CGLIB 是可以针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,来实现代理对象。具体区别可参考下图:
    img
2. 如何使用 Spring AOP
  • 在 Spring Boot 中,我们一般只要添加以下依赖就可以直接使用 AOP 功能:

    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
  • 而对于非 Spring Boot 程序,除了添加相关 AOP 依赖项外,我们还常常会使用 @EnableAspectJAutoProxy 来开启 AOP 功能。这个注解类引入(Import)AspectJAutoProxyRegistrar,它通过实现 ImportBeanDefinitionRegistrar 的接口方法来完成 AOP 相关 Bean 的准备工作。

  • 补充完最基本的 Spring 底层知识和使用知识后,我们具体看下创建代理对象的过程。先来看下调用栈:
    img

  • 创建代理对象的时机就是创建一个 Bean 的时候,而创建的的关键工作其实是由 AnnotationAwareAspectJAutoProxyCreator 完成的。它本质上是一种 BeanPostProcessor。所以它的执行是在完成原始 Bean 构建后的初始化 Bean(initializeBean)过程中。而它到底完成了什么工作呢?我们可以看下它的 postProcessAfterInitialization 方法:

    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {if (bean != null) {Object cacheKey = getCacheKey(bean.getClass(), beanName);if (this.earlyProxyReferences.remove(cacheKey) != bean) {return wrapIfNecessary(bean, beanName, cacheKey);}}return bean;
    }
    
  • 上述代码中的关键方法是 wrapIfNecessary,顾名思义,在需要使用 AOP 时,它会把创建的原始的 Bean 对象 wrap 成代理对象作为 Bean 返回。具体到这个 wrap 过程,可参考下面的关键代码行:

    protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {// 省略非关键代码Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);if (specificInterceptors != DO_NOT_PROXY) {this.advisedBeans.put(cacheKey, Boolean.TRUE);Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));this.proxyTypes.put(cacheKey, proxy.getClass());return proxy;}// 省略非关键代码 
    }
    
  • 上述代码中,第 6 行的 createProxy 调用是创建代理对象的关键。具体到执行过程,它首先会创建一个代理工厂,然后将通知器(advisors)、被代理对象等信息加入到代理工厂,最后通过这个代理工厂来获取代理对象。一些关键过程参考下面的方法:

    protected Object createProxy(Class<?> beanClass, @Nullable String beanName,@Nullable Object[] specificInterceptors, TargetSource targetSource) {// 省略非关键代码ProxyFactory proxyFactory = new ProxyFactory();if (!proxyFactory.isProxyTargetClass()) {if (shouldProxyTargetClass(beanClass, beanName)) {proxyFactory.setProxyTargetClass(true);}else {evaluateProxyInterfaces(beanClass, proxyFactory);}}Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);proxyFactory.addAdvisors(advisors);proxyFactory.setTargetSource(targetSource);customizeProxyFactory(proxyFactory);// 省略非关键代码return proxyFactory.getProxy(getProxyClassLoader());
    }
    
  • 经过这样一个过程,一个代理对象就被创建出来了。我们从 Spring 中获取到的对象都是这个代理对象,所以具有 AOP 功能。而之前直接使用 this 引用到的只是一个普通对象,自然也就没办法实现 AOP 的功能了。

问题修正

  • 从上述案例解析中,我们知道,只有引用的是被动态代理创建出来的对象,才会被 Spring 增强,具备 AOP 该有的功能。那什么样的对象具备这样的条件呢?
  • 有两种。一种是被 @Autowired 注解的,于是我们的代码可以改成这样,即通过 @Autowired 的方式,在类的内部,自己引用自己:
    @Service
    public class ElectricService {@AutowiredElectricService electricService;public void charge() throws Exception {System.out.println("Electric charging ...");//this.pay();electricService.pay();}public void pay() throws Exception {System.out.println("Pay with alipay ...");Thread.sleep(1000);}
    }
    
  • 另一种方法就是直接从 AopContext 获取当前的 Proxy。那你可能会问了,AopContext 是什么?简单说,它的核心就是通过一个 ThreadLocal 来将 Proxy 和线程绑定起来,这样就可以随时拿出当前线程绑定的 Proxy。
  • 不过使用这种方法有个小前提,就是需要在 @EnableAspectJAutoProxy 里加一个配置项 exposeProxy = true,表示将代理对象放入到 ThreadLocal,这样才可以直接通过 AopContext.currentProxy() 的方式获取到,否则会报错如下:
    img
  • 按这个思路,我们修改下相关代码:
    import org.springframework.aop.framework.AopContext;
    import org.springframework.stereotype.Service;
    @Service
    public class ElectricService {public void charge() throws Exception {System.out.println("Electric charging ...");ElectricService electric = ((ElectricService) AopContext.currentProxy());electric.pay();}public void pay() throws Exception {System.out.println("Pay with alipay ...");Thread.sleep(1000);}
    }
    
  • 同时,不要忘记修改 EnableAspectJAutoProxy 注解的 exposeProxy 属性,示例如下:
    @SpringBootApplication
    @EnableAspectJAutoProxy(exposeProxy = true)
    public class Application {// 省略非关键代码
    }
    
  • 这两种方法的效果其实是一样的,最终我们打印出了期待的日志,到这,问题顺利解决了。
    Electric charging ...
    Pay with alipay ...
    Pay method time cost(ms): 1005
    

相关文章:

Spring AOP:this 调用当前类方法无法被拦截

问题复现 假设我们正在开发一个宿舍管理系统&#xff0c;这个模块包含一个负责电费充值的类 ElectricService&#xff0c;它含有一个充电方法 charge()&#xff1a; Service public class ElectricService {public void charge() throws Exception {System.out.println("E…...

K8S-LLM:用自然语言轻松操作 Kubernetes

在 Kubernetes (K8s) 的日常管理中&#xff0c;复杂的命令行操作常常让开发者感到头疼。无论是部署应用、管理资源还是调试问题&#xff0c;都需要记住大量的命令和参数。Kubernetes 作为容器编排的行业标准&#xff0c;其强大的功能伴随着陡峭的学习曲线和复杂的命令行操作。这…...

lua和C API库一些记录

相关头文件解释 lua.h&#xff1a;声明lua提供的基础函数&#xff0c;所有内容都有个前缀lua_&#xff1b; luaxlib.h&#xff1a;声明辅助库提供的函数&#xff0c;所有内容都有个前缀luaL_&#xff1b; lualib.h&#xff1a;声明了打开标准库的函数&#xff1b; 辅助库对…...

SpringSecurity中的过滤器链与自定义过滤器

关于 Spring Security 框架中的过滤器的使用方法,系列文章: 《SpringSecurity中的过滤器链与自定义过滤器》 《SpringSecurity使用过滤器实现图形验证码》 1、Spring Security 中的过滤器链 Spring Security 中的过滤器链(Filter Chain)是一个核心的概念,它定义了一系列过…...

Slate文档编辑器-Decorator装饰器渲染调度

Slate文档编辑器-Decorator装饰器渲染调度 在之前我们聊到了基于文档编辑器的数据结构设计&#xff0c;聊了聊基于slate实现的文档编辑器类型系统&#xff0c;那么当前我们来研究一下slate编辑器中的装饰器实现。装饰器在slate中是非常重要的实现&#xff0c;可以为我们方便地…...

本地Docker部署Flowise并实现远程构建LLM应用程序原型高效开发

文章目录 前言1. Docker安装Flowise2. Ubuntu安装Cpolar3. 配置Flowise公网地址4. 远程访问Flowise5. 固定Cpolar公网地址6. 固定地址访问 前言 相信很多对AI感兴趣的小伙伴都会觉得正在逐渐流行的工作流自动化和AI集成特别酷炫&#xff0c;没错&#xff0c;这些技术像“秘密武…...

多点通信、流式域套接字

一、广播 1.1广播的发送端模型&#xff1a; #include<myhead.h>#define BEN_IP "192.168.191.129" #define BEN_PORT 8888#define PORT 6666int main(int argc, const char *argv[]) {int oldfd socket(AF_INET,SOCK_DGRAM,0);if(oldfd -1){perror("soc…...

vue3使用video-player实现视频播放(可拖动视频窗口、调整大小)

1.安装video-player npm install video.js videojs-player/vue --save在main.js中配置全局引入 // 导入视频播放组件 import VueVideoPlayer from videojs-player/vue import video.js/dist/video-js.cssconst app createApp(App) // 视频播放组件 app.use(VueVideoPlayer)2…...

模块化和面向接口的设计:深入理解和应用

模块化和面向接口的设计&#xff1a;深入理解和应用 在面向对象编程中&#xff0c;模块化 和 面向接口设计 是两种非常重要的编程理念。它们能帮助开发人员构建更加清晰、可维护和易于扩展的系统。接下来&#xff0c;我们将详细解释这两种设计思想&#xff0c;并结合 Python 中…...

《SwiftUI 实现点击按钮播放 MP3 音频》

功能介绍 点击按钮时&#xff0c;应用会播放名为 yinpin.mp3 的音频文件。使用 AVAudioPlayer 来加载和播放音频。 关键点&#xff1a; 按钮触发&#xff1a;点击按钮会调用 playAudio() 播放音频。音频加载&#xff1a;通过 Bundle.main.url(forResource:) 加载音频文件。播…...

微机接口课设——基于Proteus和8086的打地鼠设计(8255、8253、8259)Proteus中Unknown 1-byte opcode / Unknown 2-byte opcode错误

原理图设计 汇编代码 ; I/O 端口地址定义 IOY0 EQU 0600H IOY1 EQU 0640H IOY2 EQU 0680HMY8255_A EQU IOY000H*2 ; 8255 A 口端口地址 MY8255_B EQU IOY001H*2 ; 8255 B 口端口地址 MY8255_C EQU IOY002H*2 ; 8255 C 口端口地址 MY8255_MODE EQU IOY003H*2 ; …...

MySQL如何执行.sql 文件:详细教学指南

在使用MySQL数据库过程中&#xff0c;我们经常需要执行包含SQL语句的.sql文件。这些文件通常用于数据库的备份和恢复或批量执行SQL脚本。本文将详细介绍如何在不同环境下执行MySQL的.sql文件。 前置准备 在开始之前&#xff0c;请确保以下条件已经满足&#xff1a; 已经安装…...

非周期性脑活动的动态重构支持癫痫患者的认知功能:一种神经指纹识别方法

摘要 颞叶癫痫(TLE)的特征是大脑活动模式发生大规模的变化&#xff0c;并且这种变化与患者的认知功能受损密切相关。本研究旨在使用神经指纹方法分析大脑活动的动态重构&#xff0c;以描绘TLE患者的个体特征及其认知功能相关性。本研究收集了68名TLE患者和34名对照组的10min静息…...

ZYNQ初识6(zynq_7010)clock时钟IP核

基于板子的PL端无时钟晶振&#xff0c;需要从PS端借用clock1&#xff08;50M&#xff09;晶振 接下去是自定义clock的IP核封装&#xff0c;为后续的simulation可以正常仿真波形&#xff0c;需要注意顶层文件的设置&#xff0c;需要将自定义的IP核对应的.v文件设置为顶层文件&a…...

使用MFC编写一个paddleclas预测软件

目录 写作目的 环境准备 下载编译环境 解压预编译库 准备训练文件 模型文件 图像文件 路径整理 准备预测代码 创建预测应用 新建mfc应用 拷贝文档 配置环境 界面布局 添加回cpp文件 修改函数 报错1解决 报错2未解决 修改infer代码 修改MFCPaddleClasDlg.cp…...

SAP SD BP名称和销售订单描述的对应不起来的问题

问题 VBPA-ADRNR地址 和 KNA1-ADRNR 指向同一个号码 销售订单读取这个地址 改正后恢复正常 原因&#xff1a;推测 应该是创建Y0 电商客户的时候&#xff0c;引起锁和混乱导致的。 具体实际时什么样&#xff0c;不太清楚 写于20241230 浙江台州...

FlastOcc-网络复现-1.环境配置及问题

研究OCC网络 1.RuntimeError: Ninja is required to load C extensions RuntimeError: Ninja is required to load C extensions #32 Ninja is required to load C extensions File “/FlashOCC/projects/mmdet3d_plugin/core/evaluation/ray_metrics.py”, line 12, in dvr …...

Go语言中值接收者和指针接收者的区别?

在 Go 语言中&#xff0c;值接收者和指针接收者是方法定义中的两种接收者类型。它们的主要区别在于方法调用时的行为、接收者是否可以被修改&#xff0c;以及性能上的差异。 值接收者 定义 值接收者的方法接收的是调用对象的一个副本&#xff0c;方法内部对该副本的修改不会影…...

kafka小实站

需要先在前面的文章里面照着下载好kafka&#xff0c;并且启动 先启动zookeeper 项目目录 package kafka; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.springframework.kafka.annotation.KafkaListener; import…...

基于Python实现车辆检测、机动车检测、识别位置标记、计数

目录 引言背景与应用场景车辆检测的研究意义相关工作车辆检测概述机动车检测方法分类基于传统计算机视觉的检测方法基于深度学习的检测方法技术与方法车辆检测技术概述基于Python的车辆检测方法图像处理与特征提取深度学习方法(如YOLO、SSD、Faster R-CNN等)数据集与标注常用…...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…...

【人工智能】神经网络的优化器optimizer(二):Adagrad自适应学习率优化器

一.自适应梯度算法Adagrad概述 Adagrad&#xff08;Adaptive Gradient Algorithm&#xff09;是一种自适应学习率的优化算法&#xff0c;由Duchi等人在2011年提出。其核心思想是针对不同参数自动调整学习率&#xff0c;适合处理稀疏数据和不同参数梯度差异较大的场景。Adagrad通…...

解锁数据库简洁之道:FastAPI与SQLModel实战指南

在构建现代Web应用程序时&#xff0c;与数据库的交互无疑是核心环节。虽然传统的数据库操作方式&#xff08;如直接编写SQL语句与psycopg2交互&#xff09;赋予了我们精细的控制权&#xff0c;但在面对日益复杂的业务逻辑和快速迭代的需求时&#xff0c;这种方式的开发效率和可…...

Module Federation 和 Native Federation 的比较

前言 Module Federation 是 Webpack 5 引入的微前端架构方案&#xff0c;允许不同独立构建的应用在运行时动态共享模块。 Native Federation 是 Angular 官方基于 Module Federation 理念实现的专为 Angular 优化的微前端方案。 概念解析 Module Federation (模块联邦) Modul…...

稳定币的深度剖析与展望

一、引言 在当今数字化浪潮席卷全球的时代&#xff0c;加密货币作为一种新兴的金融现象&#xff0c;正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而&#xff0c;加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下&#xff0c;稳定…...

python执行测试用例,allure报乱码且未成功生成报告

allure执行测试用例时显示乱码&#xff1a;‘allure’ &#xfffd;&#xfffd;&#xfffd;&#xfffd;&#xfffd;ڲ&#xfffd;&#xfffd;&#xfffd;&#xfffd;ⲿ&#xfffd;&#xfffd;&#xfffd;Ҳ&#xfffd;&#xfffd;&#xfffd;ǿ&#xfffd;&am…...

SiFli 52把Imagie图片,Font字体资源放在指定位置,编译成指定img.bin和font.bin的问题

分区配置 (ptab.json) img 属性介绍&#xff1a; img 属性指定分区存放的 image 名称&#xff0c;指定的 image 名称必须是当前工程生成的 binary 。 如果 binary 有多个文件&#xff0c;则以 proj_name:binary_name 格式指定文件名&#xff0c; proj_name 为工程 名&…...

DingDing机器人群消息推送

文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人&#xff0c;点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置&#xff0c;详见说明文档 成功后&#xff0c;记录Webhook 2 API文档说明 点击设置说明 查看自…...

ubuntu22.04 安装docker 和docker-compose

首先你要确保没有docker环境或者使用命令删掉docker sudo apt-get remove docker docker-engine docker.io containerd runc安装docker 更新软件环境 sudo apt update sudo apt upgrade下载docker依赖和GPG 密钥 # 依赖 apt-get install ca-certificates curl gnupg lsb-rel…...

CppCon 2015 学习:Time Programming Fundamentals

Civil Time 公历时间 特点&#xff1a; 共 6 个字段&#xff1a; Year&#xff08;年&#xff09;Month&#xff08;月&#xff09;Day&#xff08;日&#xff09;Hour&#xff08;小时&#xff09;Minute&#xff08;分钟&#xff09;Second&#xff08;秒&#xff09; 表示…...