欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 时评 > 【Spring】AOP

【Spring】AOP

2025/3/1 12:10:07 来源:https://blog.csdn.net/zhyhgx/article/details/145910404  浏览:    关键词:【Spring】AOP

目录

前言

什么是AOP

什么是Spring AOP?

使用Spring AOP

引入依赖

编程AOP程序

优势

详解Spring AOP

Spring AOP核心概念

切点

连接点

通知

切面

 通知类型

结论

 切点提取

 切面优先级 @Order 注解

切点表达式

execution切点表达式

@annotation

自定义注解


前言

在前面我们介绍了Spring有两大核心:IoC和AOP,我们已经讲解了什么是IoC,以及如何使用IoC,那么本篇我们就来讲解什么是AOP,以及如何使用Spring AOP。

什么是AOP

AOP(Aspect Oriented Programming) 是一种编程范型,即面向切面编程。

什么是面向切面编程?

切面就是指某一类特定问题,所以AOP也可以理解为面向特定方法编程。面向编程旨在通过分离横切关注点来提高代码的模块化和可维护性,允许开发者将那些与业务逻辑无关,但又需要在多个地方使用的功能(如日志记录、事务管理、安全性等)从业务逻辑中分离出来。在前面我们的登录校验,用了拦截器,其实也是对AOP思想的一种实现。统一数据返回格式和统一异常处理也是AOP的一种实现。

结论:AOP是一种思想,是对某一类事情的集中处理

AOP是一种思想,实现它的方法有很多,如Spring AOP,以及AspectJ、CGLIB等。

接下来我们就来学习一下Spring AOP。

什么是Spring AOP?

Spring AOP是Spring框架对面向切面编程的支持,允许开发者将横切关注点从业务逻辑中分离出来,从而提高代码的模块化、可维护性和可扩展性。

Spring AOP 是基于动态代理实现的,支持通过注解或者XML配置来定义切面逻辑。那什么是动态代理,我们下一篇讲。

我们在前面学的拦截器、统一数据返回格式以及统一异常处理这些还不够吗?

拦截器作用的维度是URL(一次请求和响应),@ControllerAdvice 的应用场景主要是全局异常处理,数据绑定,数据预处理等,而AOP的作用维度更加细致(可以根据包、类、方法名、参数等进行拦截),能够实现更加复杂的业务逻辑

假如我们现在有一个项目,想要对其中一些业务功能进行优化,那么我们就需要知道它的耗时时长,需要对每个接口都进行添加计算耗时的逻辑,这样就太麻烦了,而且成本高。

但如果我们使用AOP,可以对原始接口不修改的情况下,对特定的方法进行功能增强。

使用Spring AOP

引入依赖

在使用AOP之前,我们需要先引入AOP的依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

编程AOP程序

我们这里来编写一个AOP类,定义一个记录方法执行耗时的方法。

需要用 @Aspect 注解修饰类,同时我们需要将这个AOP类交给Spring容器来管理,需要用类注解修饰@Component:

package com.example.demo.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Aspect
@Slf4j
@Component
public class TimeAspect {//切面是由切点+通知组成的@Around("execution(* com.example.demo.controller.*.*(..))")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {//1、起始时间long start = System.currentTimeMillis();log.info("around处理前");//2、执行目标方法Object result=pjp.proceed();//3、终止时间long end=System.currentTimeMillis();//4、返回计算结果log.info("around处理后");log.info("耗时:{}ms",end-start);return result;}
}
  • @Aspect:用这个注解修饰,表示这个类是一个切面类;
  • @Around:环绕通知,在目标方法执行前后都会被执行。后面的表达式对哪些方法进行增强;
  • pjp.proceed():让目标方法执行。

整个方法可以分为三部分: 

package com.example.demo.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@Slf4j
@RequestMapping("/aop")
public class AopController {@RequestMapping("/t1")public String t1(){log.info("t1方法执行");return "t1";}}

 我们运行一下:

可以看到,我们对调用的方法不比修改,就可以做出额外的一些工作。

优势

  •  代码不侵入:不修改原始的业务方法,就可以堆原始的业务方法进行功能增强或是功能上的改变;

  • 减少重复代码

  • 提高开发效率

  • 方便维护

详解Spring AOP

既然我们知道了Spring AOP的初始用法,那么就下来我们就来学习一下Spring AOP几个核心概念:

  • 切点
  • 连接点
  • 切面
  • 通知

Spring AOP核心概念

切点

切点(PointCut),也叫做“切入点”,提供一组规则(使用 Aspect pointCut expression language来描述),告诉程序哪些方法需要增强

前面代码中在注解@Around后面的 execution(* com.example.demo.controller.*.*(..))

字符串就是切点表达式:

切点表达式中的含义我们后面讲。

连接点

满足切点表达式规则的方法,就是连接点,也就是可以被AOP控制的方法。例如上面例子中也就是在 com.example.demo.controller 包下的所有类都叫做连接点。

在上面的例子中,pjp就是一个连接点:

通过执行该参数的proceed() 方法就可以执行目标方法。 

通知

通知指的就是具体做的工作,需要重复执行的逻辑,也就是共性功能(最终体现为一个方法)

我们在上面例子中计算方法执行耗时的逻辑,就是通知。

 

切面

切面(Aspect)是由切点(PointCut)+通知(Advice)组成的

切面封装了横切关注点的逻辑,横切关注点指的是那些影响多个类或模块的逻辑,如日志记录、事务管理等。

通过切面就能够描述当前AOP程序需要对哪些方法,在什么时候执行什么样的操作。切面既包含了通知逻辑的定义,也包括了连接点的定义。切面所在的类,我们一般称为切面类(被@Aspect修饰的类)。

 通知类型

在Spring AOP中的通知类型,不仅仅只有我们上面例子中用到的 @Around (环绕通知)注解,还有以下其它几种通知类型,总结起来:

  • @Around:环绕通知,此注解修饰的通知方法在目标方法执行前后都被执行;
  • @Before:前置通知,此注解标注的通知方法在目标方法执行前被执行;
  • @After:后置通知,此注解标注的通知方法在目标方法执行后被执行;
  • @AfterReturning:返回后通知,此注解标注的通知方法在目标方法执行前被执行,有异常不会执行;
  • @AfterThrowing:异常后通知,此注解标注的通知方法在目标方法发送异常后执行

我们通过代码来加深一下对这几个通知的理解:

package com.example.demo.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Aspect
@Slf4j
@Component
public class TimeAspect {//切面是由切点+通知组成的@Around("execution(* com.example.demo.controller.*.*(..))")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {//1、起始时间long start = System.currentTimeMillis();log.info("around处理前");//2、执行目标方法Object result=pjp.proceed();//3、终止时间long end=System.currentTimeMillis();//4、返回计算结果log.info("around处理后");log.info("耗时:{}ms",end-start);return result;}@After("execution(* com.example.demo.controller.*.*(..))")public void afterRecordTime(){log.info("执行After通知");}@Before("execution(* com.example.demo.controller.*.*(..))")public void beforeRecordTime(){log.info("执行Before通知");}@AfterReturning("execution(* com.example.demo.controller.*.*(..))")public void afterReturningRecordTime(){log.info("执行AfterReturning通知");}@AfterThrowing("execution(* com.example.demo.controller.*.*(..))")public void afterThrowingRecordTime(){log.info("执行AfterThrowing通知");}}

测试类:

package com.example.demo.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@Slf4j
@RequestMapping("/aop")
public class AopController {@RequestMapping("/t1")public String t1(){log.info("t1方法执行");return "t1";}@RequestMapping("/t3")public String t3(){int a = 1/0;return "t3";}
}

运行一下进行测试:

对于t1方法:

我们可以看到,在程序正常运行的情况下,由 @AfterThrowing 修饰的方法并不会被执行、。

同时,我们可以观察到, @Around 标识的通知方法包含两部分,一个“前置逻辑”,一个“后置逻辑”。其中“前置逻辑”会先于 @Before 标识的通知方法执行,“后置逻辑”会晚于 @After 标识的通知方法执行。

 

对于t3方法:

t3方法中我们给了一个异常, 在程序发生异常的时候,@AfterReturning 标识的方法不会被执行,但@AfterThrowing 标识的通知方法被执行了。

@Arounf环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会被执行(因为此时原始方法调用出现了问题)

结论

如果目标方法中不出现异常:

 

目标方法中出现异常:

注意:

  • @Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑;
  • @Around 环绕通知方法的返回值,必须指定为 Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的;
  • 一个切面类可以有多个切点

 切点提取

在前面的代码中,我们可以看到,每个通知的切点表达式都是一样的,那么我们有没有办法可以将这些公共的表达式提取出来,需要的使用引入即可?

在Spring中,提供了 @PointCut 注解,可以把公共的切点表达式提取出来

所以上面的代码我们可以修改为:

package com.example.demo.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Aspect
@Slf4j
@Component
public class TimeAspect {@Pointcut("execution(* com.example.demo.controller.*.*(..))")public void pointCut(){}//切面是由切点+通知组成的@Around("pointCut()")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {//1、起始时间long start = System.currentTimeMillis();log.info("around处理前");//2、执行目标方法Object result=pjp.proceed();//3、终止时间long end=System.currentTimeMillis();//4、返回计算结果log.info("around处理后");log.info("耗时:{}ms",end-start);return result;}@After("pointCut()")public void afterRecordTime(){log.info("执行After通知");}@Before("pointCut()")public void beforeRecordTime(){log.info("执行Before通知");}@AfterReturning("pointCut()")public void afterReturningRecordTime(){log.info("执行AfterReturning通知");}@AfterThrowing("pointCut()")public void afterThrowingRecordTime(){log.info("执行AfterThrowing通知");}}

如果存在多个切面类时,其他切面类想要使用这个切点定义时,就需要将访问修饰符修改为public,其他切面类引用这个切点表达式的方法需要使用 全限定类名.方法名() 。如果只想要在该类中使用,就用private修饰。

package com.example.demo.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;@Slf4j
@Component
@Aspect
public class Aspectdemo {@After("com.example.demo.aspect.TimeAspect.pointCut()")public void after() {log.info("执行Aspectdemo的after方法");}@Before("com.example.demo.aspect.TimeAspect.pointCut()")public void before() {log.info(" 执行Aspectdemo的before方法");}
}

 切面优先级 @Order 注解

那么存在多个切面的时候,它们的执行顺序是怎么的?随机的?其实不是的,如果有多个切面类的多个切入点都匹配到同一个目标方法,当目标方法运行时,会执行以下两个原则:

  1. 前面通知类型的先后顺序
  2. 切面类的类名排序

这里怎么不是先执行完Aspectdemo中的通知方法再执行TimeAspect中的通知方法?

 切面类中的通知方法的执行顺序,我们可以看成以下这幅图:

那么如果我们想要改变一下切面类的执行顺序,就需要在想要优先执行的切面类上添加 @Order()注解。

可以看到,通过 @Order注解并填入value值,就能够改变切面类的执行顺序,value值越小的切面类优先级越高。

切点表达式

前面我们使用切点表达式来描述切点,下面我们就来介绍一下切点表达式的语法。

切点表达式常见有两种表达方式:

  • excution(......):根据方法的签名来匹配;
  • @annitation(......):根据注解匹配

execution切点表达式

execution()是最常用的一种切点表达式,用来匹配方法,语法:

execution(<访问修饰符>  <返回类型>  <包名.类名.方法(方法参数)>  <异常>)

 

 其中:访问修饰符合异常可以省略

切点表达式支持通配符表达:

  1. *匹配任意字符,只匹配一个元素(返回类型,包,类名,方法或者方法参数)
    1. 包名使用 * 表示任意包(一层包使用一个*)

    2. 类名使用 * 表示任意类

    3. 返回值使用 *表示任意返回值类型

    4. 方法名使用 * 表示任意方法

    5. 参数使用 * 表示一个任意类型的参数

  2. .. :匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数

    1. 使用 .. 配置包名,表示此包以及此包下的所有子包

    2. 可以使用 .. 配置参数,任意个任意类型的参数

示例:

AopController下public修饰,返回类型为String,方法名t1,无参方法:

execution(public String com.example.demo.controller.AopController.t1())

省略访问修饰符:

execution(String com.example.demo.controller.AopController.t1())

匹配所有返回类型:

execution(* com.example.demo.controller.AopController.t1())

匹配AopController下的所有无参方法:

execution(* com.example.demo.controller.AopController.*())

匹配AopController下的所有方法:

execution(* com.example.demo.controller.AopController.*(..))

匹配controller包下所有类的所有方法:

execution(* com.example.demo.controller.*.*(..))

匹配所有包下面的AopController:

execution(* com..controller.AopController.*(..))

匹配com.example.demo包下,子孙包下的所有类的所有方法:

execution(* com.example.demo..*(..))

@annotation

execution表达式更适用有规则的,但如果我们想要匹配多个无规则的方法,比如我们想要增强AopController中t1方法和t2方法,但由于返回值不同,所以使用execution不能同时增强这两个方法。

那么这里就需要用到我们的 @annotation 注解来捕获更多无规则的方法。

自定义注解

那么如何使用 @annotation 注解呢?

我们首先需要创建出一个自定义的注解:

package com.example.demo.aspect;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}

这里我们创建了一个简单的注解,参考@Component注解:

  • @Target:标识了Annotation所修饰的对象范围,即该注解在什么地方可以用;
    • ElementType.TYPE:⽤于描述类、接⼝(包括注解类型)或enum声明
    • ElementType.METHOD:描述方法
    • ElementType.PARAMETER:描述参数
    • ElementType.TYPE_USE:可以标注任意类型
  • @Retention 指Annotation 被保留的时间长短,标明注解的⽣命周期:
    • RetentionPolicy.SOURCE表示注解仅存在于源代码中,编译成字节码后会被丢弃。这意味着在运行时无法获取到该注解的信息,只能在编译时使⽤。比如 @SuppressWarnings ,以及lombok提供的注解 @Data ,@Slf4j
    • RetentionPolicy.CLASS编译时注解。表示注解存在于源代码和字节码中,但在运行时会被丢弃。这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行时⽆法获取。通常⽤于⼀些框架和⼯具的注解
    • RetentionPolicy.RUNTIME运行时注解。表示注解存在于源代码,字节码和运行时中。这意味着在编译时,字节码中和实际运行时都可以通过反射获取到该注解的信息。通常⽤于⼀些需要在运行时处理的注解,如Spring的 @Controller @ResponseBody

我们自定完注解之后,就需要在切面类中使用@annotation切点表达式定义切点,只对@MyAspect生效。 

@annotation中需要填入我们自定义注解的全限定名。

package com.example.demo.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;@Slf4j
@Component
@Aspect
@Order(2)
public class Aspectdemo {@Around("@annotation(com.example.demo.aspect.MyAspect)")public Object around(ProceedingJoinPoint pjp) throws Throwable {log.info("执行Aspectdemo的around方法前");long start = System.currentTimeMillis();Object proceed = pjp.proceed();long end = System.currentTimeMillis();log.info("耗时:{}ms", end - start);log.info("执行Aspectdemo的around方法后");return pjp.proceed();}}

 

 

可以看到,我们只要在对应的方法上加上@MyAspect注解,就能够对对应方法进行增强。


以上就是本篇所有内容~

若有不足,欢迎指正~ 

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词