【Spring】AOP源码解析

AOP源码解析

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

这篇文章将带你学习Spring AOP的基本使用以及底层原理。

1.基本使用

开启AOP动态代理的配置。

package com.ducx.playground.spring;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
 * AOP配置类,开启自动代理功能
 */
@EnableAspectJAutoProxy
@Configuration
public class AopConfig {
    
}

声明需要被代理的类的接口(JDK动态代理)

/**
 * 被代理的接口
 */
@Component
public interface Person {
    //切点方法
    public void study();
}

声明接口类的实现类,也就是实际的被代理的类。

package com.ducx.playground.spring;

import org.springframework.stereotype.Component;

/**
 * Person类的实现类,注意如果是CGLIB动态代理那么这里就不能实现接口!
 * 并且JDK动态代理的切点方法必须实现于接口父类。
 */
@Component
public class Student implements Person{

    //实际的被代理的方法
    @Override
    public void study(){
        System.out.println("study");
    }
}

声明一个切面,定义前置通知、后置通知。

package com.ducx.playground.spring;

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

/**
 * 切面
 */
@Aspect
@Component
public class StudentAspect {

    //切点的表达式
    @Pointcut("execution(* com.ducx.playground.spring.Student.study(..))")
    public void study(){}

    //前置通知
    @Before("study()")
    public void beforeStudy(){
        System.out.println("before study");
    }

    //后置通知
    @After("study()")
    public void afterStudy(){
        System.out.println("after study");
    }
}

测试类

package com.ducx.playground.spring;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * AOP动态代理测试类
 */
public class SpringClient {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext();
        //注册我们AOP需要被用到的bean,当然这里也可以在容器的构造方法里面指定要被扫描的包路径
        context.registerBean(AopConfig.class);
        context.registerBean("student", Student.class);
        context.registerBean("studentAspect", StudentAspect.class);
        //刷新容器生成bean
        context.refresh();
        
        //JDK动态代理的测试,因为JDK动态代理生成的是指定类的接口类的子类,
        // 所以这里的类似必须是接口类否则会类型转换异常
        Person person = (Person) context.getBean("student");
        //代理方法的调用
        person.study();

        //CGLIB动态代理,因为CGLIB是生成指定类的子类所以这里的类型可以是Student
        Student student = (Student) context.getBean("student");
        //代理方法的调用
        student.study();
    }
}

使用总结

  • 要通过注解开启自动动态代理的功能。

  • 要想使用JDK动态代理的话需要定义一个接口类让我们需要被代理的类实现这个接口,并且被代理的方法必须要在接口类中声明

  • 要想使用CGLIB动态代理的话就不能有实现接口,当然也可以强制指定用CGLIB

  • 切面中定义指定的切点以及方法。

2.代理类的生成以及执行流程

2.1代理类生成的时机

代理类的生成时机是在doCreateBean方法中在bean被实例化完成并且通过populateBean方法执行填充完bean属性之后的初始化bean步骤的时候,在方法initializeBean中的调用初始化方法的之前和之后会调用相应的后置处理器来对bean实例进行进一步处理。关于后置处理器的调用流程可以去看看我之前写的ioc流程的文章,这里就不多说了。

2.2代理对象的生成过程

关于代理类的生成spring使用的是AbstractAutoProxyCreator这个后置处理器,虽然这个类看起来不像是一个后置处理器但是他简介地继承了spring的后置处理器,继承了postProcessBeforeInstantiation和postProcessAfterInitialization两个方法。而实际生成代理对象的方法是postProcessAfterInitialization方法,通过这个方法可以生成一个原来bean的代理对象bean。我们来看一下其中的代码代码不长也很容易理解,如果当前的bean有一个早期的代理应用的话那么就调用warpIfNecessary方法来生成代理类。

我们看一下方法上面的注释:通过配置的拦截器来创建一个代理。这里就点题了,AOP的before、after、around都是通过***拦截器来拦截被代理的方法来实现的***。

2.2.1主要流程

我们点开wrapIfNecessary方法,这个方法就是实际生成代理bean的方法。我们看一下方法上面的注释:将符合条件的bean包装成代理对象。

总结一下整个方法的流程也就是实际生成代理对象的流程:

  • 判断当前bean是否应该被代理
  • 获取符合条件的advice(增强器)
  • 根据获取到的增强器来生成代理对象

2.2.2判断当前bean是否应该被代理

上图红框部分的代码就是判断当前bean是否应该被代理的代码。

  • 首先是判断当前bean是否已经被代理过。
  • 再判断当前bean是否没有配置增强器。
  • 最后判断当前bean是否是一些不应该被代理的基础bean。
2.2.3获取符合条件的增强器

在实际生成代理对象之前需要先获取我们代码中针对于这个当前bean配置的增强器,也就是我们实际开发中的使用@Aspect注解修饰的类中配置的@Before、@After、@Around方法,在AOP中将这些方法获取到并封装成了增强器(Advisor)。这个方法的逻辑比较多我们一步一步地看。首先我们点开getAdvicesAndAdvisorsForBean方法,这个方法的作用就是获取所有的当前bean的符合条件的增强器。在调用完这个方法之后就将所有的增强器返回到上一层。

我们点开findEligibleAdvisore方法来看看获取符合条件的增强器的实际方法的执行逻辑。

总体来说有四个步骤:

  • 获取所有层次的容器中的实现了Advisor接口的增强器。
  • 获取生成当前代理bean需要使用到的增强器。
  • 允许子类通过钩子方法在增强器被提交之前进行修改。
  • 通过Array的比较器对增强器进行排序。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c3LWc8dk-1625636068658)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210630211058804.png)]

首先是获取所有层次容器中的实现了Advisor接口的增强器,这里的实现逻辑很简单先尝试从各个层次的容器中获取类型为Advisor的beanName,然后通过调用beanFactory的getBean方法来获取这些bean的实例再返回。
在通过获取配置的Advisor之后就会去调用findAdvisorsThatCanApply方法来获取实际要在当前bean代理中使用的增强器。我们看一下这个方法里面canApply方法。

总结一下里面主要的逻辑:

  • 根据@Pointcut注解中配置的execution表达式判断当前的bean类是不是我们被切的类。
  • 如果当前类是需要被代理的类就获取其父接口。
  • 遍历上一步获取到的类,遍历其中的方法,根据@Pointcut注解中配置的execution表达式判断切点方法是否匹配。

2.2.4createProxy创建代理

在获取完当前代理需要的增强器之后就需要创建代理对象了。创建代理对象的方法是createProxy,我们点开这个方法,

可以看到这个方法的执行步骤如下:

  • 构造代理工厂ProxyFactory,我们的代理对象是通过这个工厂去生产的。

  • 判断是直接代理当前类还是代理当前类的接口类。

  • 构建增强器。

  • 设置上面获取到的代理目标类到ProxyFactory。

  • 执行自定义的构造方法。

  • 通过工厂创建代理对象。

    我们首先来说一下红框部分的代码,判断需要代理的是当前类还是当前类的接口类。默认都是先去尝试代理当前类的接口类,但是如果当前类没有接口类,那么在evaluateProxyInterfaces方法里面还是会设置去代理当前的类。代理当前类还是接口类就直接决定了是使用jdk动态代理还是使用cglib动态代理。

    jdk动态代理是通过继承当前类的接口类来生成一个当前类的增强对象,而cglib动态代理是通过生成一个当前类的继承子类来实现方法的增强对象。所以当前类实现了接口类那么默认情况下会使用jdk动态代理来对类实现增强,当然这种情况也可以强制指定使用cglib代理,而当前类没有接口实现类那么就只能使用cglib进行动态代理

    buildAdvisors方法是用来合并我们当前获取到的Advisor和容器中配置的基础的Advisor,这些Advisor都是需要被用作代理的增强。

2.2.5实际的创建代理对象AopProxy

在完成获取增强器以及设置ProxyFactory的准备工作之后就要开始实际的创建代理对象了。在createProxy方法的最后一行我们可以看到这里是通过proxyFactory的getProxy方法来生成代理对象的。我们点开这个方法,抽丝剥茧之后来到了DefaultAopProxyFactory类的createAopPoxy方法,在这里就判断了我们之前获取到的实际代理类是接口类还是普通类,如果是接口类的话那么就使用jdk动态代理,否则就使用cglib动态代理。而这里入参中的AdvisedSupport类型的config参数其实就是针对于我们目前生成代理对象需要的参数的一个包装,包括之前获取到的所有的增强器以及目标类、代理接口等等参数。

而这里的JdkDynamicAopProxy、ObjenesisCglibAopProxy方法返回的对象都是对当前AdvisedSupport中代理参数的封装类对象,都是实现了AopProxy接口的代理对象。到此为止我们的代理对象bean就生成好了。注意这里返回的代理对象只是一个代理参数的封装

2.2.6创建Proxy对象,以及代理方法的执行过程
cglib动态代理

代理对象的实际创建是通过调用上一步获取的AopProxy对象的getProxy方法来获取的,上面的步骤只是生成了类似于一个代理对象的工厂的对象在里面已经设置了代理需要的参数,包括targetClass、增强器等等。那么现在就需要去创建代理对象了。

cglib创建代理对象是通过Enhancer来实现的,关于Enhancer是怎么使用的这里就不介绍了。

总结一下总体的流程:

  • 获取将要被代理的类。
  • 创建Enhancer。
  • 获取回调并配置Enhancer。
  • 创建代理对象。

可以看到这里调用了getCallbacks方法,这个方法的作用是获取并设置对象的拦截器。我们看一下这个方法里面比较关键的一行代码。这里是将我们之前获取到的advised封装到一个DynamicAdvisedInterceptor类型的拦截器里面。而我们在使用代理对象调用被代理的方法的时候就会调用这个拦截器里面的intercept方法。

我们点开DynamicAdvisedInterceptor这个拦截器看看里面的执行逻辑。比较重要的地方我用红字标注了。方法中通过getInterceptorsAndDynamicInterceptionAdvice方法来获取当前被代理方法的advise数组,在这里被称为链可以看到返回的类型是List< Object >类型的。之后根据上面是否获取到了执行链来判断是否需要执行代理方法。如果执行链中没有advise的话那么就直接执行被代理的方法。如果不为空的话就创建一个CglibMethodInvocation也就是cglib方法执行器来执行当前被代理方法的代理方法,这里调用的是执行器的proceed方法,proceed字面意思就是继续执行,那么根据这个方法名我们就可以知道执行的流程一定是before -> after这样的流程。

我们点开proceed方法一探究竟。首先会有一个变量currentInterceptorIndex标记当前执行到的方法,从-1开始。而interceptorsAndDynamicMethodMatchers缓存里面就记录了我们当前需要执行的所有方法包括代理方法以及被代理的方法,上面说到了执行之前要被执行的方法就被按照需要执行的顺序放在list里面了,所以一个被before、after代理的切点在这个list中的顺序就是before -> after。这里获取当前要被执行的方法是通过代码++this.currentInterceptorIndex,所以我们最先开始会获取到before,之后看到方法的开始调用了invokeJoinpoint方法,这个方法执行的就是我们的切点方法。由于我们是动态代理,所以并不会走中间部分的代码逻辑。最后代理方法的执行是调用了MethodInterceptor类的invoke方法。

我们通过断点调试可以看到before的代理方法执行的是MethodBeforeAdviceInterceptor类型拦截器的invoke方法,通过MethodBeforeAdvice中的before方法来执行before的逻辑,关于before方法的具体实现逻辑这里就不深究了只是反射调用方法。

关于After方法是在AspectJAfterAdvice类的invoke中的finally中执行的。

jdk动态代理

jdk动态代理的代理对象的生成是通过反射调用构造方法来生成的。

我们点开newProxyInstance方法,里面的执行逻辑很简单,先通过当前被代理对象的接口来获取代理对象的构造方法,再通过这个构造方法通过反射来生成代理对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8gg4iZcy-1625636068665)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210707101434986.png)]

而通过代理对象来调用方法其实调用的就是代理对象的invoke方法。这个方法在JdkDynamicAopProxy类中。在这里我把比较关键的代码通过红字标注了。大致的执行流程和cglib的intercept方法相似。

  • 判断是否是equals或hashCode方法,如果是的话就直接调用。
  • 暴露增强器。
  • 获取增强器的调用链。
  • 创建方法执行器,调用proceed方法。
  • 返回值类型的转换。

关注我的gitte或公众号可以获取mysql、jvm、redis底层原理的脑图哦。

gitte地址:https://gitee.com/duchenxi/total-war
公众号:完美的工程学

公众号二维码

mysql部分脑图
mysql脑图

JVM部分脑图
jvm脑图

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值