当前位置: 首页 > 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等)数据集与标注常用…...

Appium+python自动化(十六)- ADB命令

简介 Android 调试桥(adb)是多种用途的工具&#xff0c;该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具&#xff0c;其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利&#xff0c;如安装和调试…...

【Java学习笔记】Arrays类

Arrays 类 1. 导入包&#xff1a;import java.util.Arrays 2. 常用方法一览表 方法描述Arrays.toString()返回数组的字符串形式Arrays.sort()排序&#xff08;自然排序和定制排序&#xff09;Arrays.binarySearch()通过二分搜索法进行查找&#xff08;前提&#xff1a;数组是…...

Spring AI 入门:Java 开发者的生成式 AI 实践之路

一、Spring AI 简介 在人工智能技术快速迭代的今天&#xff0c;Spring AI 作为 Spring 生态系统的新生力量&#xff0c;正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务&#xff08;如 OpenAI、Anthropic&#xff09;的无缝对接&…...

华为云Flexus+DeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建

华为云FlexusDeepSeek征文&#xff5c;DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建 前言 如今大模型其性能出色&#xff0c;华为云 ModelArts Studio_MaaS大模型即服务平台华为云内置了大模型&#xff0c;能助力我们轻松驾驭 DeepSeek-V3/R1&#xff0c;本文中将分享如何…...

select、poll、epoll 与 Reactor 模式

在高并发网络编程领域&#xff0c;高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表&#xff0c;以及基于它们实现的 Reactor 模式&#xff0c;为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。​ 一、I…...

Springboot社区养老保险系统小程序

一、前言 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;社区养老保险系统小程序被用户普遍使用&#xff0c;为方…...

以光量子为例,详解量子获取方式

光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学&#xff08;silicon photonics&#xff09;的光波导&#xff08;optical waveguide&#xff09;芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中&#xff0c;光既是波又是粒子。光子本…...

网站指纹识别

网站指纹识别 网站的最基本组成&#xff1a;服务器&#xff08;操作系统&#xff09;、中间件&#xff08;web容器&#xff09;、脚本语言、数据厍 为什么要了解这些&#xff1f;举个例子&#xff1a;发现了一个文件读取漏洞&#xff0c;我们需要读/etc/passwd&#xff0c;如…...

WebRTC从入门到实践 - 零基础教程

WebRTC从入门到实践 - 零基础教程 目录 WebRTC简介 基础概念 工作原理 开发环境搭建 基础实践 三个实战案例 常见问题解答 1. WebRTC简介 1.1 什么是WebRTC&#xff1f; WebRTC&#xff08;Web Real-Time Communication&#xff09;是一个支持网页浏览器进行实时语音…...

人工智能--安全大模型训练计划:基于Fine-tuning + LLM Agent

安全大模型训练计划&#xff1a;基于Fine-tuning LLM Agent 1. 构建高质量安全数据集 目标&#xff1a;为安全大模型创建高质量、去偏、符合伦理的训练数据集&#xff0c;涵盖安全相关任务&#xff08;如有害内容检测、隐私保护、道德推理等&#xff09;。 1.1 数据收集 描…...