22-Spring源码解析之AOP(1)——@EnableAspectJAutoProxy注解原理

Spring版本:<version>5.2.1.RELEASE</version>

上一篇:21-Spring源码解析——IOC容器创建与Bean生命周期总结

截至到本篇文章,我终于把IOC写完啦!现在开启Spring的第二个百宝箱:AOP

一、AOP概览

我们知道,使用 面向对象编程(OOP 有一些弊端,当需要为多个不具有继承关系的对象引入同一个公共行为的时候,例如:日志、安全检测等,我们只有在每个对象里引用公共行为,这样程序中就产生了大量的重复代码,程序就不便于维护了,所以就有了一个对面向对象编程的补充,即 面向切面编程(AOPAOP所关注的方向是横向的,不同于OOP的纵向。

Spring 2.0采用@AspectJ注解对POJO进行标注,从而定义了包含切点信息增强横切逻辑的切面。Spring 2.0 将这个切面织入到匹配的目标Bean中。

下面,我们先来直观地浏览一下SpringAOP的简单示例。

1. 例子

我们这个例子比较纯粹,只有4个类

  • 创建用于拦截的计算器类
  • 切面类(用于实现AOP功能)
  • 配置类
  • 测试类

1.1 创建用于拦截的Bean

在实际工作中,这个Bean可能是满足业务需要的核心逻辑,比如满足计算需要的计算器类。这个类包含一个用于做除法运算的除法div()方法,但是我们想在div()方法前后增加日志来跟踪调试,如果这个时候我们直接修改源码并不符合面向对象的设计特点,而且随意改动原有的代码会造成一定的风险,还好Spring给我们提供了方案:在不改变计算器类的情况下,完成我们的增加日志的需求。

import org.springframework.stereotype.Component;

@Component
public class MathCalculator {

    public int div(int i, int j) {
        return i / j;
    }

    @Override
    public String toString() {
        return "MathCalculator{}";
    }
}

1.2 切面类

package com.fj.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

// @Aspect 告诉Spring这是一个切面类
@Component
@Aspect
public class LogAspects {

    // 抽取公共的切入点表达式
    @Pointcut("execution(public int com.fj.aop.MathCalculator.div(int, int))")
    public void pointCut(){};


    // MathCalculator类的所有方法 *,方法的参数是任意多的 ..
    @Before("pointCut()")
    public void logStart(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        System.out.println(" " + joinPoint.getSignature().getName()+ " 的【logStart】方法: 除法开始运行,参数列表是,{"+ Arrays.asList(args) +"}");
    }

    @After("pointCut()")
    public void logEnd(JoinPoint joinPoint) {
        System.out.println(" " + joinPoint.getSignature().getName()+ " 的【logEnd】方法: 除法结束。");
    }

    @AfterReturning(value = "pointCut()", returning = "result")
    public void logReturn(Object result) {
        System.out.println(" 【logReturn】方法: 运行结果:{"+ result +"}");
    }

    @AfterThrowing(value = "pointCut()", throwing = "exception")
    public  void logException(JoinPoint joinPoint, Exception exception){
        System.out.println(" " + joinPoint.getSignature().getName()+ " 的 【logException】 方法 除法出现异常,异常信息:{"+exception+"}");
    }
}


这个切面类包含5个方法:一个用于抽取公共的切入点表达式方法。剩下四个为:前置通知(@Before)、后置通知(@After)、返回通知(@AfterReturning)和异常通知(@AfterThrowing)方法。

扫描二维码关注公众号,回复: 9983524 查看本文章

1.3 配置类

@ComponentScan("com.fj.aop")
@Configuration
@EnableAspectJAutoProxy
public class MainConfig_AOP {
}

这个配置类很简单,主要的作用有两个:

  • 包扫描:将 创建用于拦截的Bean类和切面类扫描到Spring容器中
  • 开启基于注解的aop模式:@EnableAspectJAutoProxy

1.4 测试类

public class AOPTest {
    @Test
    public void test01() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig_AOP.class);
        Object calculator = applicationContext.getBean(MathCalculator.class);
        Object logAspect = applicationContext.getBean(LogAspects.class);
        System.out.println(logAspect.getClass().getName());
        System.out.println(calculator.getClass().getName());
        System.out.println(calculator instanceof MathCalculator);

        ((MathCalculator) calculator).div(3,2);
    }
}

测试类做了4个事情:

  • 输出从容器中获取到的LogAspects类型的Bean名字。
  • 输出从容器中获取到的MathCalculator类型的Bean名字。(这一步输出的结果很重要)
  • 判断从容器中获取到的MathCalculator类型的Bean是否还是MathCalculator类型
  • 调用获取到的MathCalculator类型的Beandiv()方法

1.5 测试结果

在这里插入图片描述
我看可以看到上面的执行结果:

  • 输出从容器中获取到的LogAspects类型的Bean名字。

    • Bean的名字是原来的 LogAspects
  • 输出从容器中获取到的MathCalculator类型的Bean名字。

    • Bean的名字已经不是原来的 MathCalculator,而是MathCalculator$$EnhancerBySpringCGLIB$$44e84cdd,为什么?为什么LogAspects得到的就是原来的LogAspects名字,而MathCalculator得到的就不是呢?之后详细介绍,这一步很重要!
  • 判断从容器中获取到的MathCalculator类型的Bean是否还是MathCalculator类型

    • 我们使用Object类型的对象来得到applicationContext.getBean(MathCalculator.class);的返回值,返回虽然已经不是我们原来的 MathCalculator,但是还是MathCalculator类型的对象,所以这一步返回true
  • 调用获取到的MathCalculator类型的Beandiv()方法

    • 因为方法正常返回没有出现异常,所以会调用切面类的logStart方法、logEnd方法和logReturn方法

从上面的执行结果可以看出,Spring实现了对MathCalculator类的div方法的增强,且增强的逻辑独立于MathCalculator类之外。

那么,Spring是如何实现AOP功能的呢?

那我们就得从源头开始找,首先我们知道,Spring创建容器的时候,是需要解析配置类(带有@Configuration注解的类)的吧,我们项目中所有的类都是通过这个配置类来让Spring发现且管理的。其次,记得不记得在本篇文章的例子中配置类上面增加了一个注解@EnableAspectJAutoProxy,对,就是它,这个注解是做什么的呢?就是开启基于注解的AOP模式。所以要分析AOP原理,首先就逮住@EnableAspectJAutoProxy开始分析。

二、 @EnableAspectJAutoProxy注解

我们看一下这个注解的源码

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

	boolean proxyTargetClass() default false;

	boolean exposeProxy() default false;

}

看它上面有一个非常重要的注解@Import(AspectJAutoProxyRegistrar.class),引入AspectJAutoProxyRegistrar类。我们先看一下这个类结构:

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
	@Override
	public void registerBeanDefinitions(
			AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

		AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

		AnnotationAttributes enableAspectJAutoProxy =
				AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
		if (enableAspectJAutoProxy != null) {
			if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
				AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
			}
			if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
				AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
			}
		}
	}

这个类实现了ImportBeanDefinitionRegistrar接口,且就一个方法:registerBeanDefinitions,从方法名字也可以看出来这个方法的作用是:注册BeanDefinition。具体registerBeanDefinitions方法是怎么注册BeanDefinition的这里先不分析。毕竟程序还没有debug到这里,后面会一步一步走到这里的时候会详细分析。

现在,我们就从初始化容器开始一步一步debug看一下到底Spring是怎么实现AOP功能的。

三、解析@EnableAspectJAutoProxy注解

我们从下面这行代码开始分析,不求快但求精致。

 ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig_AOP.class);

我们知道,在Spring创建容器的时候会从解析配置类开始管理我们项目中的类。那我们就从解析配置类开始。 解析配置类是在:refresh方法调用的第五个方法invokeBeanFactoryPostProcessors方法中进行。这里我只抽取与该功能有关的部分代码进行讲解,与该功能无关的方法暂时省略。(若有兴趣了解invokeBeanFactoryPostProcessors方法的同学参见文章10-Spring源码解析之refresh(4)

Spring首先创建解析配置类的类ConfigurationClassPostProcessor,然后开始利用ConfigurationClassPostProcessor解析配置类。

在这里插入图片描述
我们可以看到:

  • 当前postProcessorNames数组只有一个值,即ConfigurationClassPostProcessor类型的对象,我们先对它调用getBean方法,当前容器中还没有这个Bean,所以调用getBean方法后Spring会创建一个ConfigurationClassPostProcessor类型的对象。
  • 然后利用ConfigurationClassPostProcessor对象去解析我们的配置类(MainConfig_AOP

我们进入invokeBeanDefinitionRegistryPostProcessors方法看它是如何解析的配置类:

在这里插入图片描述
因为这里只有一个BeanDefinitionRegistryPostProcessor类型的PostProcessor,即解析配置类的类:ConfigurationClassPostProcessor。继续跟踪postProcessBeanDefinitionRegistry

在这里插入图片描述
上图调用的processConfigBeanDefinitions方法的实现有点多,所以就截取与解析@EnableAspectJAutoProxy注解有关的步骤了。

在这里插入图片描述
注意到目前为止只有上面的图片我标注了图一,是因为,一会解析完配置类后还要回到这里!

processConfigBeanDefinitions方法调用ConfigurationClassParser类型的对象parseparse方法来解析配置类mainConfig_AOP。 要开始解析咯,关注@Import是怎么解析的咯!

在这里插入图片描述

在这里插入图片描述
进入ConfigurationClassParser类的processConfigurationClass方法
在这里插入图片描述
遇到doXXX了!说明Spring要开始真正解析了。进入doProcessConfigurationClass方法,找到解析@Import的地方。

在这里插入图片描述
我们要注意一下右下角configClassimportBeanDefinitionRegistrars属性,在解析配置类mainConfig_AOP之前,这个属性里面什么都没有。

进入processImports方法

在这里插入图片描述
我们可以看到for循环中的值在本例子中只有一个,即AspectJAutoProxyRegistrar。在本篇文章二、节中我们已经知道AspectJAutoProxyRegistrar类实现了ImportBeanDefinitionRegistrar接口,因此会进入else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class))语句块。这个语句块的作用为:创建AspectJAutoProxyRegistrar类对象,然后将它加入到configClassimportBeanDefinitionRegistrar属性中。

因此执行完这段代码后,我们再看configClass

在这里插入图片描述

现在已经解析完了配置类mainConfig_AOP中的@Import注解,并将其对应的类添加到configClassimportBeanDefinitionRegistrar属性中了。那我们就回去看看解析完parse之后做了什么,即我们回到图一。

在这里插入图片描述
我们注意,在parser.parse(candidates)方法下面,我标注黄色框的区域,Spring创建了一个ConfigurationClassBeanDefinitionReader类型的对象,并将这个对象赋值给this.readerthis.reader是什么呢?马上去源码里看了一下粘了出来。

在这里插入图片描述
接着调用ConfigurationClassBeanDefinitionReaderloadBeanDefinitions方法。将configClasses传递进去。首先,我们得知道configClasses里面存储的东西,它里面存储的是从配置类mainConfig_AOP解析出来的类信息。那我们看一下当前项目中configClasses里面的值。

在这里插入图片描述

configClasses里面包含三个值:

  • LogAspects
  • MathCalculator
  • MainConfig_AOP

其中LogAspects类和MathCalculator类是通过解析@ConponentScan包扫描得到的。MainConfig_AOP类中有一个importBeanDefinitionRegistrars

接下来我们看一下this.reader.loadBeanDefinitions(configClasses);方法的具体实现

在这里插入图片描述

这里是一个for循环,我们主要关注当configClassmainConfig_AOP时,在执行loadBeanDefinition方法之前,我们先看一下当前beanFactoryBeanDefintion的情况。

在这里插入图片描述
注意只有7个BeanDefinition。接着我们开始执行这个方法的具体实现。

在这里插入图片描述
我将前面与AOP功能无关的条件语句折叠起来了,我们主要看最后一条红色框住的地方:注册configClassimportBeanDefinitionRegistrars属性值为BeanDefintion

在这里插入图片描述
实际上就是注册AspectJAutoProxyRegistrarBeanDefinition
在这里插入图片描述

从上图可以看出来,为了注册AspectJAutoProxyRegistrarBeanDefinition,程序走到了ImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法,但是由于AspectJAutoProxyRegistrar实现了该接口并重写了registerBeanDefinitions方法,因此下一步程序就走到了AspectJAutoProxyRegistrar类的registerBeanDefinitions方法中。即在本篇文章二、节贴出来的第二段代码。下面再重新贴出来一下。

在这里插入图片描述
到达了AspectJAutoProxyRegistrar类的registerBeanDefinitions方法才算开始与AOP功能有了进一步的深入了解。在registerBeanDefinitions方法中首先调用了AopConfigUtils类的registerAspectJAnnotationAutoProxyCreatorIfNecessary方法。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
从上图可以看出,在最后将AnnotationAwareAspectJAutoProxyCreator的信息作为BeanDefinition注册到beanFactory中。注册完AnnotationAwareAspectJAutoProxyCreator类型的BeanDefinition,解析@EnableAspectJAutoProxy注解的工作配置类就算是做完了。因此完成这一步我们就可以在BeanDefinition中看到新增AnnotationAwareAspectJAutoProxyCreator类型的BeanDefintion

在这里插入图片描述
我们看到,上图长须执行到return beanDefinition后,BeanDefinition中看到新增AnnotationAwareAspectJAutoProxyCreator类型的BeanDefintion

四、总结

@EnableAspectJAutoProxy注解:
在初始化容器 -> 调用refresh方法的第五个方法invokeBeanFactoryPostProcessors -> 创建ConfigurationAnnotationProcessor类 -> 利用ConfigurationAnnotationProcessor类解析配置类 -> 解析@Import类型 -> 调用loadBeanDefinition将所有配置类中的信息转换为BeanDefintion并将其存储到beanFactory中。

至此,@EnableAspectJAutoProxy注解就解析完成,解析的结果就是:将AnnotationAwareAspectJAutoProxyCreator类转换为BeanDefintion并将其存储到beanFactory中。


那么现在的问题就转变为了:

  • 为什么在配置类中写了 @EnableAspectJAutoProxy注解,Spring就为我们在容器中注册了一个AnnotationAwareAspectJAutoProxyCreator类的BeanDefinition
  • BeanDefinition只是Bean的定义信息,什么时候创建AnnotationAwareAspectJAutoProxyCreator
  • AnnotationAwareAspectJAutoProxyCreator类是什么,它的类结构是什么样子的
  • 实现AOP功能与AnnotationAwareAspectJAutoProxyCreator类有什么关系

那么我们就带着这4个问题来看下一篇文章。

发布了397 篇原创文章 · 获赞 71 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/xiaojie_570/article/details/104791656