【Spring】Spring AOP入门及实现原理剖析

文章目录

  • 1 初探Aop
    • 1.1 何为AOP?
    • 1.2 AOP的组成
      • 1.2.1 切面(Aspect)
      • 1.2.2 连接点(Join Point)
      • 1.2.3 切点(Pointcut)
      • 1.2.4 通知(Advice)
    • 1.3 AOP的使用场景
  • 2 Spring AOP入门
    • 2.1 添加 Spring AOP 框架⽀持
    • 2.2 定义切面和切点
    • 2.3 定义相关通知
  • 3 Spring AOP实现原理
    • 3.1 何为动态代理?
    • 3.2 JDK 动态代理实现
    • 3.3 CGLIB 动态代理实现
    • 3.4 两种方式的区别
  • 写在最后


1 初探Aop

1.1 何为AOP?

AOP (Aspect-Oriented Programming) 是一种编程范式,它提供一种将程序中的横切关注点模块化的方式。横切关注点可以是日志、事务、安全等,它们不属于业务逻辑,但是又必须要与业务逻辑紧密耦合在一起。在 AOP 中,我们将这些横切关注点称为“切面”,它们独立于业务逻辑模块,但是可以在程序运行的不同阶段被织入到业务逻辑中。使用 AOP 可以提高代码复用性、降低模块之间的耦合度、简化代码的维护性等。

1.2 AOP的组成

AOP由切面、切点、连接点和通知组成。

1.2.1 切面(Aspect)

切面是包含了通知、切点和切面的类,相当于AOP实现的某个功能的集合。通俗理解,在程序中就是一个处理某方面具体问题的一个类。里面包含了许多方法,这些方法就是切点和通知。

1.2.2 连接点(Join Point)

应⽤执⾏过程中能够插⼊切⾯的⼀个点,这个点可以是⽅法调⽤时,抛出异常时,甚⾄修改字段时。切⾯代码可以利⽤这些点插⼊到应⽤的正常流程之中,并添加新的⾏为。连接点可以理解为可能会触发AOP规则的所有点。 狭义可以理解为需要进行功能增强的方法。

1.2.3 切点(Pointcut)

切点是连接点的集合。它定义了在哪些连接点上应用特定的通知。通过使用切点表达式,可以根据连接点的特征(例如方法签名或类名)选择特定的连接点。即,切点是用来进行主动拦截的规则(配置)。
具体来说:Pointcut 的作⽤就是提供⼀组规则(使⽤ AspectJ pointcut expression language 来描述)来匹配 Join Point,给满⾜规则的 Join Point 添加 Advice。

1.2.4 通知(Advice)

在AOP术语中,切面的工作被称之为通知。 通知是切面在连接点上执行的动作。它定义了在何时(例如在方法调用之前或之后)以及如何(例如打印日志或进行性能监控)应用切面的行为。即,程序中被拦截请求触发的具体动作。

1.3 AOP的使用场景

回顾下笔者之前的文章,基于Servlet实现的前后端分离的博客系统中,除了登录等⼏个功能不需要做⽤户登录验证之外,其他⼏乎所有⻚⾯调⽤的前端控制器( Controller)都需要先验证⽤户登录的状态。然⽽,当系统的功能越来越多,则要写的登录验证也越来越多,一旦某些功能需要改动,这种处理方式由于耦合性很高,牵一发就会动全身。⽽这些⽅法⼜是相同的,对于这种功能统⼀,且使⽤的地⽅较多的功能,就可以考虑 AOP来统⼀处理了。
例如,原本的博客系统在作者删除、发布、浏览博客前都需要进行登录状态的验证,如果用户未登录,则请求重定向到登录界面。使用AOP后,在用户调用Server服务之前,统一进行校验,验证通过则正常服务,否则,被“拦截”。
AOP登录拦截
除了统一登录判断外,使用AOP还可以实现:

  • 统⼀⽇志记录
  • 统⼀⽅法执⾏时间统计
  • 统⼀的返回格式设置
  • 统⼀的异常处理
  • 事务的开启和提交等

2 Spring AOP入门

以上,我们已经对AOP有了基本的了解。接下来,我们的目标是尝试使用Spring AOP来实现AOP的功能,完成的目标如下:

拦截所有StudentController里的方法,即每次调用StudentController中的任意方法的时候,执行相应的通知事件。

Spring AOP 的实现步骤如下:

  1. 添加 Spring AOP 框架⽀持。
  2. 定义切⾯和切点。
    (1)创建切面类
    (2)配置拦截规则
  3. 定义通知。

2.1 添加 Spring AOP 框架⽀持

首先,创建Spring Boot项目
Spring Boot项目
在pom.xml中添加Spring AOP的依赖配置:

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.2 定义切面和切点

使用 @Aspect 注解表明当前类为一个切面,而在切点中,我们要定义拦截的规则,具体实现如下:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect // 表明此类为一个切面
@Component // 随着框架的启动而启动
public class StudentAspect {
    // 定义切点, 这里使用 Aspect 表达式语法
    @Pointcut("execution(* com.hxh.demo.controller.StudentController.*(..))")
    public void pointcut(){ }
}

在上述实现代码中,pointcut 为一个空方法,只是起到一个“标识”的作用,即,标识下面的通知方法具体指的是哪个切点,切点可以有多个。

切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:

execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
修饰符和异常可以省略

*常见的切点表达式的示例:

  • 匹配特定类的所有方法:
    execution(* com.example.MyClass.*(..)):匹配 com.example.MyClass 类中的所有方法。
  • 匹配特定包下的所有方法:
    execution(* com.example.*.*(..)):匹配 com.example 包及其子包下的所有方法。
  • 匹配特定注解标注的方法:
    execution(@com.example.MyAnnotation * *(..)):匹配被 com.example.MyAnnotation 注解标注的所有方法。
  • 匹配特定方法名的方法:
    execution(* com.example.MyClass.myMethod(..)):匹配 com.example.MyClass 类中名为 myMethod 的方法。
  • 匹配特定方法参数类型的方法:
    execution(* com.example.MyClass.myMethod(String, int)):匹配 com.example.MyClass 类中具有一个 String 参数和一个 int 参数的 myMethod 方法。
  • 匹配特定返回类型的方法:
    execution(String com.example.MyClass.myMethod(..)):匹配 com.example.MyClass 类中返回类型为 String 的 myMethod 方法。

StudentController.java

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/student")
public class StudentController {

    @RequestMapping("/hi")
    public String sayHi(String name) {
        System.out.println("执行了 sayHi 方法~");
        return "Hi," + name;
    }

    @RequestMapping("/hello")
    public String sayHello() {
        System.out.println("执行了 sayHello 方法~");
        return "Hello, hxh";
    }
}

2.3 定义相关通知

通知定义的是被拦截方法具体要执行的业务。
Spring 切⾯类中,可以在⽅法上使⽤以下注解,会设置⽅法为通知⽅法,在满⾜条件后会通知本⽅法进⾏调⽤:

  • 前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。
  • 后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。
  • 返回之后通知使⽤ @AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。
  • 抛异常后通知使⽤ @AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。
  • 环绕通知使⽤ @Around:通知包裹了被通知的⽅法,在被通知的⽅法调用之前和调⽤之后执⾏⾃定义的⾏为。

具体实现如下:

前置通知与后置通知(异常通知和返回后通知仅仅是注解不同,方式一致,这里不再赘述~)

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect // 表明此类为一个切面
@Component // 随着框架的启动而启动
public class StudentAspect {
    // 定义切点, 这里使用 Aspect 表达式语法
    @Pointcut("execution(* com.hxh.demo.controller.StudentController.*(..))")
    public void pointcut(){ }

    // 前置通知
    @Before("pointcut()")
    public void beforeAdvice() {
        System.out.println("执行了前置通知~");
    }

    // 后置通知
    @After("pointcut()")
    public void afterAdvice() {
        System.out.println("执行了后置通知~");
    }
}

实现结果

环绕通知的具体实现
环绕通知是有Object返回值的,需要把执行流程的结果返回给框架,框架拿到对象继续执行。

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect // 表明此类为一个切面
@Component // 随着框架的启动而启动
public class StudentAspect {
    // 定义切点, 这里使用 Aspect 表达式语法
    @Pointcut("execution(* com.hxh.demo.controller.StudentController.*(..))")
    public void pointcut(){ }

    // 环绕通知
    @Around("pointcut()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
        System.out.println("进入环绕通知~");
        Object obj = null;
        // 执行目标方法
        try {
            obj = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("退出环绕通知~");
        return obj;
    }
    
}

实现结果


3 Spring AOP实现原理

Spring AOP 是通过动态代理的⽅式,在运⾏期将 AOP 代码织⼊到程序中的,它的实现⽅式有两种:JDK ProxyCGLIB。因此,Spring 对 AOP 的支持局限于方法级别的拦截。

  • CGLIB是Java中的动态代理框架,主要作⽤就是根据⽬标类和⽅法,动态⽣成代理类。
  • Java中的动态代理框架,⼏乎都是依赖字节码框架(如 ASM,Javassist 等)实现的。
  • 字节码框架是直接操作 class 字节码的框架。可以加载已有的class字节码⽂件信息,修改部分信息,或动态⽣成⼀个 class。

3.1 何为动态代理?

动态代理(Dynamic Proxy)是一种设计模式,它允许 在运行时创建代理对象,并将方法调用转发给实际的对象。 动态代理可以用于实现横切关注点(如日志记录、性能监控、事务管理等)的功能,而无需修改原始对象的代码。
在Java中,动态代理通常使用java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口来实现。

以下是使用动态代理的一般步骤:

  1. 创建一个实现InvocationHandler接口的类,该类将作为代理对象的调用处理程序。在InvocationHandler接口的invoke方法中,可以定义在方法调用前后执行的逻辑。

  2. 使用Proxy类的newProxyInstance方法创建代理对象。该方法接受三个参数:类加载器、代理接口数组和调用处理程序。它将返回一个实现指定接口的代理对象。

  3. 使用代理对象调用方法。当调用代理对象的方法时,实际上会调用调用处理程序的invoke方法,并将方法调用转发给实际的对象。

动态代理

3.2 JDK 动态代理实现

先通过实现 InvocationHandler 接⼝创建⽅法调⽤处理器,再通过 Proxy 来创建代理类。

// 动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现),此种⽅式实现,要求被代理类必须实现接⼝
public class PayServiceJDKInvocationHandler implements InvocationHandler {

    // ⽬标对象即就是被代理对象
    private Object target;

    public PayServiceJDKInvocationHandler(Object target) {
        this.target = target;
    }

    // proxy代理对象
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 1.安全检查
        System.out.println("安全检查");
        // 2.记录⽇志
        System.out.println("记录⽇志");
        // 3.时间统计开始
        System.out.println("记录开始时间");
        // 通过反射调⽤被代理类的⽅法
        Object retVal = method.invoke(target, args);
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }

    public static void main(String[] args) {
        PayService target= new AliPayService();
        // ⽅法调⽤处理器
        InvocationHandler handler =
                new PayServiceJDKInvocationHandler(target);
        // 创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
        PayService proxy = (PayService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[]{PayService.class},
                handler
        );
        proxy.pay();
    }
}

3.3 CGLIB 动态代理实现

public class PayServiceCGLIBInterceptor implements MethodInterceptor {
    // 被代理对象
    private Object target;

    public PayServiceCGLIBInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 1.安全检查
        System.out.println("安全检查");
        // 2.记录⽇志
        System.out.println("记录⽇志");
        // 3.时间统计开始
        System.out.println("记录开始时间");
        // 通过cglib的代理⽅法调⽤
        Object retVal = methodProxy.invoke(target, args);
        // 4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }

    public static void main(String[] args) {
        PayService target= new AliPayService();
        PayService proxy= (PayService) Enhancer.create(target.getClass(),new PayServiceCGLIBInterceptor(target));
        proxy.pay();
    }
}

3.4 两种方式的区别

  • JDK 实现,要求被代理类必须实现接⼝, 之后是通过 InvocationHandler 及 Proxy,在运⾏时动态的在内存中⽣成了代理类对象,该代理对象是通过实现同样的接⼝实现(类似静态代理接⼝实现的⽅式),只是该代理类是在运⾏期时,动态的织⼊统⼀的业务逻辑字节码来完成。
  • CGLIB 实现,被代理类可以不实现接⼝, 是通过继承被代理类,在运⾏时动态的⽣成代理类对象。

写在最后

本文被 JavaEE编程之路 收录点击订阅专栏 , 持续更新中。
 以上便是本文的全部内容啦!创作不易,如果你有任何问题,欢迎私信,感谢您的支持!

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

pyqt 使用pixmap展示图片时候出现失真(图片偏移)

像上图上面的情况&#xff0c; 都是经过放大、旋转等操作&#xff0c;展示图片的时候出现失真的情况 一般都是显卡的问题 需要在qimage转pixmap时&#xff0c;添加部分参数 修改办法&#xff1a; 原本是&#xff1a; pixMap QImage(self.pic_image, width, height, QImage…

iOS APP外包开发的语言比较

iOS APP是Apple公司运行在iPhone手机上的APP&#xff0c;开发这样的APP有两种开发语言可以选择&#xff0c;都是由Apple公司提供的语言。其中Objective-C使用时间相对较长&#xff0c;有历史兼容考虑&#xff0c;而Swift是新的开发语言&#xff0c;更符合近些年开发语言的发展理…

基于单片机水质检测系统的设计与实现

功能介绍 以STM32单片机作为主控系统&#xff1b;液晶显示当前参数&#xff1b;PH模块采集当前水质PH酸碱度&#xff1b;DS18B20温度传感器采集当前水体温度&#xff1b;TDS传感器采集当前水体TDS值&#xff1b;浊度传感器采集当前水体浑浊度&#xff1b;按键设置PH、温度、TDS…

unity02 物体运动

旋转&#xff0c;增量旋转&#xff0c;默认增量为15度 ctrl拖拽物体旋转 设置增量旋转角度大小 edit–>grid and snap settings privot 轴心坐标系&#xff08;中心坐标系&#xff09;默认 local 世界坐标系&#xff08;局部坐标系&#xff09; ctrlD复制物体 物体激活&…

【golang】12、gin 源码解析

文章目录 快速使用返回响应路由匹配pathqueryMultipart/Urlencoded Form 解析请求MultipartFrom MiddleWare github.com/gin-gonic/gin 是 golang 的 web 框架&#xff0c;其用字典树做路由匹配、支持中间件&#xff0c;本文介绍其源码实现。 快速使用 package mainimport (&…

Spring-AOP(面向切面)

Spring-AOP(面向切面) 场景模拟(计算器) 功能接口 public interface Calculator {int add(int i, int j);int minus(int i, int j);int multiply(int i, int j);int div(int i, int j); }实现类 public class CalculateLogImpl implements Calculator {Overridepublic int …

ELK-日志服务【kafka-配置使用】

kafka-01 10.0.0.21 kafka-02 10.0.0.22 kafka-03 10.0.0.23 【1】安装zk集群、配置 [rootes-01 ~]# yum -y install java maven [rootes-01 ~]# tar xf apache-zookeeper-3.5.9-bin.tar.gz -C /opt/[rootes-01 ~]# cd /opt/apache-zookeeper-3.5.9-bin/conf/ [rootes-…

2023年四川大学生程序设计竞赛-A.旷野之息

题目描述 Cuber QQ 终于打败盖农救回了塞尔达公主&#xff0c;海拉鲁大地也开始灾后重建。 在统计学中&#xff0c;幂律表示的是两个量之间的函数关系&#xff0c;其中一个量的相对变化会导致另一个量的相应幂次比例的变化&#xff0c;且与初值无关&#xff1a;表现为一个量是…

【Linux后端服务器开发】UDP协议

目录 一、端口号 二、UDP报头格式 三、UDP的特点 四、UDP协议实现网络聊天群 一、端口号 端口号port标识了一个主机上进行通信的不同的应用程序。 0 ~ 1023&#xff1a;系统端口号&#xff0c;HTTP、FTP、SSH等这些广为使用的应用层协议&#xff0c;它们的端口号都是固定…

TypeScript 1 - 小记

文章目录 关于 TypeScript 关于 TypeScript TypeScript is a superset of JavaScript that compiles to clean JavaScript output. 官网&#xff1a;https://www.typescriptlang.orggithub : https://github.com/microsoft/TypeScriptplayground : https://www.typescriptlan…

AWS MSK集群认证和加密传输的属性与配置

通常&#xff0c;身份认证和加密传输是两项不相关的安全配置&#xff0c;在Kafka/MSK上&#xff0c;身份认证和加密传输是有一些耦合关系的&#xff0c;重点是&#xff1a;对于MSK来说&#xff0c;当启用IAM, SASL/SCRAM以及TLS三种认证方式时&#xff0c;TLS加密传输是必须的&…

RabbitMq(一)

一、基本概念、常见工作模式以及简单使用 MQ全称Message Queue (消息队列)&#xff0c;是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。 小结 MQ消息队列&#xff0c;存储消息的中间件分布式系统通信两种方式:直接远程调用和借助第三方完成间接通信发…

Layui基本功能(增删改查)

话不多说&#xff0c;根据我前面的博客我们直接进行操作。记住以下的文件放置&#xff0c;防止操作出不来. 这是我们要完成的界面及功能 后台功能实现 数据查看 我们在userDao方法里面进行增删改查的方法我们在userAction进行方法的编写R工具类的介绍 查询 userDao方法 因为我…

51单片机学习--矩阵键盘、电子密码锁

矩阵键盘的模块原理图&#xff1a; 功能&#xff1a;按下矩阵按键&#xff0c;在LCD上显示对应的数值。 采用模块化编程&#xff0c;在此只给出MatrixKey.c 和 main.c 两部分代码 #include <REGX52.H> #include "Delay.h"/*矩阵键盘读取键码按下不放&#xf…

typescript manual

这里写目录标题 throw new Error在浏览器中调试Json定义类型定义数组 functionNamed functionanonymous function Axios经典片段 错误及解决ref valuebecause it is a constantAPI 和 客户端定义的数据结构不一样ServerClient throw new Error throw new Error(“Get data err…

存css实现动态时钟背景

代码实现 <!DOCTYPE html> <html lang"en"> <head><meta http-equiv"Content-Type" content"text/html; charsetUTF-8"><title>Title</title><meta name"referrer" content"no-referrer…

掌握无人机遥感数据预处理的全链条理论与实践流程、典型农林植被性状的估算理论与实践方法、利用MATLAB进行编程实践(脚本与GUI开发)以及期刊论文插图制作等

目录 专题一 认识主被动无人机遥感数据 专题二 预处理无人机遥感数据 专题三 定量估算农林植被关键性状 专题四 期刊论文插图精细制作与Appdesigner应用开发 近地面无人机植被定量遥感与生理参数反演 更多推荐 遥感技术作为一种空间大数据手段&#xff0c;能够从多时、多…

Linux —— 进程状态

目录 一&#xff0c;进程状态分类 二&#xff0c;僵尸进程 三&#xff0c;孤儿进程 一&#xff0c;进程状态分类 进程状态反应进程执行过程中的变化&#xff0c;状态会随外界条件的变化而转换&#xff1b; 三态模型&#xff0c;运行态、就绪态、阻塞态&#xff1b;五态模型…

PageObject+Python+Appium

目录 前言&#xff1a; 简介 功能 常用目录 配置 实例 - 第一次启动 app 实例 - 登录 代码入口实例 结果展示 前言&#xff1a; Page Object模式是一种常用的设计模式&#xff0c;用于组织和管理自动化测试脚本中的页面对象。它将页面的元素和操作封装在一个独立的类…

skbuff.h在哪

今天看来下底层socket的实现&#xff0c;发现很多人都说有个skbuff结构&#xff0c;我就想着去找下这个&#xff0c; cat /include/linux/skbuff.h 结果找不到&#xff0c;查了下&#xff0c;需要下载内核源码 //Ubuntusudo apt install linux-headers-genericdpkg -S skbuff.…