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

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

接下来,直接进入正文。spring

为何要有aop

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

Aop是什么东西

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

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

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

Advice Advice行内不少人都定义成了通知,可是我总以为有点勉强。所谓的Advice其实就是定义了Aop什么时候被调用,确实有种通知的感受,什么时候调用其实也不过如下几种:函数

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

JoinPoint【链接点】 JoinPoint链接点,其实很好理解,上面又有通知、又有切点,那和具体业务的链接点又是什么呢?没错,其实就是对应业务的方法对象,由于咱们在横切代码中是有可能须要用到具体方法中的具体数据的,而链接点即可以作到这一点。源码分析

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

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

图片描述
图片描述
接下来该给出说了那么久的切片了
图片描述
能够从中看到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
相关文章
相关标签/搜索