Spring AOP入门

26 阅读5分钟

本文将从一下两个方面介绍Spring AOP:初识AOP和AOP快速入门

初识AOP

Spring AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 框架的核心模块之一,用于将横切关注点(如日志、事务、安全等)从业务逻辑中分离,通过动态代理技术实现代码的模块化和解耦。 通俗来讲,AOP就像是程序的 “装备” ,通过装备这些武器,使其方便优雅地拓展功能。

下面是一个非常形象的使用场景:你需要统计每个业务方法的运行耗时,需要在每个方法执行前后分别获取时间戳,然后做差得出结果,这部分代码简单但是需要重复多次,此时AOP就将这部分代码抽象为通知,编写一遍就能对所有匹配的方法生效

AOP中的核心概念:

  • 连接点: JoinPoint 可以被AOP控制的方法

  • 通知: Advice 指的是重复的逻辑,也就是共性功能,最终体现为通知方法

  • 切入点: PointCut 连接点匹配的条件,通知仅会在切入点方法执行时被应用

  • 切面: Aspect 描述通知与切入点的对应关系(通知+切入点)

  • 目标对象: Target 通知所应用的对象

通知类型:

  • @Around: 环绕通知,此注解标注的通知方法在目标方法前、后都被执行

  • @Before: 前置通知,此注解标注的通知方法在目标方法前被执行

  • @After: 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行

  • @AfterReturning: 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行

  • @AfterThrowing: 异常后通知,此注解标注的通知方法发生异常后执行

切入点表达式

execution匹配方法签名

语法:

execution(访问修饰符==?== 返回值 包名.类名. ==?== 方法名(形参数据类型) throws 异常==?==)

带 ==?== 的部分可以省略, 包名.类名.不建议省略

切入点的两种通配符用法

*(一个星号) 含义:代表一个独立的任意符号应用

  1. 可以匹配任意的返回值类型、包名、类名或方法名。
  2. 可以匹配任意类型的一个参数。
  3. 可以通配包、类或方法名的一部分。

..(两个点) 含义:代表多个连续的任意符号应用

  1. 可以匹配任意层级的子包。
  2. 可以匹配任意个数、任意类型的参数。

书写建议:

  • 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:findXxx,updateXxx。

  • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。

  • 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名尽量不使用..,使用*匹配单个包。

annotation匹配特定注解

语法:

@annotation(注解全类名)

注意:两种表达式之间可以使用逻辑运算符 || , && , ! 来连接

@Pointcut注解

将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可

@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.(..))")
public void pt(){};

@Around("pt()")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {}

连接点

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

注意:

  • 对于@Around通知,获取连接点信息只能使用 ProceedingJoinPoint。

  • 对于其它四种通知,获取连接点信息只能使用JoinPoint,它是 ProceedingJoinPoint的父类型。

获取参数的方法:

@Before("@annotation(全类名)")
public void before(JoinPoint joinPoint){
    //1. 获取目标对象
    Object target = joinPoint.getTarget();

    //2. 获取目标类
    String className = joinPoint.getTarget().getClass().getName();

    //3. 获取目标方法
    String methodName = joinPoint.getSignature().getName();

    //4. 获取目标方法参数
    Object[] args = joinPoint.getArgs();

}

AOP执行过程:

  1. Spring启动时,容器会根据切入点找到对应的Bean类,通过动态代理的方式构造新的Bean(实现了通知的Bean)并替换掉原始的Bean,在后续的依赖注入中,注入的就是代理出来的对象
  2. 程序运行时,执行通知和目标方法。此时,由于通知类型不同,切面的执行过程略有差异

AOP入门应用

笔者接下来就在项目场景中展现AOP的优雅 场景:对一个订单管理系统,每次修改订单时都要更新修改时间 方案:自定义更新时间注解,编写切面类实现需求

1.自定义注解,通过注解指定连接点

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 AutoFill {}

后续只需在对应的mapper层接口上加入该注解即可

2.编写切面类,实现通知方法

import com.sky.annotation.AutoFill;  
import com.sky.constant.AutoFillConstant;  
import com.sky.context.BaseContext;  
import com.sky.enumeration.OperationType;  
import lombok.extern.slf4j.Slf4j;  
import org.aspectj.lang.JoinPoint;  
import org.aspectj.lang.annotation.Aspect;  
import org.aspectj.lang.annotation.Before;  
import org.aspectj.lang.annotation.Pointcut;  
import org.aspectj.lang.reflect.MethodSignature;  
import org.springframework.stereotype.Component;  
import java.lang.reflect.Method;  
import java.time.LocalDateTime;  
  
/**  
 * 自定义切面,实现公共字段自动填充  
 */  
@Slf4j  
@Aspect  
@Component  
public class AutoFillAspect {  
  
    /**  
     * 切入点  
     */  
    @Pointcut("@annotation(com.annotation.AutoFill)")  
    public void autoFillPointCut(){}  
  
    /**  
     * 前置通知,在方法执行前进行数据填充  
     */  
    @Before("autoFillPointCut()")  
    public void autoFill(JoinPoint joinPoint){  
        log.info("开始进行公共字段数据填充");  
  
        
        //获取被拦截的方法参数,进行非空判断
        Object[] args = joinPoint.getArgs();  
        if (args == null || args.length == 0){  
            return;  
        } 
         
        //获取实体对象
        Object entity = args[0];  
        //准备赋值的数据  
        LocalDateTime now = LocalDateTime.now();  
        //根据不同数据库操作类型,为对应的属性赋值  
            try {  
                Method setUpdateTime = entity.getClass().getDeclaredMethod(setUpdateTime, LocalDateTime.class);  
                //通过反射为对象赋值  
                setUpdateTime.invoke(entity,now);  
            } catch (Exception e) {  
                e.printStackTrace();  
            } 
        }  
  
    }

我们规定,mapper接口中的更新数据方法的第一个参数必须是和表中字段对应的实体类,这样我们就可以获得这个对象并通过反射赋值 此外,切入点可以使用execution()与annotation相&&来提高切入点的准确性 如果要进行其他类似操作,如填充创建时间字段,可以在注解中添加元素,并在通知中根据元素值进行不同操作