学Aop?看这篇文章就够了!!!

2,252 阅读14分钟

在实际研发中,Spring是我们经常会使用的框架,毕竟它们太火了,也因此Spring相关的知识点也是面试必问点,今天我们就大话Aop。 特地在周末推文,因为该篇文章阅读起来还是比较轻松诙谐的,当然了,更主要的是周末的我也在充电学习,希望有追求的朋友们也尽量不要放过周末时间,适当充电,为了走上人生巅峰,迎娶白富美。【话说有没有白富美介绍(o≖◡≖)】

接下来,直接进入正文。

为什么要有aop

我们都知道Java是一种面向对象编程【也就是OOP】的语言,不得不说面向对象编程是一种及其优秀的设计,但是任何语言都无法十全十美,对于OOP语言来说,当需要为部分对象引入公共部分的时候,OOP就会引入大量的重复代码【这些代码我们可以称之为横切代码】。而这也是Aop出现的原因,没错,Aop就是被设计出来弥补OOP短板的。Aop便是将这些横切代码封装到一个可重用模块中,继而降低模块间的耦合度,这样也有利于后面维护。

Aop是什么东西

学过Spring的都知道,Spring内比较核心的功能便是Ioc和Aop,Ioc的主要作用是应用对象之间的解耦,而Aop则可以实现横切代码【如权限、日志等】与他们绑定的对象之间的解耦,举个浅显易懂的小栗子,在用户调用很多接口的地方,我们都需要做权限认证,判断用户是否有调用该接口的权限,如果每个接口都要自己去做类似的处理,未免有点sb了,也不够装x,因此Aop就可以派上用场了,将这些处理的代码放到切片中,定义一下切片、连接点和通知,刷刷刷跑起来就ojbk了。

想要了解Aop,就要先理解以下几个术语,如PointCut、Advice、JoinPoint。接下来尽量用白话文描述下。

PointCut【切点】 其实切点的概念很好理解,你想要去切某个东西之前总得先知道要在哪里切入是吧,切点格式如下:execution(* com.nuofankj.springdemo.aop.Service.(..)) 可以看出来,格式使用了正常表达式来定义那个范围内的类、那些接口会被当成切点,简单明了。

Advice Advice行内很多人都定义成了通知,但是我总觉得有点勉强。所谓的Advice其实就是定义了Aop何时被调用,确实有种通知的感觉,何时调用其实也不过以下几种:

  • Before 在方法被调用之前调用
  • After 在方法完成之后调用
  • After-returning 在方法成功执行之后调用
  • After-throwing 在方法抛出异常之后调用
  • Around 在被通知的方法调用之前和调用之后调用

JoinPoint【连接点】 JoinPoint连接点,其实很好理解,上面又有通知、又有切点,那和具体业务的连接点又是什么呢?没错,其实就是对应业务的方法对象,因为我们在横切代码中是有可能需要用到具体方法中的具体数据的,而连接点便可以做到这一点。

给出一个Aop在实际中的应用场景

先给出两个业务内的接口,一个是聊天,一个是购买东西

图片描述
图片描述
接下来该给出说了那么久的切片了
图片描述
可以从中看到PointCut【切点】是

execution(* com.nuofankj.springdemo.aop.Service.(..))

Advice是

Before

JoinPoint【连接点】是

MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod();

代码浅显易懂,其实就是将ChatService和BuyService里边给userId做权限校验的逻辑抽出来做成切片。

那么如何拿到具体业务方法内的具体参数呢? 这里是定义了一个新的注解

图片描述
作用可以直接看注释,使用地方如下
图片描述
可以看到对应接口使用了AuthPermission的注解,而取出的地方在于
图片描述
是的,这样便可以取出来对应的接口传递的userId具体是什么了,而校验逻辑可以自己处理。

送佛送到西,不对,撸码撸整套,接下来给出运行的主类

图片描述
可以看到,上面有一个接口传递的userId是1,另一个是123,而上面权限认证只有1才说通过,否则会抛出异常。

运行结果如下

图片描述
运行结果可想而知,1的通过验证,123的失败。

Spring Aop做了什么【开始源码跟踪阅读】

首先给出Main类

2

可以看到我这里用的是AnnotationConfigApplicationContext,解释下

AnnotationConfigApplicationContext是一个用来管理注解bean的容器,所以我可以用该容器取得我定义了@Service注解的类的实例。

打断点后,启动程序,我们可以看到TestDemo的实例在idea的表现是这样的

3

而BuyService的实例却不同

4

我们可以从看到BuyService是SpringCGLIB强化过的一个实例,那么问题来了

  • 为什么BuyService被强化过而TestDemo没有?
  • SpringCGLIB又是什么?
  • Spring是在什么时候生成一个强化后的实例的?

带着这些疑问,让我们一步步从Spring源码中找到答案。

为什么BuyService被强化过而TestDemo没有?

这个问题比较简单,我们可以看回上面我对切片的定义

5

可以从代码中看出,我定义的切点是*Service命名的类,而TestDemo很明显不符合这个设定,因此TestDemo逃过被强化的命运。

SpringCGLIB又是什么?

CGLIB其实就是一种实现动态代理的技术,利用了ASM开源包,先将代理对象类的class文件加载进来,之后通过修改其字节码并且生成子类。结合demo来解读便是SpringCGLIB会先将BuyService加载到内存中,之后通过修改字节码生成BuyService的子类,该子类便是强化后的BuyService,上文看到的强化后的实例便是该子类的实例。

Spring是在什么时候生成一个强化后的实例的?

这个便厉害了,首先,我们要先从Spring如何加载切片入手。

【思考Time】 为什么我会选择从切片入手呢?原因很简单,Spring就是因为发现了切片,并且对切片进行解析后才知道了要强化哪些类。

6

切片的处理第一步便是要加上@Aspect注解,学过注解的都知道,注解的作用更多的是标志识别,也就是告诉Spring这个类要做相关特殊处理,因此我们可以基于该认识,反调该注解使用的地方

7

可以从截图看出,我反调了@Aspect后定位到了AbstractAspectJAdvisorFactory类中的hasAspectAnnotation函数,并且携带参数clazz,因此我猜测该接口就是用来识别clazz是否使用了注解@Aspect的地方,于是我打上了断点,并且加了条件 clazz == AuthAspect.class ,重新启动后

8

我们看到确实被断点到了,可以得出我的猜测是对的。 我们先看下断点后做了什么事情,之后再看下具体是哪里进行了扫描。在断点处按F8继续往下走,最后发现

13

没错,可以看到最终是构建成了一个Advisor对象 ,并且放入了BeanFactoryAspectJAdvisorsBuilder中的advisorsCache中,这样意味着Spring最终会将使用了@Aspect注解的类构建成Advisor对象后保存进BeanFactoryAspectJAdvisorsBuilder.advisorsCache中。

接下来我们看看具体是哪里进行了使用@Aspect注解的相关类的扫描,这次我断点的地方在BeanFactoryAspectJAdvisorsBuilder中的advisorsCache调用了put的地方。

【思考Time】 为什么我会选择在advisorsCache调用了put的地方打断点呢?原因很简单,因为我们上面已经分析出@Aspect注解的类构建成Advisor对象后保存进BeanFactoryAspectJAdvisorsBuilder.advisorsCache中,而我通过反调知道put的地方只有一个,因此我可以断定在此处打断点可以知道到底哪里进行了扫描的操作。

14

通过打断点后我从idea的Frames面板中看到

19

没错,做了扫描@Aspect注解的扫描器是AbstractAutoProxyCreator类

11
12

我们可以从中看到AbstractAutoProxyCreator最终实现了InstantiationAwareBeanPostProcessor接口。

【思考Time】 这个接口有什么作用呢?具体可以看我前阵子写的一篇文章:mp.weixin.qq.com/s/r2OEqsap6…

现在已经找到了扫描注解的地方,并且我们也看到了最终是生成了Advisor对象 ,并且放入了BeanFactoryAspectJAdvisorsBuilder中的advisorsCache中,那么Spring是在什么时候生成强化后的实例的呢? 接下来我的切入点是AbstractAutoProxyCreator中的postProcessAfterInitialization接口。

【思考Time】 之所以会选择AbstractAutoProxyCreator为切入点,是因为通过命名可以看出这是SpringAop用来构建代理[强化]对象的地方,并且由于SpringCGLIB是先将目标类加载到内存中,之后通过修改字节码生成目标类的子类,因此我猜测强化是在目标类实例化后触发postProcessAfterInitialization的时候进行的。

因此我在postProcessAfterInitialization接口中做了断点,并且加了调试条件。

14

可以看到我这里断点到了ChatService这个类。

【思考Time】 为什么专门断点ChatService这个类?之所以会专门定位这个类,因为我的切面的目标类就包含了ChatService,通过定位到该类,我们可以一步步捕捉Spring的强化操作。

我们可以看到,生成强化后的对象就藏在wrapIfNecessary中。

【思考Time】 为什么我会知道是生成强化后的对象就藏在wrapIfNecessary中呢?因为我通过调试发现,在调用了wrapIfNecessary接口后,返回的对象是强化后的对象。

那么问题来了,为什么Spring会知道ChatService类需要进行进行强化呢?我们可以从wrapIfNecessary中走入更深一层,通过调试,可以看到

16

在此处会从advisorsCache中根据aspectName取出对应的Advisor。拿到Advisor后,便是进行过滤的地方了,通过F8往后走,可以看到过滤的地方在AopUtils.canApply接口中。

17

可以看到此处传进来的targetClass符合切面的要求,因此可以进行构建强化对象。 接下来让我们看下真正产生强化对象的地方了

18

我们可以看到在AbstractAutoProxyCreator的createProxy函数中看到,最后会构造出一个强化后的chatService。 那么createProxy又做了什么呢?通过断点一层层深入后,发现最后会到达

18

通过源码分析,我们发现在AbstractAutoProxyCreator构建强化对象的时候是调用了createAopProxy函数,重点来了,我们可以看到针对targetClass,也就是ChatService做了判断,如果targetClass有实现接口或者targetClass是Proxy的子类,那么使用的是JDK的动态代理实现AOP,如果不是才会使用CGLIB实现动态代理。

那么JDK实现的动态代理和CGLIB实现的动态代理有什么区别吗? 首先动态代理可以分为两种:JDK动态代理和CGLIB动态代理。从文中我们也可以看出,当目标类有接口的时候才会使用JDK动态代理,其实是因为JDK动态代理无法代理一个没有接口的类。JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,而CGLIB是针对类实现代理,主要是对指定的类生成一个子类,并且覆盖其中的方法。

Aop实现机制之代理模式

本来想一篇文章说完源码跟踪分析Aop和Aop的实现机制代理模式,发现源码跟踪分析已经很占篇幅了,因此没办法只能再开一篇文章专门阐述Aop的实现机制代理模式,期待下篇文章。

大家都知道,我有个习惯,在动手写一篇文章之前会先将该文章相关的资料仔细琢磨一遍,然后再结合源码再调试一遍,结果,说好的

看源码也确实是

源码确实有进行了是否是接口的判断,但是问题来了,我调试的时候发现无论代理类是否有接口,最终都会被强制使用CGLIB代理,没办法,只能翻看SpringBoot的相关文档,最终发现原来SpringBoot从2.0开始就默认使用Cglib代理了,好家伙,怪不得我调试半天找不到原因。

那么如何解决呢?肯定是通过配置啦,按照如下配置即可

在application.properties文件中配置 spring.aop.proxy-target-class=false

即可。

【划重点】 曾经遇见过面试官问,SpringBoot默认代理类型是什么?看完该篇文章,我们就可以果断的回答是Cglib代理了。通过调试代码发现的规则,我想我这辈子都不会忘记这个默认规则。

动态代理原理剖析

什么是代理

简单来说,就是在运行的时候为目标类动态生成代理类,而在操作的时候都是操作代理类,代理模式有个显而易见的好处,那便是可以在不改变对象方法的情况下对方法进行增强。试想下,我们在你必须要懂的Spring-Aop之应用篇有提到使用Aop来做权限认证,如果不用Aop,那么我们就必须要为所有需要权限认证的方法都加上权限认证代码,听起来就觉得蛋疼,你觉得对不对?

为什么不用静态代理

静态代理类不是说不可以用,如果只有一个类需要被代理,那么自然可以用,如 这是在你必须要懂的Spring-Aop之应用篇使用的一个例子类,该类的作用只是打印出我要买东西。

3

代理类如下

4

可以看到这个BuyProxy代理类只是塞了一个IBuyServcie接口进行,而且自身也实现了接口IBuyService,而在buyItem方法被调用的时候会先做自己的操作,再调用塞进去的接口的buyItem方法。 测试类很简单,如下

5

运行后很自然而然的打印出

6

静态代理就是简单,但是弊端也很明显,如果有多个类都需要同样的代理,都实现了同样的接口,那么如果使用静态代理的话,我们就要构造多个Proxy类,就会造成类爆炸。 而使用了Aop后,也就是动态代理后,便可以一次性解决该问题了,具体可以看你必须要懂的Spring-Aop之应用篇中的操作方法。

JDK动态代理原理

这里给出一个JDK动态代理的demo 首先给出一个简单的业务类,Hello类和接口

7

8

真正实现了类的代理功能的其实就是这个实现了接口InvocationHandler的JdkProxy类

9

我们可以看到其中必须实现的方法是invoke,可以看到invoke方法的参数带有Method对象,这个就是我们的目标Method,现在我们的目的就是要在这个Method在被调用前后实现我们的业务,可以看到在method.invoke反调前后实现了before和after业务。

这里再给出一个Main测试类,作用是取得Hello的代理类,然后调用其中的say方法。

10

运行结果如下

11

原理很简单 在JdkProxyMain中hello调用say的时候,由于Hello已经被“代理”了,所以在调用say函数的时候其实是调用JdkProxy类中的invoke函数,而在invoke函数中先是实现了before函数才实现Object result = method.invoke(target, args),这一句其实是调用say函数,而后才实现after函数,于是这样就可以不必在改动目标类的前提下实现代理了,并且不会像静态代理那样导致类爆炸。

CGLIB动态代理原理

先给出一个Cglib动态代理的demo

13

核心类是实现了MethodInterceptor的CGlibProxy类

14

可以看到其中实现了方法intercept,先是在目标函数被调用前实现自己的业务,比如before()和after(),之后再通过 proxy.invokeSuper(obj, args) 触发目标函数。

最后给出入口类

15

最后给出运行类,运行类如下

15

可以看到运行结果

16

原理很简单 在CglibProxyMain中hello调用say的时候,由于Hello已经被“代理”了,所以在调用say函数的时候其实是调用CGlibProxy类中的intercept函数。

logo