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

在实际研发中,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】 这个接口有什么作用呢?具体可以看我前阵子写的一篇文章:https://mp.weixin.qq.com/s/r2OEqsap6NgaEnNveO1mVg

现在已经找到了扫描注解的地方,并且我们也看到了最终是生成了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的实现机制代理模式,期待下篇文章。

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

image

看源码也确实是

image

源码确实有进行了是否是接口的判断,但是问题来了,我调试的时候发现无论代理类是否有接口,最终都会被强制使用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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容