【Spring Aop】使用全攻略,开启面向切面编程世界的大门

概述

使用 AOP(面向切面编程)可以帮助你实现横切关注点(如日志记录、事务管理、权限控制等)的分离,比如你对外提供了一个接口,接口上线后产品又要求需要对接口的出入参做记录,以提供数据支撑供业务分析以及方便问题排查。

你想着,这不简单嘛,我只要在接口返回的时候发送一个MQ 记录下来不就行了嘛,于是你兴致勃勃的打开代码一看,傻眼了,这个接口里有众多的if()判断,并且每个if判断里都会有结果返回,难道在所有的if返回前都发送一个消息嘛?示例代码如下:

class BussinessService{
	public Response invoke(Param param){
		if(参数校验失败){
			// 发送消息?
			return new Response(400,参数校验失败);
		}
		if(无数据返回){
			// 发送消息?
			return new Response(204,返回内容为空);
		}
		// 。。。。。省略一大堆 if 判断及返回
		// 正常业务逻辑
		// 发送消息?
		return new Response(200,success,result);
	}
}

在所有的if返回前都执行一个逻辑有两个点非常不友好,一个点是如果将来新增if判断,仍然需要新增发送消息代码,第二个点是太具有侵入性,需要修改原来代码。这时候,使用切面就是一个比较好的选择。在日常开发中,Spring Aop就是较为常见的切面的选择。下面就对 Spring Aop切面做详细介绍。

Spring Aop的基本使用

1.添加依赖(可选)

我们现在常用的是 SpringBoot 项目,一般来说,依赖会被自动引入。


    org.springframework.boot
    spring-boot-starter-aop

2.创建切面类

这里我们假设场景是在某个方法执行前,进行日志的打印,这里创建一个名为LoggingAspect的切面类。

切面类是用来定义横切逻辑的地方,我们需要使用@Aspect注解标明这是一个切面类,此外,需要使用@Component使 SpringBoot 项目能够扫描到这个 Bean。

import org.aspectj.lang.annotation.Aspect;  
import org.springframework.stereotype.Component;  
@Aspect  
@Component  
public class LoggingAspect {  
  
}

3.切面类配置

3.1 定义切入点表达式

简单来说,切入点表达式就是说你要拦截哪个方法的调用。

这里我们假设要拦截ControllerTest#test方法的执行,方法定义如下,则我们的切入点表达式定义为:"execution(* com.gumei.webapp.ControllerTest.test(..))",这里我们先不细究切面表达式的定义规则,先将功能实现,在下文中会对切面表达式的规则做详细介绍。

@Controller  
@CrossOrigin  
public class ControllerTest {  
    @ResponseBody  
    @RequestMapping("/test")  
    public Response test(Param param){  
        Response response = new Response();  
        response.setAge(param.age);  
        response.setName(param.name);  
        response.setDate(new Date());  
        response.setLocalDateTime(LocalDateTime.now());  
        return response;  
    }  
}
3.2 使用通知Advice

通俗的理解通知就是在方法的某个时间节点执行时发出的消息,比如要在方法调用之前监听则使用@Before通知。至于不同通知的类型及可接收/返回的参数,也将在下文中进行详细介绍。

import org.aspectj.lang.JoinPoint;  
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.gumei.webapp.ControllerTest.test(..))")  
    public void logBefore(JoinPoint joinPoint){  
        System.out.println("Before method: " + joinPoint.getSignature().getName());  
    }   
}

4. 测试

此时,我们已经完成了一个 Aop 切面的基本使用步骤,启动项目后,在浏览器输入http://localhost:8787/test?age=3&name=xiaoming,便可以看到控制台打印的消息:Before method: test。这样我们就使用Spring Aop完成了一个基础功能,至于更复杂的功能,无非是在上述基础上根据业务需求进行丰富罢了。

Spring Aop概念剖析

在上文中,我们涉及了很多概念没有进行详细解释,没有解释的目的是方便读者能先快速上手使用,如果解释太多不方便理解。但现在在有了上述基础使用的基础之上,再来解释这些概念便是水到渠成了。上文涉及到的概念有切入点表达式(Pointcut Expression通知Advice类型和连接点(Join Point),下面将对这些概念做详细解析。

切入点表达式(Pointcut Expression

上文提到,所谓切入点表达式,无非就是要拦截哪个方法的调用。说的更详细一些:

在 Spring AOP 中,切入点表达式(Pointcut Expression)用于指定在哪些连接点(Join Point)上应用通知(Advice)。切入点表达式是 AOP 的核心部分,它定义了哪些方法应该被拦截,以及在哪些地方应用横切逻辑。理解切入点表达式的语法和用法是有效使用 AOP 的关键。

切入点表达式的基本语法

切入点表达式主要通过 execution 语法来定义。以下是 execution 表达式的基本格式:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

大部分参数都是可选的,去除可选参数后,仅剩下如下所示的格式,下述格式也是我们日常最为常用的格式:

execution(ret-type-pattern name-pattern(param-pattern))
  • modifiers-pattern:方法的修饰符模式(如 publicprivate 等),通常省略。
  • ret-type-pattern:返回类型模式,可以是具体类型或通配符 *
  • declaring-type-pattern:方法所属的类或接口的模式。
  • name-pattern:方法名模式,可以是具体方法名或通配符 *
  • param-pattern:方法参数模式,用于匹配参数类型。
  • throws-pattern:异常模式(可选),通常省略。
常用的切入点表达式示例

本举例中有很多两个点 ..这种语法,下文会对其进行详细说明,这里就理解为通配符即可。

  1. 匹配特定包下的所有方法

    匹配 com.example.service 包下的所有类的所有方法。

    execution(* com.example.service.*.*(..))   
    
  2. 匹配特定类的所有方法

    匹配 com.example.service.MyService 类中的所有方法。

    execution(* com.example.service.MyService.*(..))
    
  3. 匹配返回类型为 String 的方法

    匹配 com.example.service 包下的所有类中返回类型为 String 的方法。

    execution(String com.example.service.*.*(..))
    
  4. 匹配方法名以 get 开头的方法

    匹配 com.example.service 包下的所有类中方法名以 get 开头的方法。

    execution(* com.example.service.*.get*(..))
    
  5. 匹配带有特定参数的方法

    匹配 com.example.service 包下的所有类中第一个参数为 String 的方法。

    execution(* com.example.service.*.*(String, ..))
    
切入点表达式的通配符

上文中关闭切入点表达式示例中,有很多两个点 ..这种语法,这里对其进行详细说明。

在 Spring AOP 的切入点表达式中,两个点 .. 是一个通配符,表示零个或多个任意类型的参数或包,使用 .. 可以极大地提高切入点表达式的灵活性,使得 AOP 能够更广泛地应用于不同的方法和类。。它的使用场景主要有两个:

  1. 匹配方法参数:在方法参数列表中使用 .. 可以匹配任意数量和类型的参数。

  2. 匹配包及其子包:在包名中使用 .. 可以匹配该包及其所有子包。

使用场景示例

  1. 匹配任意参数的方法
execution(* com.example.service.MyService.someMethod(..))

这个表达式匹配 MyService 类中的 someMethod 方法,无论它有多少个参数或参数的类型是什么。

  1. 匹配包及其子包中的所有方法
execution(* com.example..service.*.*(..))

这个表达式匹配 com.example 包及其所有子包中的 service 包下的所有类的所有方法。这里的 .. 通配符用于匹配 com.example 包下的所有子包。

  1. 匹配特定方法和任意参数
execution(* com.example.service.MyService.*(..))

这个表达式匹配 MyService 类中的所有方法,无论方法名是什么,也无论参数是什么。

其他切入点表达式(扩展)

除了 execution 以外,Spring AOP 还支持其他类型的切入点表达式,进行更高阶的使用:

  • within:限制匹配特定类型内的方法。
    匹配 com.example.service 包及其子包下的所有类的方法。

    within(com.example.service..*)
    
  • this:匹配当前 AOP 代理对象的类型。
    匹配代理对象实现 MyService 接口的方法。

    this(com.example.service.MyService)
    
  • target:匹配目标对象的类型。
    匹配目标对象实现 MyService 接口的方法。

    target(com.example.service.MyService)
    
  • args:匹配方法参数的类型。
    匹配第一个参数为 String 类型的方法。

    args(java.lang.String)
    
  • @annotation:匹配被特定注解标记的方法。
    匹配被 @Transactional 注解标记的方法。

    @annotation(org.springframework.transaction.annotation.Transactional)
    

通知Advice

在 Spring AOP 中,通知(Advice)是实际执行横切逻辑的地方。Spring AOP 提供了多种类型的通知,每种通知类型在目标方法执行的不同阶段切入。以下是各种通知类型的详细讲解,包括它们的使用方法和参数示例。

下面将对各种通知类型以及其能够接收的出入参做详细介绍:

1. @Before 通知

@Before 通知在目标方法执行之前运行。

示例:

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

@Aspect
@Component
public class BeforeAdviceExample {

    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before method: " + joinPoint.getSignature().getName());
    }
}

参数:

  • JoinPoint:提供对连接点处的可用信息的访问,如方法名、参数等。
2. @After 通知

@After 通知在目标方法执行完成后运行,无论方法是否成功返回或抛出异常。

示例:

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

@Aspect
@Component
public class AfterAdviceExample {

    @After("execution(* com.example.service.*.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("After method: " + joinPoint.getSignature().getName());
    }
}
3. @AfterReturning 通知

@AfterReturning 通知在目标方法成功返回后运行。

示例:

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

@Aspect
@Component
public class AfterReturningAdviceExample {

    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
    public void logAfterReturning(Object result) {
        System.out.println("Method returned: " + result);
    }
}

参数:

  • returning:指定返回值的变量名,用于捕获目标方法的返回值。
4. @AfterThrowing 通知

@AfterThrowing 通知在目标方法抛出异常后运行。

示例:

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

@Aspect
@Component
public class AfterThrowingAdviceExample {

    @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "error")
    public void logAfterThrowing(Throwable error) {
        System.out.println("Method threw exception: " + error);
    }
}

参数:

  • throwing:指定异常的变量名,用于捕获目标方法抛出的异常。
5. @Around 通知

@Around 通知是最强大的通知类型,可以在目标方法执行之前和之后进行自定义逻辑。它还可以选择不调用目标方法来完全取代方法的执行。

  • @Around 通知需要调用 proceed() 方法来执行目标方法,否则目标方法不会被执行。

示例:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AroundAdviceExample {

    @Around("execution(* com.example.service.*.*(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Before method: " + joinPoint.getSignature().getName());
        Object result = joinPoint.proceed();  // 执行目标方法
        System.out.println("After method: " + joinPoint.getSignature().getName());
        return result;
    }
}

参数:

  • ProceedingJoinPoint:是 JoinPoint 的子接口,允许控制目标方法的执行。proceed() 方法用于执行目标方法。

连接点JoinPoint

JoinPoint 是一个接口,用于提供有关当前连接点的静态信息。它通常用于 @Before@After@AfterReturning 和 @AfterThrowing 通知中。

主要方法
  • Object[] getArgs():返回被拦截方法的参数列表。
  • String getKind():返回连接点的类型(例如,方法调用)。
  • Signature getSignature():返回被拦截方法的签名信息,包括方法名、返回类型等。
  • Object getTarget():返回目标对象(被代理的对象)。
  • Object getThis():返回当前代理对象。
ProceedingJoinPoint

ProceedingJoinPoint 是 JoinPoint 的子接口,专门用于 @Around 通知。它不仅提供静态信息,还允许控制目标方法的执行。

  • Object proceed():继续执行被拦截的方法,并返回其结果。
  • Object proceed(Object[] args):使用新的参数继续执行被拦截的方法。

ProceedingJoinPoint 允许在目标方法执行前后插入自定义逻辑,甚至可以完全取代目标方法的执行。

结语

通过学习本文,你将能够熟练掌握Spring Aop的使用,快去项目中试一试吧!

以上,祝你今天愉快。

你可能感兴趣的:(SpringBoot实战,Java实战,spring,java,spring,boot,Aop)