Spring AOP

Spring AOP

Spring AOP 作为和 Spring IOC 平起平坐的大兄弟,在日常开发中还是经常会用到的,通过本文记录一下相关的概念和常见的使用场景。

什么是 AOP

AOP 全称为 Aspect Oriented Programing,翻译为「面向切面编程」,指的是将一定的切面逻辑按照一定的方式编织到指定的业务模块中,从而将这些业务模块的调用包裹起来,使得业务逻辑各部分之间的耦合度降低,提高代码的可重用性,提高开发效率。

核心概念

切面(Aspect)

切面由切点和通知组成,Spring AOP 中通过 @Aspect 注解标注的类就是一个切面。

连接点(join point)

所有我们能够将通知应用到的地方都是连接点,在 Spring 中,我们可以认为连接点就是所有的方法(构造函数除外)。连接点没啥实际意义,这个概念的提出只是为了更好的说明切点。

切点(point cut)

切点就是那些我们想要应用通知的连接点。

通知(advice)

就是我们想在连接点附近想要实现的功能。

目标对象(target)

就是要被通知的对象,也就是切点所对应的实际业务逻辑所在的对象。

代理对象(target)

目标对象织入切面后所产生的代理对象,即包含通知的对象。

织入(Weaving)

把切面应用的目标对象来创建新的代理对象的过程。织入方式有三种:

  • 编译时织入(特殊编译器实现)
  • 加载时织入(特使的类加载器实现)
  • 运行时织入,通常 Spring AOP 就是通过这种方式实现织入

Spring 中如何使用 AOP

1.开启 AOP 支持

在配置类上加上 @EnableAspectJAutoProxy 注解即自动开启 AOP 的支持,这个注解有两个属性如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AspectJAutoProxyRegistrar.class})
public @interface EnableAspectJAutoProxy {
    //是否使用CGLIB代理,默认false,使用JDK动态代理
    boolean proxyTargetClass() default false;
    //是否将代理类作为线程本地变量(threadLocal)暴露(可以通过AopContext访问)
    //主要设计的目的是用来解决内部调用的问题
    boolean exposeProxy() default false;
}

2.声明切面

@Aspect  // 申明是一个切面
@Component  // 切记,一定要将切面交由Spring管理,否则不起作用
public class WycAnnotationAspect {
 //......
}

3.声明切点

一个切点有两部分组成,一个方法签名和一个切点表达式。切点表达时又分为切点标志符操作参数。常用的切点表达式有 excecution @annotation within @within 等。

@Aspect
@Component
public class WycAnnotationAspect {
    @Pointcut("execution(public * *(..))")
    private void executionPointcut() {}
 
    @Pointcut("@annotation(com.spring.study.aop.annotation.WycAnnotation)")
    private void annotationPointcut() { }
    
    // 可以组合使用定义好的切点
    
    // 表示同时匹配满足两者
    @Pointcut("executionPointcut() && annotationPointcut()")
    private void annotationPointcutAnd() {}
 
    // 满足其中之一即可
    @Pointcut("executionPointcut() || annotationPointcut()")
    private void annotationPointcutOr() {}
 
    // 不匹配即可
    @Pointcut("!executionPointcut()")
    private void annotationPointcutNot() {}
}

4.声明通知

常用的 advice 类型有以下几种

  • @Before 在切点之前执行
  • @AfterReturning 在切点正常返回后执行
  • @AfterThrowing 在切点抛出异常后执行
  • @After 在方法执行结束之后执行,不管是正常返回还是抛出异常都会执行
  • @Around 最强大的通知类型,可以包裹目标方法
@Aspect
@Component
public class WycAspect {
 
    // 申明的切点
    @Pointcut("execution(public * *(..))")
    private void executionPointcut() {}
    @Pointcut("@annotation(com.spring.study.aop.annotation.WycAnnotation)")
    private void annotationPointcut() {}
 
    // 前置通知,在目标方法前调用
    @Before("executionPointcut()")
    public void executionBefore() {
        System.out.println("execution aspect Before invoke!");
    }
    
    // 后置通知,在目标方法返回后调用
    @AfterReturning("executionPointcut()")
    public void executionAfterReturning() {
        System.out.println("execution aspect AfterReturning invoke!");
    }
 
    // 最终通知,正常的执行时机在AfterReturning之前
    @After("executionPointcut()")
    public void executionAfter() {
        System.out.println("execution aspect After invoke!");
    }

    // 异常通知,发生异常时调用
    @AfterThrowing("executionPointcut()")
    public void executionAfterThrowing() {
        System.out.println("execution aspect AfterThrowing invoke!");
    }
 
    // 环绕通知,方法调用前后都能进行处理
    @Around("executionPointcut()")
    public void executionAround(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("execution aspect Around(before) invoke!");
        System.out.println(pjp.proceed());
        System.out.println("execution aspect Around(after) invoke!");
    }
}

通知中的参数

在上面应用的例子中,只有在「环绕通知」的方法上添加了一个 ProceedingJoinPoint 类型的参数。这个 ProceedingJoinPoint 意味着当前执行中的方法,它继承了 JoinPoint 接口。

JoinPonit 可以再任意方法上作为第一个参数声明,代表的是当前通知所应用的切点(即目标类中的方法),提供了以下几个方法:

  • getArgs() 返回当前切点的参数
  • getThis() 返回代理对象
  • getTarget() 返回目标对象
  • getSignature() 返回当前切点的描述信息,比如修饰符、名称等

ProceedingJoinPointJoinPonit 的基础上多提供了两个方法:

  • proceed() 直接执行当前的方法,基于此,我们可以在方法的执行前后直接加入对应的业务逻辑
  • proceed(Object[] args) 可以改变当前执行方法的参数,然后用改变后的参数执行这个方法

通知的排序

当我们对于一个切点定义了多个通知时,例如,在一个切点上同时定义了两个before类型的通知。这个时候,为了让这两个通知按照我们期待的顺序执行,我们需要在切面上添加 @Order 注解或者让切面实现 org.springframework.core.Ordered 接口,重写 getOrder() 方法。例如:

@Aspect
@Component
@Order(-1)
public class WycFirstAspect {
    // ...
}

@Aspect
@Component
@Order(0)
public class WycSecondAspect {
    // ...
}

AOP 的应用

  • 监控方法执行时长,记录出入参
  • 对外接口的统一验签、权限控制
  • 缓存优化
  • 事务支持

下面举一个AOP 结合自定义注解完成接口限流的例子

@Slf4j
@Aspect
@Configuration
public class AopAdviseDefine {

    @Around("execution(* com.wyc1856.assistant.controller..*.*(..)) && @annotation(com.wyc1856.assistant.annotation.Limit)")
    public Object checkLimit(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Limit annotation = method.getAnnotation(Limit.class);
        LimitTypeEnum limitType = annotation.limitType();
        String key = "";
        String prefix = annotation.prefix();
        switch (limitType) {
            case CUSTOMER:
                key = annotation.key();
                break;
            case IP:
                key = NetUtil.getIpAddress();
                break;
            default:
                break;
        }
        if (StringUtils.isBlank(key)) {
            key = method.getName();
        }
        ImmutableList<String> keys = ImmutableList.of(StringUtils.join(prefix, key));
        try {
            String luaScript = buildLuaScript();
            DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
            Number inCount = limitRedisTemplate.execute(redisScript, keys, annotation.count(), annotation.period());
            if (inCount == null || inCount.intValue() > annotation.count()) {
                throw new LimitException("请求太频繁了,请稍后再试");
            }
            return joinPoint.proceed();
        } catch (LimitException e) {
            throw e;
        } catch (Throwable th) {
            throw new RuntimeException("限流处理异常: " + th.getLocalizedMessage());
        }
    }
}