SpringFramework — AOP 理论

小龙 369 2022-06-08

AOP 联盟

在 SpringFramework 2.0 之前,它还没有整合 AspectJ ,当时的 SpringFramework 还有一套相对低层级的实现,它也是 SpringFramework 原生的实现,而我们要了解它,首先要先了解一个组织:AOP 联盟。

早在很久之前,AOP 的概念就被提出来了。同之前的 EJB 一样,作为一个概念、思想,它要有一批人来制定规范,于是就有了这样一个 AOP 联盟。这个联盟的人将 AOP 的这些概念都整理好,形成了一个规范 AOP 框架底层实现的 API ,并最终总结出了 5 种 AOP 通知类型。

AOP联盟制定的通知类型

5 种通知类型分别为:

  • 前置通知
  • 后置通知(返回通知)
  • 异常通知
  • 环绕通知
  • 引介通知

注意它跟 AspectJ 规定的 5 种通知类型的区别:它多了一个引介通知,少了一个后置通知。而且还有一个要注意的,AOP 联盟定义的后置通知实际上是返回通知( after-returning ),而 AspectJ 的后置通知是真的后置通知,与返回通知是两码事。

SpringFramework 中对应 AOP 联盟五种通知对应的接口

  • 前置通知org.springframework.aop.MethodBeforeAdvice
  • 返回通知org.springframework.aop.AfterReturningAdvice
  • 异常通知org.springframework.aop.ThrowsAdvice
  • 环绕通知org.aopallicance.intercept.MethodInterceptor
  • 引介通知or.springframework.aop.IntroductionAdvice

这里有一点要注意:环绕通知是 AOP 联盟原生定义的接口(不是 CGLIB 那个 MethodInterceptor)

由于 SpringFramework 是基于 AOP 联盟制定的规范来的,所以自然会去兼容原有的方案。又由于咱之前写过原生的动态代理,知道它其实就是环绕通知,所以 SpringFramework 要在环绕通知上拆解结构,自然也会保留原本环绕通知的接口支持。

切面类的通知方法参数

在使用 环绕通知时 用到了一个接口 ProceedingJoinPoint,下面来学一下它的具体使用,以及切面类中的通知方法参数等等。

JoinPoint的使用

切面类的通知方法上都可以使用一个参数 JoinPoint

@Before("execution(public * com..FinanceService.*(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println("Logger beforePrint run ......");
}

直接在方法中调用 joinPoint 的方法,可以发现它能获取到这么多东西:

image-1654654229025

选几个比较重要的来了解一下。

getTarget & getThis

getTarget:返回的是未经代理的原始对象
getThis:直接打印,返回的也是元素对象字符串,但实际上 getThis 获取到的是代理后的对象

image-1654654591545

它增强了 equals 方法,增强了 hashcode 方法,就是没有增强 toString 方法,那当然就执行目标对象的方法啦,自然也就打印原来的目标对象的全限定名了。

getArgs

image-1654654656998

它可以获取到被拦截的方法的参数列表

getSignature

image-1654654801484

它可以拿到方法的信息:

image-1654654854903

获取到的 Signature 就可以强转为类似于 Method 的 Signature ,这个接口就是 MethodSignature !在 MethodSignature 中定义了获取 Method 的方法:

public interface MethodSignature extends CodeSignature {
    Class getReturnType();
    Method getMethod();
}

所以我们就可以获取到执行的方法信息了

image-1654655113053

ProceedingJoinPoint 的扩展

在环绕通知中直接使用的就是 ProceedingJoinPoint,而它是基于 JoinPoint 的扩展,它扩展的方法只有 proceed 方法,也就是那个能让我们在环绕通知中显式执行目标对象的目标方法的那个 API 。

public interface ProceedingJoinPoint extends JoinPoint {
    void set$AroundClosure(AroundClosure var1);
    default void stack$AroundClosure(AroundClosure arc) {
        throw new UnsupportedOperationException();
    }
    Object proceed() throws Throwable;
    Object proceed(Object[] var1) throws Throwable;
}

在环绕通知中,可以自行替换掉原始目标方法执行时传入的参数列表!

返回通知和异常通知的特殊参数

返回通知中我们要拿到方法的返回值,异常通知中我们要拿到具体的异常抛出。这个非常的简单,只要在切面类中对应的方法上增加对应的参数即可,但是直接写在参数列表上,运行 main 方法是不好使的,是拿不到返回值的!我们还需要告诉 SpringFramework ,我拿了一个名叫 retval 的参数来接这个方法返回的异常,拿一个名叫 e 的参数来接收方法抛出的异常,反映到代码上就应该是这样:

@AfterReturning(value = "execution(* com..FinanceService.subtractMoney(double))", returning = "retval")
public void afterReturningPrint(Object retval) {
    System.out.println("Logger afterReturningPrint run ......");
    System.out.println("返回的数据:" + retval);
}

@AfterThrowing(value = "defaultPointcut()", throwing = "e")
public void afterThrowingPrint(Exception e) {
    System.out.println("Logger afterThrowingPrint run ......");
    System.out.println("抛出的异常:" + e.getMessage());
}

多个切面的执行顺序

日常开发中,或许我们会碰到一些特殊的情况:一个方法被多个切面同时增强了,这个时候如何控制好各个切面的执行顺序,以保证最终的运行结果能符合最初设计,这个也是非常重要的。

SpringFramework AOP 切面默认执行的顺序是根据切面类的 unicode 编码,按照十六进制排序得来的,unicode 编码靠前的,那自然就会排在前面。

显式声明执行顺序

  1. 继承 Ordered 接口,覆写 getOrder() 方法, getOrder() 方法中可以指定执行时机,默认是 Integer.MAX_VALUE,数字越小,越优先执行

  2. 使用 @Order 注解,@Order 注解的默认值是 2147483647,可以指定,数字越小越优先执行。

同切面的多个通知执行顺序

同切面的多个通知执行也是按照方法名称的 unicode编码顺序 来的,这个规则无法被改变(使用@Order注解无效),只能靠方法名去区分

代理对象调用自身的方法

有一些特殊的场景下,我们产生的这些代理对象,会出现自身调用自身的另外方法的。
SpringFramework 提供了一个 AopContext 的类,使用这个类,可以在代理对象中取到自身,它的使用方法很简单:

public void update(String id, String name) {
    ((UserService) AopContext.currentProxy()).get(id);
    System.out.println("修改指定id的name。。。");
}

使用 AopContext.currentProxy() 方法就可以取到代理对象的 this 了。但是直接写完之后,运行是不好使的,它会抛出一个异常:

Exception in thread “main” java.lang.IllegalStateException: Cannot find current proxy: Set ‘exposeProxy’ property on Advised to ‘true’ to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.

这个异常的大致含义是,没有开启一个 exposeProxy 的属性,导致无法暴露出代理对象,从而无法获取。开启 exposeProxy 这个属性的位置在注解 AOP 的那个 @EnableAspectJAutoProxy 上:

@Configuration
@ComponentScan("com.rsthe")
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露代理对象
public class AopContextConfiguration {
    
}

它的默认值是 false,需要改为 true 使其生效

SpringFramework 生成代理对象的时机

最先想到的肯定是 普通的 bean 在初始化阶段,被 BeanPostProcessor 影响后,在 postProcessorAfterInitialization() 方法中生成的代理对象 吧!这也是 AOP 的实现机制中最重要的环节之一。

解析切入点表达式的时机

问题:BeanPostProcessor 怎么知道哪些 bean 在创建时需要织入通知,生成代理对象呢?可能是在 bean 的初始化逻辑中检查的吧,可是检查的依据是什么呢?依据是那些被 @Aspect 标注的切面类,里面定义的 pointcut 方法定义的切入点表达式吧!但是什么时候解析这些切入点表达式呢?

  • 负责 AOP 的核心后置处理器初始化的时候再去 BeanFactory 中解析,此时所有的 BeanDefinition 都加载进 BeanFactory 了,而且后置处理器都初始化好了,普通 bean 也都没有创建,所以这个时机点是相对最合适的。

结论:在 AOP 核心后置处理器的初始化阶段,解析容器中的所有切面类中的切入点表达式


# AOP # AspectJ表达式