SpringAop浅析

137 阅读7分钟

这是我参与2022首次更文挑战的第26天,活动详情查看:2022首次更文挑战

介绍

AOP面向切面编程。在运行时,动态的将代码织入到类的指定方法,指定位置上的思想。实际上AOP不一定都像Spring Aop那样实现,Spring Aop是通过在运行时生成代理对象来进行织入。还有其他的方式,比如AspectJ是在编译期、类加载期织入。本篇文章就来介绍,Spring Aop在运行时生成代理对象的时期和过程。

基础回顾

​ 若想使用SpringAop可以通过两种方式,一种是声明式,通过调用Api的方式;一种是通过注解方式。两者在真正触发生成代理对象的点略有不同。

1.声明式

​ 这里只做简单介绍,首先需要实现某种通知类型的接口,再实现切点接口(Pointcut),如果是环绕通知,需将通知实现类和切点实现类绑定DefaultPointcutAdvisor,最后在客户代码中将通知、目标类等信息添加到ProxyFactory实例中后,通过ProxyFactorygetProxy();获取代理对象,从而实现SpringAop

2.注解式

​ 通过@Aspect标记一个类为切面类,并通过@Pointcut注解标记一个切点,最后通过注解标记通知类型,比如环绕通知使用@Around进行Aop的业务处理。

那么Spring如何去解析这些注解?在什么时机解析?

原理

1.开启SpringAop

​ 在Spring中提供一个开启SpringAop的配置注解@EnableAspectJAutoProxy(在springboot中一般标记在启动类上),在这个注解上使用@Import注解引入SpringAop配置类AspectJAutoProxyRegistrar

  • proxyTargetClass: 为true的话开启cglib代理,默认为jdk代理
  • exposeProxy: 是否将代理对象暴露到线程上下文中
public @interface EnableAspectJAutoProxy {

    boolean proxyTargetClass() default false;
    
    boolean exposeProxy() default false;
}

AspectJAutoProxyRegistrar实现ImportBeanDefinitionRegistrar#registerBeanDefinitions接口,在Spring启动扫描配置类后会调用该接口。重点在于会注册AnnotationAwareAspectJAutoProxyCreatorBeanDefinitionMapAnnotationAwareAspectJAutoProxyCreator是一个BeanPostProcessor后置处理器这个方法比较简单, 可自行跟进去看一眼。

public void registerBeanDefinitions() {
    // 重点在这
    AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

    //...
}

2.Aop代理对象的生成

​ 在开启SpringAop时添加了一个AnnotationAwareAspectJAutoProxyCreator后置处理器到Spring容器中,在Spring启动的生命周期中,会在适时调用它的实现方法。

​ 看一下这个类的类图,AnnotationAwareAspectJAutoProxyCreator实现了AbstractAutoProxyCreator抽象类,在这个抽象类中,实现了BeanPostProcessor生命周期接口。

Bean实例化之前调用接口:用于提前生成代理对象,this.advisedBeans用于标记某个key是否需要进行AOP代理

public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
    Object cacheKey = getCacheKey(beanClass, beanName);
    TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
    return null;
}

Bean初始化之后调用接口:这个接口的调用点是在Bean对象初始化之后, 也就是Bean对象基本上走完了初始化流程。一般情况下remove(cacheKey) != bean肯定会成立,因为前者remove方法返回为空,只有在循环依赖时,可能会出现提前暴露的bean对象与当前bean不等

public Object postProcessAfterInitialization(Object bean, String beanName) {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}

进入wrapIfNecessary方法:如果已经完成代理了,那么直接返回这个代理的对象

  1. 若这个Bean对象被标记无需AOP代理, 直接返回
  2. 如果不需要代理,直接返回,且标记为无需AOP代理
  3. 获取通知可以作用在这个Bean上的通知,主要逻辑在findCandidateAddvisors方法,首先获取容器中类型为Advisor的Bean, 再从容器中获取@Aspect注解标记的Bean后, 将二者结合。最后筛选出可以作用到这个Bean的通知。
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    // 1.
    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
        return bean;
    }
    // 2.
    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }

    // 3.
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
}

根据一定的配置规则获取动态代理工厂的实现类(jdk动态代理或者是cglib动态代理)

public Object getProxy(ClassLoader classLoader) {
    return createAopProxy().getProxy(classLoader);
}

我这边跟踪下来createAopProxy()返回的是cglib动态代理的实现,所以以cglib动态代理为例,继续往下走。进入CglibAopProxy#getProxy:获取正在包装的bean的类型然后进行cglib动态代理的创建。

3.代理对象的调用

  1. 获取匹配该方法的通知列表主要逻辑在getInterceptorsAndDynamicInterceptionAdvice方法中根据advised中的通知列表,对当前调用方法进行匹配,将匹配成功的通知转为拦截器链返回。
  2. 实例化CglibMethodInvocation后执行proceed方法
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    try {
        // 1. 
        List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
        Object retVal;
        if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
        
        }else {
            // 2. 
            retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
        }
        retVal = processReturnType(proxy, target, method, retVal);
        return retVal;
    }
}

执行拦截器链proceed(),进入ReflectiveMethodInvocation#proceed方法

调用拦截器中的invoke方法,可以看到这里将this作为参数传入了,这里是一种拦截器链的典型用法在我之前有篇文章中有两种拦截器链的典型用法,二者的区别就是有没有携带链对象,这里明显是携带链对象,从而达到拦截器链逐个执行的目的。

  1. 表明执行到链尾, 直接通过反射调用目标方法
  2. 获取到下一个要执行的拦截器
public Object proceed() throws Throwable {
    // 1.
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return invokeJoinpoint();
    }

    // 2.
    Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
        // ..
    } 
}

这里返回的是根据通知类型的不同会进入到不同的具体实现中,比如我的测试代码是环绕通知类型,这里进入环绕通知的具体实现中AspectJAroundAdvice#invoke去执行当前链节点的invoke方法。这个方法中这里只是将链对象包装了一层。

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
  ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
  ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
  JoinPointMatch jpm = getJoinPointMatch(pmi);
  return invokeAdviceMethod(pjp, jpm, null, null);
}

反射调用通知方法,把链对象封装在了这个arctualArgs中了, 所以切面方法中通过jointPoint.process方法实际调用方法是与链对象挂钩的, 如果执行到链尾这调用目标对象, 若非链尾, 则继续进入链节点对象。

protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
  Object[] actualArgs = args;
  try {
    ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
    return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
  }
}

以我的测试代码为例,环绕通知@Around("testPointcut()")的标记实现方法中,一般在前置执行一些业务代码之后,会调用ProceedingJoinPoint#process()方法,后再调用后置的业务代码。

ProceedingJoinPoint#process()方法就是转入下一个拦截器链的方法。这个proceed方法就回到了上面的ReflectiveMethodInvocation#proceed,且链下标不是-1, 而是下一个链节点的下标。

public Object proceed() throws Throwable {
    return this.methodInvocation.invocableClone().proceed();
}

注意:我这里使用的是springboot项目,仅在项目启动类上添加了一个@EnableAspectJAutoProxy,且默认情况下该注解的proxyTargetClass()属性为false,那么为什么我在调试的时候会进入到Cglib动态代理的实现中,逻辑上应该是使用jdk动态代理?

​ 这是因为springboot的自动装配AopAutoConfiguration中,配置的是cglib动态代理。

采用jdk动态代理的配置:需明确指定配置项spring.aop.proxy-target-class=false

采用cglib动态代理的配置:明确指定配置项spring.aop.proxy-target-class=true或者无该配置项时启用cglib动态代理

总结

​ 这里只是对SpringAop做了个很浅的分析,主要是明确SpringAop的大致创建时机、创建流程和基本的调用流程。SpringAop是采用动态代理的方式实现,通过@EnableAspectJAutoProxy的方式开启,开启的原理为添加AnnotationAwareAspectJAutoProxyCreator后置处理器到BeanDefinitionMap中;在Bean对象实例化之前可以通过用户自定义的方式进行提前生成代理对象。或者是在Bean对象初始化之后,通过上述后置处理器的postProcessAfterInitialization方法,将Bean对象转为代理对象。

​ 在业务中调用代理对象的某个方法时,进入对应的拦截方法intercept方法,该方法会去获取匹配当前被调用方法的拦截器链,进入链对象后,通过反射调用对应的切面方法后,通过链对象调用下一个链节点,从而遍历整个拦截器链列表,这是一种典型的拦截器设计,我之前也有写一篇文章有简单的介绍了拦截器的两种使用方式。