Spring之所以成为java工程师界的“南天一柱,万世楷模”,除了有帮我们解决对象管理的IOC(可见我上一篇文章),还有对面向对象编程的有力补充的AOP(Aspect Oriented Programming)面向切面编程。还是那个古老的话题,专业术语可以帮我简化交流复杂度,却对概念进行了封装,让其变得晦涩难懂。所以我们需要搞清楚的第一点就是何为“面向切面”?为什么一定需要“面向切面”?不需要行不行?
这里补充一句,昨天有朋友跟我反馈了下,希望我可以多聊些Spring里的设计模式。首先我非常感谢有这些能够认真看完我这个菜鸡博客并且经过思考还提出宝贵意见的朋友,也正是你们我才能乐此不疲的继续写下去^_^!但我个人的观点是我们拿到一个问题,正常逻辑是先去分析问题,然后解决问题,至于解决方案,那是仁者见仁智者见智的事儿。就像面向切面也不是非要动态代理不可,用内部类一样可以做到。当然了,Spring AOP中确实相比于IOC要复杂的多,所以自然有相当的不错的设计模式模式,后面咱们再细聊这个问题。首先,我们还是先把目光放到我们最开始的问题上,不忘初心嘛~~
软件工程,不仅仅是实现一个功能就完事了,随着时间的推移,版本的迭代,会产生不计其数的需求变更。有一句笑话叫作“写时一时爽,重构火葬场”讲的就是写代码的人没有考虑过未来的变更场景,导致修改时候的工作量如同再造,那你加班加到猝死不能也怪资本主义丑陋的面目吧,哈哈哈。所以我们希望的就是能否有这样的一个功能,当我们要往1000个类上面加一个功能时候,不需要修改这1000个类,而是有一个名为切面的类,里面写上我们需要添加的功能,然后分别切开那1000个类,悄悄的帮我们把功能填进去。也正是这么个奇思妙想,造就了现如今的伟大的AOP模块!
在我刚开始学习Spring AOP的时候也去网上找了很多博客,其实他们大致都是千篇一律的讲JDK动态代理和Cglib,然后大刀阔斧的在讲代理模式,适配器模式这类设计模式。我不是觉得他们讲的不好讲的不对。只是他们都在很细化的讲“庐山”的某一花某一草某一木,我看完之后还是不是很清楚这个“庐山”真面目。我不知道你们有没想过这个问题。那些博客说了那么多代理对象原理,请问这个代理对象什么时候将我这个正常的对象给狸猫换太子成代理对象?所以,为了给我的好奇心买单,我开始着手于AOP的源码。
Spring的IOC是基石,所以即便是AOP也会将对象托管给IOC容器。回到最开始的需求,我既然把对象给你Spring管理,自然存在向你索取的需求,所以我还是以getBean为线索展开。开始之前,我先解释一下几个概念,因为Spring的类的含义往往从它的名字中暴露出来,可以减少阅读源码负担。
1. Advice:通知。虽然我至今还不清楚老外为什么取名字,所以不管它语言含义。Advice是AOP联盟定义的接口,代表切面行为。Spring自身提供了三种切面切入的方式,落实到Spring源码中的类是MethodBeforeAdviceInterceptor,AfterReturningAdviceInterceptor,ThrowsAdviceInterceptor。也就是我们熟悉在目标方法之前之前执行切面逻辑,目标方法返回之前执行切面逻辑,抛出异常的时候执行切面逻辑
2. PointCut:切点。这个比较容易理解,就是要被代理的方法,也就是我们所说的目标方法
3. Advisor:通知器。封装Advice,PointCut。因为我们通常在配置文件中声明一个切面配置的时候,Advice和Pointcut都是成对出现的,不是吗?
上述的匹配方法是采用的AspectJ的库函数,具体做法目前不是我们关心的重点,其实也就是玩转字符串,匹配 <property name ="expression" value ="execution(* main.Knight.say(..))"> 中的Knight是不是和我这个bean(类型为main.Knight)一致,就像JSON工具包,虽然我也不知道他的具体做法,但我知道底层大致的做法足矣。
如此一来,我们就清楚了AOP是如何做到“狸猫换太子”了。接下来就是众多博客都有讲解的JDK Proxy 和Cglib做法了。Spring源码在决定生成代理对象的时候会去判断下目标类有没有实现接口,如果实现了接口就生成Proxy代理对象,如果没有实现接口就通过Cglib生成一个类去继承目标类,复写目标类中需要加入切面逻辑的方法,所以目标类方法不能声明称final类型的。这就是二者的区别!这不是我最关心的地方,其实若想知道他们二者的本质区别,只要写一个小demo,然后用反编译的工具就可以一探二者究竟,这里就不赘述了,下面我也只介绍Proxy。
Spring中JDK 的Proxy类名曰JdkDynamicAopProxy,也就是我们耳熟能详的动态代理。这个类主要实现了AopProxy,InvocationHandler接口
所谓的代理模式就是,当你对代理对象调用目标类的方法时候,JVM都会触发上图这个invoke方法,以改变方法调用的轨迹,实现切面逻辑的悄悄织入,这是被API封装掉的,如果想落实真相,可以去反编译。我们先看看invoke里面都做了什么?
没错,methodInvocation.procceed()就是一个回调方法。
看到没有,这个procceed就是通过反射的机制调用目标对象的方法。只不过他的调用需要交给拦截器来调用,也就是所谓的回调。
如此一来Spring实现AOP的脉络就很清楚了,真实的Spring AOP是非常非常复杂的。除非亲自去看,不然很难在有限的篇幅讲清楚,而且本人还是那句话,解决方案是应运着需求的,技术总是后知后觉得,有印象的朋友可能会记得我在前面提到过Spring AOP有用到适配器这种设计模式,为什么我到现在都没提及过,那是因为我给出都是Spring AOP代理过程中最理想最简单的情况,自然不需要想尽办法利用多态的性质去简化代码。
还记得我前面留下的问题,为什么methodInterceptor就当它是Advice了。因为我给出的切面逻辑是单一的,真实的使用切面逻辑是三个,before,afterReturning,throws。所以Spring源码在执行目标方法之前是有一个拦截链的,里面有多个拦截器,这多个拦截器执行完成之后才会执行目标方法。
适配器是吧,直接上图!
Spring源码是想干啥呢?我解释一下,我现在手上有一堆从配置文件获得的Advice,但我想获得我想要的拦截器,因为拦截器才是真正改变目标方法的地方。但是有由于advice这个对象究竟是before,afterReturning还是throws不得而知,所以我们用适配器去适配它,目的是为了生成对应的不同的拦截器。adapters是一个List,里面分别装有MethodBeforeAdviceAdapter,AfterReturningAdviceAdapter,ThrowsAdviceAdapter。我们的做法就是遍历它们,然后去supportsAdvice()。下面我举一个AfterReturningAdvice例子,其他两个可类推。
这就是所谓的适配器设计模式。本质还是面向对象的多态,不同的玩法而已,而且instanceof不到万不得以的地步不要使用,因为这样会是代码的扩展性变得不好。至于简单工厂,单例模式这种老掉牙的设计模式就不必也来Spring中凑数了
好了,Spring AOP也落下帷幕了。我虽然还没工作,但是实习也大半年了,我自己的感觉真实生产中Spring IOC比AOP使用的要多一些,或者说AOP是衣架子,先人已经做的挺好的,我们只需要改一改换一换衣架上的衣服罢了。而且Spring AOP的使用意识其实比Spring AOP的原理在生产中来的更实际一些。
最后,分享一波阿里云的面试心得:我今年2月份的时候面试了下阿里云的实习生。其中有一个面试题就是针对我在上一家公司的业务,业务是这样的:镜像删除,既要将数据库中的镜像信息删除,还要将调用镜像仓库服务进程的API,实现镜像描述信息和实际的镜像文件都删除。我的回答也比较耿直,因为确实是这么做的,我说我先将数据库中的镜像信息删除,然后删除镜像文件,并且为了提升前端的响应速度,我采用异步的方式,创建子线程的方式去调镜像仓库的API。面试官问:那如果数据库出现异常会怎么样?我:数据会回滚,这是Spring的事务管理机制。面试官:好,那数据回滚了,镜像仓库的镜像没了,你怎么办?我:。。。
结果可想而知,我挂了。其实这是一个非常简单的问题,只怪当初我对AOP的理解和数据库事务的理解太过僵硬。首先我没有抓住事务的本质,为什么只有操作数据库才算是一个事务?事务的本质就是将一堆行为作为一个整体来看,要么一起成功要么一起失败。就像数据库删除镜像信息和删除镜像文件,这两样事情就是要么一起成功要么一起失败,所以应该将二者视为一个事务。
因此解决方案很简单,只要在这个业务函数的整体上加一个@Transaction即可,@Transaction就是一个AOP,在业务函数开始之前transaction.start()。然后try catch住整个方法,无论数据库还是调用镜像仓库API只要抛出异常执行transaction,rollback()。方法返回之前调用transaction.commit()。是不是就对应了before,afterReturning,throws啊。
Spring的实现是复杂的,如何讲清楚他,从构思到攥写都是相当耗时耗力的。我也总算告一段落了,希望这两篇博客可以给予JAVA开发的同学一点帮助。
后续的话我还会写一些Mysql,Hadoop,Kubernetes博客,希望技术不再是逼格的代表,而是艺术的化身!