Spring系列之AOP的原理及手动实现

目录

引入

到目前为止,咱们已经完成了简易的IOC和DI的功能,虽然相好比Spring来讲确定是很是简陋的,可是毕竟咱们是为了理解原理的,也不必必定要作一个和Spring同样的东西。到了如今并不能让咱们松一口气,前面的IOC和DI都还算比较简单,这里要介绍的AOP难度就稍微要大一点了。html

tips

本篇内容难度较大,每一步都须要理清思路,可能须要多看几遍,多画类图和手动实现更容易掌握。git

AOP

什么是AOP

Aspect Oriented Programming:面向切面编程,做用简单来讲就是在不改变原类代码的前提下,对类中的功能进行加强或者添加新的功能。程序员

AOP在咱们开发过程当中使用频率很是的高,好比咱们要在多个地方重用一段代码的功能,这时咱们能够选择的方式不少,好比直接代码拷贝,也能够将代码封装成类或方法,使用时调用。可是问题是这种方式对代码来讲有着很强的侵入性,对于程序员来讲,将重复的东西拷来拷去也是一件麻烦事。而AOP能够很好的解决这类问题,在AOP中咱们能够指定对一类方法进行指定须要加强的功能。好比咱们在系统中记录数据修改的日志,每一个对数据修改的方法都要记录,可是其实彻底是同样的方法,使用AOP能大大增长开发效率。github

AOP的一些概念

通知(advice):通知定义了一个切面在何时须要完成什么样的功能,通知和切点组成切面。正则表达式

切点(pointCut):切点定义了切面须要做用在什么地方。spring

切面(Aspect):是通知和切点的组合,表示在指定的时间点什对指点的地方进行一些额外的操做。数据库

连接点(join points):链接点表示能够被选择用来加强的位置,链接点是一组集合,在程序运行中的整个周期中都存在。express

织入(Weaving):在不改变原类代码的前提下,对功能进行加强。编程

关于AOP的简单分析

通知(advice)

通知定义了一个切面在何时须要完成什么样的功能,很明显advice的实现不是由框架来完成,而是由用户建立好advice而后注册到框架中,让框架在适当的时候使用它。这里咱们须要考虑几个问题。设计模式

用户建立好的advice框架怎么感知?框架如何对用户注册的不一样的advice进行隔离?

这个问题很简单,大多数人都明白,这就相似于Java中的JDBC,Java提供一套公共的接口,各个数据库厂商实现Java提供的接口来完成对数据库的操做。咱们这里也提供一套用于AOP的接口,用户在使用时对接口进行实现便可。

advice的时机有哪些?须要提供哪些接口?

这里直接拿Spring中定义好的加强的时机。

  • Before——在方法调用以前调用通知
  • After——在方法完成以后调用通知,不管方法执行成功与否
  • After-returning——在方法执行成功以后调用通知
  • After-throwing——在方法抛出异常后进行通知
  • Around——通知包裹了被通知的方法,在被通知的方法调用以前和调用以后执行自定义的行为

好了,咱们可使用一个接口来定义上面的处理方法,在用户使用的时候实现方法便可,以下:

貌似差很少了,可是咱们须要注意到,用户在使用advice的使用,不可能说每次都是须要对上诉几种方式同时进行加强,更多多是只须要一种方式。可是若是只有一个接口的话就要求用户每次都须要实现全部的方法,这样显的十分的不友好。

咱们应该让这些不一样的方法对于用户来讲是可选,须要什么就实现哪个。那么咱们须要将每个方法都对应一个接口吗?不须要。上面的after(...)afterSuccess(...)都是在方法执行以后实现,不一样在于一个须要成功后的返回值而另外一个不须要,这两个能够做为一个实现由返回值区分。进行异常后的加强处理这要求对被执行的方法进行包裹住,捕获异常。这就和环绕差很少了,二者能够放一块儿。

类图:

pointcut

advice基本就这样了,下面就是pointcut了。提及切点,用过Spring中的AOP的确定对切入点表达式比较了解了,在Spring中用户经过切入点表达式来定义咱们的加强功能做用在那一类方法上。这个切入点表达式十分的重要。对于咱们的手写AOP来讲,也须要提供这样的功能。固然表达式由用户来写,由咱们的框架来解析用户的表达式,而后对应到具体的方法上。

如何解析用户定义的表达式?上面说到了,由一串字符来匹配一个或多个不一样的目标,咱们第一个反应确定是正则表达式,很明显这个功能使用正则是能够进行实现的。但实际上这样的表达式还有不少。好比AspectJAnt path等。具体使用什么就本身决定了,这里我实现正则匹配这一种。

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
复制代码
  1. 如何找到咱们要加强的方法呢?

当咱们肯定好有哪些类的哪些方法须要加强,后面就须要考虑咱们如何获取到这些方法(对方法加强确定须要获取到具体的方法)。

  1. 有了表达式咱们能够肯定具体的类和方法,表达式只是定义了相对的路径,如何根据相对路径获取Class文件地址?

对bean实例的加强是在初始化的时候完成的,初始化的时候判断若是须要加强,则经过代理生成代理对象,在返回时由该代理对象代替原实例被注册到容器中。

  1. Class文件有了,怎么取到类中的方法?

在前面章节中咱们获取过方法,使用Class对象便可获取全部的非私有方法。在实际调用被加强方法时,将该方法与全部的advice进行匹配,若是有匹配到advice,则执行相应的加强。固然咱们并不须要每一次都须要遍历获取,为了效率能够对方法和加强的advice进行缓存。

Aspect/Advisor

咱们有了加强功能的实现和肯定了须要加强那些方法。到了如今咱们就须要将拿到的方法进行加强了。

在运行过程当中对已有的类或方法的功能进行加强同时又不改变原有类的代码,这妥妥的代理模式嘛。若是不理解代理模式的能够看这个教程:代理模式,代理模式能够在运行期间对方法进行加强,很好的实现咱们的需求。

到如今,用户要实现AOP须要提供什么呢?

用户若是要实现AOP,首先必须提供一个Advice(通知)来加强功能,一个expression表达式来定义加强哪些方法,实际上还须要指定使用哪个解析器来解析传入的表达式(正则,AspectJ...)。若是单独提供这些东西对用户来讲仍是比较麻烦的,而框架的做用是帮用户简化开发过程当中的流程,尽可能的简单化。因此在这里咱们能够对用户提供一个新的外观(门面),让用户更加简单的使用。这里实际上是使用了外观模式的思想。

当咱们在注册bean和调用方法时,对方法的加强会用到Advisor,因此咱们还须要提供一个注册和获取Advisor的接口。

AdvisorRegistry

Weaving

如今咱们有了切面,用户也已经可以比较简单的来定义如何使用切面,最重要的一步到了,那就是咱们应该如何对须要加强的类进行加强呢?何时进行加强?

上面已经说过了对类和方法进行加强就使用代理模式来加强。那么咱们做为框架该在什么何时来加强呢?

这里有两种时机。一是在启动容器初始化bean的时候就进行加强,而后容器中存放的不是bean的实例,而是bean的代理实例。二是在每一次使用bean的时候判断一次是否须要加强,须要就对其加强,而后返回bean的代理实例。这两种方法很明显第一种比较友好,只是让容器的启动时间稍微长了一点,而第二种在运行时判断,会使得用户的体验变差。

在初始化bean的那个过程来加强?会不会存在问题?

根据以前的介绍,咱们的框架初始化bean是在BeanFactory中进行,还包括bean的实例化,参数注入以及将bean放入容器中等。很明显对bean的加强应该是在bean实例化完成并在尚未放进容器中的时候。那么也就是在BeanFactory的doGetBean方法中了。这里有一个小问题在于,doGetBean方法作的事情已经够多了,继续往里加入代码无疑会使得代码大爆炸,很难维护也不易扩展。为了解决这个问题这里咱们可使用观察者模式来解决这一问题,将doGetBean方法中每个过程都做为一个观察者存在,当咱们须要添加功能是既能够添加一个观察者而后注入,这样不会对已有代码作出改变。

定义一个观察者的接口:

BeanPostProcessor

这里咱们暂时只定义了aop应用的观察者,其余的好比实例化,参数注入后面慢慢加。

BeanPostProcessor是在BeanFactory中对bean进行操做时触发,咱们也应该在BeanFactory中加入BeanPostProcessor的列表和注册BeanPostProcessor的方法。

BeanFactory

在这里的观察者模式的应用中,BeanFactory充当subject角色,BeanPostProcessor则充当observer的角色,BeanFactory监听BeanPostProcessor,咱们能够将功能抽出为一个BeanPostProcessor,将其注册到BeanFactory中,这样既不会使得BeanFactory中代码过多,同时也比较容易作到了功能的解耦,假设咱们不须要某一个功能,那么直接接触绑定便可而不须要任何其余操做。在这里咱们只实现了Aop功能的注册。

image

假设咱们要对其余功能也抽为一个观察者,那么直接继承BeanPostProcessor接口实现本身的功能而后注册到BeanFactory中。

功能实现分析

如今接口有了,咱们如今须要考虑如何来实现功能了。那么咱们如今梳理一下咱们须要作什么。

  1. 在进行bean建立的时候,须要判断该bean是否须要被加强,这个工做是由AopPostProcessor接口来作,判断是否须要被加强和经过哪一种方式来加强(JDK代理仍是cglib代理)。若是须要加强则建立代理对象,注册到容器是则使用该代理对象。
  2. 在1中说到须要建立代理对象,那么咱们也就须要提供代理的实现,目前代理主要是经过JDK代理和cglib代理模式,二者的主要区别在去JDK代理模式必需要求类实现了接口,而cglib则不须要。
  3. 在实际对实例加强方法调用时,框架须要对该方法的加强方法进行调用,如何进行调用以及存在多个加强方法是如何来调用。

如今咱们对以上的问题分别分析解决。

代理实现

代理的实现就是常规的实现,咱们提供对外建立代理实例的方法和执行方法的处理。

AopProxy

JDKDynamicProxy和CglibDynamicProxy共同实现了AopProxy接口,除此以外要实现代理JDKDynamicProxy还需实现InvocationHandler接口,CglibDynamicProxy还需实现MethodInterceptor接口。

可能有朋友注意到了,在建立代理的类中都有一个BeanFactory的变量,之因此会用到这一个类型的变量是由于当方法运行时匹配到advice加强时能从BeanFactory中获取Advice实例。而Advisor中并无存Advice的实例,存储的是实例名(beanName)。可是问题在于这个变量的值咱们如何获取,对于通常的bean咱们能够从容器中获取,而BeanFactory自己就是容器,固然不可能再从容器中获取。咱们首先梳理下获取变量值的方法:

  1. 经过依赖注入从容器中获取,这里不合适。
  2. 直接建立一个新的值,这里须要的是容器中的实例,从新建立新的值确定没了,若是再按照原流程走一次建立如出一辙的值无疑是一种愚蠢的作法,这里也不合适。
  3. 传参,若是方法的调用流程能够追溯到该变量整个流程,能够经过传参的方式传递
  4. Spring中的作法,和3差很少,也是咱们平时用的比较多的方法。提供一系列接口,接口惟一的做用就是用于传递变量的值,而且接口中也只有一个惟一的Set方法。

Aware

提供一个Aware父接口和一系列的子接口,好比BeanFactoryAware ,ApplicationContextAware用于将这些值放到须要的地方。若是那个类须要用到Spring容器的变量值,则直接实现xxxAware接口便可。Spring的作法是在某一个过程当中检测有哪些类实现了Aware接口,而后将值塞进去。

这里咱们的准备工做都已经差很少了,后面就是开始将定义好的接口中的功能实现了。

image

若是存在多个不一样类型的加强方法时如何调用

因为在加强过程当中,对于同一个方法可能有多个加强方法,好比多个环绕加强,多个后置加强等。一般状况下咱们是经过一个for循环将全部方法执行,这样的:

执行顺序

可是这里的问题在于,这中间的任何一个环绕方法都会执行一次原方法(被加强的方法),好比在环绕加强中的实现是这样的:

//before working
//invoke 被增强的方法执行
//after working 
复制代码

这样若是仍是一个for循环执行的话就会致使一个方法被屡次执行,因此for循环的方法确定是不行的。咱们须要的是一种相似于递归调用的方式嵌套执行,这样的:

递归顺序

前面的方法执行一部分进入另外一个方法,依次进入而后按照反顺序结束方法,这样只需把咱们须要增强的方法放在最深层次来执行就能够保证只执行依次了。而责任链模式能够很好的作到这一点。

调用流程的具体实现:

public class AopAdviceChain {

    private Method nextMethod;
    private Method method;
    private Object target;
    private Object[] args;
    private Object proxy;
    private List<Advice> advices;

    //通知的索引 记录执行到第多少个advice
    private int index = 0;

    public AopAdviceChain(Method method, Object target, Object[] args, Object proxy, List<Advice> advices) {
        try {
            //对nextMethod初始化 确保调用正常进行
            nextMethod = AopAdviceChain.class.getMethod("invoke", null);
        } catch (NoSuchMethodException | SecurityException e) {
            e.printStackTrace();
        }

        this.method = method;
        this.target = target;
        this.args = args;
        this.proxy = proxy;
        this.advices = advices;
    }

    public Object invoke() throws InvocationTargetException, IllegalAccessException {
        if(index < this.advices.size()){
            Advice advice = this.advices.get(index++);
            if(advice instanceof BeforeAdvice){
                //前置加强
                ((BeforeAdvice) advice).before(method, args, target);
            }else if(advice instanceof AroundAdvice){
                //环绕加强
                return ((AroundAdvice) advice).around(nextMethod, null, this);
            } else if(advice instanceof AfterAdvice){
                //后置加强
                //若是是后置加强须要先取到返回值
                Object res = this.invoke();
                ((AfterAdvice) advice).after(method, args, target, res);
                //后置加强后返回  不然会多执行一次
                return res;
            }
            return this.invoke();
        }else {
            return method.invoke(target, args);
        }
    }
}
复制代码

在代码中能够看到,若是是前置加强则直接调用,而若是是环绕或者后置加强,则都不会马上执行当前的加强方法,而是相似递归调用同样,进行下一个执行。这样就能保证被加强的方法不会被屡次执行,同时对方法加强的顺序也不会乱。

代码托管

在上面基本都只是分析了主要的原理和实现思路,在实际实现过程当中涉及的类和借口会更多,一些涉及到公共方法或者工具类上面都没有列出,因为代码较多限于篇幅缘由不在文章列出。若需看实现,代码已经所有托管到GitHub

小结

AOP的简单实现这里也算是完成了,AOP算是比较难的内容了,主要是涉及到知识点不少。使用的设计模式也不少,包括工厂模式,外观模式,责任链模式等等。而且也和前面的IOC和DI的内容紧密相关。因此你们最好仍是理一遍思路后能手动进行实现一次,这样掌握起来也比较容易。