【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注解,就能够对对应方法进行增强。


以上就是本篇所有内容~

若有不足,欢迎指正~ 

###Spring AOP 的概念 AOP(Aspect-Oriented Programming)即面向切面编程,是一种编程范式,旨在通过分离横切关注点来提高模块化程度。在 Spring 框架中,AOP 被广泛用于实现诸如日志记录、事务管理、安全性等通用功能,这些功能通常与业务逻辑无关但又需要在多个地方重复使用。 Spring AOP 主要是基于 AspectJ 实现的,尽管 AspectJ 是一个独立的 AOP 框架,并不是 Spring 的组成部分,但它通常与 Spring 一起使用以提供更强大的 AOP 功能[^1]。Spring AOP 支持两种方式来定义切面:基于 XML 配置文件的方式和基于注解的方式。 ###Spring AOP 的原理 Spring AOP 使用运行时代理来实现 AOP 功能,这意味着它会在运行时动态生成代理对象。对于实现了接口的类,Spring AOP 默认使用 JDK 动态代理;而对于没有实现接口的类,则会使用 CGLIB 代理[^4]。这种方式允许在不修改原始代码的情况下向程序中添加新的行为。 织入(Weaving)是将增强(advice)应用到目标对象的过程,Spring AOP 在运行时进行织入操作[^3]。当创建了代理对象后,所有对目标对象方法的调用都会被拦截,并且可以插入额外的操作,比如在方法执行前后做一些处理。 ###Spring AOP 的使用教程 要开始使用 Spring AOP,首先需要确保项目中包含了必要的依赖。如果使用 Maven 构建工具,可以在 `pom.xml` 文件中加入如下依赖: ```xml <!-- 引入aop依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` 一旦添加了依赖并刷新了 Maven 项目,就可以开始编写切面了。下面是一个简单的例子,展示如何使用注解来定义一个切面: ```java import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @Before("execution(* com.example.service.*.*(..))") public void logBefore(JoinPoint joinPoint) { System.out.println("Method " + joinPoint.getSignature().getName() + " is called."); } } ``` 在这个示例中,`LoggingAspect` 类被标记为 `@Aspect` 和 `@Component` 注解,这样 Spring 就能识别这是一个切面组件。`@Before` 注解指定了在哪些方法上应用前置通知(before advice),这里的表达式表示匹配 `com.example.service` 包下所有的方法。 ###
评论 34
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小猪同学hy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值