如何用最通俗的方法讲spring中的AOP
这篇文章给大家介绍如何用最通俗的方法讲spring中的AOP,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。
写这个系列的目的(可以跳过不看)
自己写这个系列的目的,是因为自己是个比较笨的人,我曾一度怀疑自己的智商不适合干编程这个行业.因为在我自学的过程中,看过无数别人的文章,博客,心得.可那一大堆的术语,技术基础,根本就难以阅读理解.再加上网上文章良莠不齐,很多文章都是写给已经懂的人看的. 一个例子: Q:什么是控制反转. A:将对在自身对象中的一个内置对象的控制反转,反转后不再由自己本身的对象进行控制这个内置对象的创建,而是由第三方系统去控制这个内置对象的创建. ??? 这种专业的解释,在我看来就不是给学习的人看的,而是给已经有相当经验的人读的. 难道不能用更加感性的解释这些东西吗? 编程语言作为一门语言,我认为一定程度上是相当感性的东西,既然我们互相说话时可以听懂,那么,这些技术就可以在一定程度上,完全可以用感性的方式解释.
AOP
AOP是面向切面. 得,说了白说. 什么是面向切面?这是个问题,但我们先不着急回答.
一. 举个例子
我们的代码结构,大多都是MVC模式.那我们用MVC举个简单的例子. MVC模式的话,最常用的spring + springMVC + Mybatis,大多数是下面的样子.
┌───────────────┐| view | 用户界面├───────────────┤| Controller | 控制层├───────────────┤| Service | 服务层,也叫业务层├───────────────┤| Dao | 持久层,也叫数据访问层├───────────────┤| Database | 数据库└───────────────┘
外加Model作为数据载体在各个层之间传来传去 只要是接触过JavaWeb开发的,上面都看的懂,如果这个不懂暂时还是不要深究这些知识,先以框架应用为主.
这个Service是个UserService,作用只有一个:保存用户.
┌────────────────────┐| view |├────────────────────┤| Controller |├────────────────────┤| UserService.add | ───> 新增用户├────────────────────┤| Dao |├────────────────────┤| Database |└────────────────────┘
现在,有需求要我们在Service中增加日志,开发这个功能的同事不在了,经理要求我们来做这个需求. 那么根据面向对象的理念. 调用service方法前要调用一个日志方法,调用完后要调用一个日志方法,那么代码中就要增加两行代码. 于是,代码成了这样:
┌────────────────────┐| view |├────────────────────┤| Controller |├────────────────────┤| log.info... | ───> 调用日志| UserService.add | ───> 新增用户| log.info... | ───> 调用日志├────────────────────┤| Dao |├────────────────────┤| Database |└────────────────────┘
这么写正确吗? 答案是当然正确,需求解决了,日志正常保存了,领导很高兴.
第二天,又有需求要控制service的事务,OK,然后代码变成了这样.
┌────────────────────┐| view |├────────────────────┤| Controller |├────────────────────┤| Transaction | ───> 事务管理| log.info... | ───> 调用日志| UserService.add | ───> 新增用户| log.info... | ───> 调用日志| Transaction | ───> 事务管理├────────────────────┤| Dao |├────────────────────┤| Database |└────────────────────┘
第三天,项目经理要求我们记录方法的运行所用时间,小意思,然后代码变成了这样
┌────────────────────┐| view |├────────────────────┤| Controller |├────────────────────┤| beginTime | ───> 开始时间| Transaction | ───> 事务管理| log.info... | ───> 调用日志| UserService.add | ───> 新增用户| log.info... | ───> 调用日志| Transaction | ───> 事务管理| endTime | ───> 结束时间├────────────────────┤| Dao |├────────────────────┤| Database |└────────────────────┘
第四天,客户要求我们实现新增用户时的权限校验,好说,代码变成了这样
┌────────────────────┐| view |├────────────────────┤| Controller |├────────────────────┤| permissions | ───> 校验权限| beginTime | ───> 开始时间| Transaction | ───> 事务管理| log.info... | ───> 调用日志| UserService.add | ───> 新增用户| log.info... | ───> 调用日志| Transaction | ───> 事务管理| endTime | ───> 结束时间├────────────────────┤| Dao |├────────────────────┤| Database |└────────────────────┘/*不知道你们怎么想,但我现在,仍然觉得这种写法挺好的.通俗易懂,可读性强到不知道哪里去了.当然有个前提,这个项目只有这一个方法.*/
第五天,开发UserService的同事回来一看,一脸懵逼,明明走的时候只有一行,现在却有一大堆不知道什么意思的代码存在. 得了,他在此基础上开发,增加各种方法,功能. 随着需求的变动,慢慢的,一百个service都变成了上面那个样子. 接着记录日志的功能有了变动,你发现会影响到调用的类,搜了一下整个项目,发现有特么几千个地方都调用了这个方法. 你心中一万头草泥马掠过,然后提出了辞职. 接着有个新人接手了这个项目,打开一看,这映入眼帘的一大坨屎一样的是特么什么东西?? 硬着头皮改了几版,每个版本都有BUG,领导一怒之下,项目作废了,重金找了几个人重新做了.
==那么问题来了== 现在的需求搞砸了可以辞职,项目烂了可以重做. 那世界上第一个遇到这个问题的人,是怎么解决的?技术就是这技术,重做不还是一样会遇到这些问题吗?那我还怎么拓展功能呢?难道所有项目最终都会走向这种绝境? 于是前人开始探索:
┌───────────────┐| Controller || ┼───────> 拓展方法写在这?├───────────────┤| Service |├───────────────┤| ┼───────> 拓展方法写在这?| Dao |└───────────────┘
这不都是一回事吗?根本不解决问题.
我等凡夫俗子解决不了,但世界上有的是自带外挂的人. 于是,有那么几个人就找到了解决办法! 既然我要拓展方法,就要在你的方法里写代码,那么我能不能在不改你代码的基础上来拓展呢?
==比如在这个地方!==
┌───────────────┐| Controller |├───────────────┼───────> 比如,写在这!| Service |└───────────────┘
what the hell ?????? 你是让我在一个莫须有的地方写代码?
当然不是,大神的意思是代码要这样写.
┌───────────────┐| Controller | ┌───────────────┐├───────────────┼──────┤ log.info... || Service | └───────────────┘└───────────────┘
但执行的时候,会是这样
┌───────────────┐| Controller |├───────────────┤| log.info... || Service |└───────────────┘
what the hell ??????????????????????????????? 这是为什么?
==答案很简单:== 首先你既然打算了解AOP,那你应该是有一些Java基础的. 我们都知道Java文件是没有用的,这就相当于一个txt文件,当程序真正运行的时候,是把Java文件通过编译器编译成class文件来运行的. 那么我们好像看到重点了,编译器! 既然最终运行的是class文件,而class文件又是编译器生成的,那我们是不是可以在编译器上动动手脚呢? 没错,想要达成上面的效果,就需要一个不同的编译器. 把日志这段代码编译到service中 当然了,这个编译器不需要我们开发,而是早有人搞定了.(若是我能开发,我早就去谷歌开发者大会上做演讲了= =) 这就是大名鼎鼎的 :==ajc编译器==(他属于AspectJ框架,可以单独使用此框架写个例子,可以加深理解)
1. 想象一下,有这么两坨东西┌────────────────────┐| Controller.java |├────────────────────┤ ┌─────────────────┐| Service.java | | log.info.java |└────────────────────┘ └─────────────────┘2. 然后开始编译 : 编译器开始把Service.java中的代码编织成一个class文件.3. ajc编译器开始编译 : ajc开始把log.info.java编织插入到service中.┌────────────────────┐| Controller.java | _________________├────────────────────┤ _/ log.info.java || Service.java | \ ________________|└────────────────────┘4. 就像上面,把log.info织入到这个这个方法中.5. 最终形成的calss文件,就是这样┌───────────────┐| Controller |├───────────────┤| log.info... || Service |└───────────────┘
成了,问题解决了.有了这个编译器,前面的问题有迎刃而解. ==ps : ajc编译器并不是唯一解决办法==
那么问题又又来了,==面向切面是什么?== 也许你已经有答案了.
┌───────────────┐| Controller | ┌──────────────────────────────────┐├───────────────┼──────> | 我们在这个角度写代码,就是面向切面. || Service | | |├───────────────┼──────> | 我们在这个角度写代码,就是面向切面. || Dao | └──────────────────────────────────┘└───────────────┘
我们站在那条线的角度写代码,就是面向切面. 那条线,是不是将原本的代码分成了两个部分呢? 更形象一点
┌───────────────┐ | Controller | └───────────────┘───────────────────────────────────── 这条线,像不像把整个代码切开的一条线呢?那么这条线,就叫切面───────────────────────────────────── 如 : 日志调用───────────────────────────────────── 如 : 调用时间───────────────────────────────────── 如 : 事务管理 ┌───────────────┐ | Service | ├───────────────┤ | Dao | └───────────────┘
上面的每一个如,就是一个切面类,也相当于一个切面,毕竟没有人规定我们必须只有一个切面不是?
面向切面是一种人们面对难题时的解决方案,他并不是一个新的语言或是新的技术. 他消除了面向对象的一系列编程陋习.或者说解决了面向对象的一些缺点.
到了这里,我相信有些人还是不太懂面向切面的意义或者作用是什么,没关系,不要气馁,你一定比我当时可聪明多了.请继续往下看.
这张图可能并不太能直观的表达面向切面意义有多么重大. ┌───────────────┐ | Controller | └───────────────┘ ───────────────────────── ┌───────────────┐ | Service | ├───────────────┤ | Dao | └───────────────┘ 那么,这张图呢? ┌────────────────┬────────────────┬────────────────┬────────────────┬────────────────┬────────────────┐ | Controller1 | Controller2 | Controller3 | Controller4 | Controller5 | Controller6 | └────────────────┴────────────────┴────────────────┴────────────────┴────────────────┴────────────────┘─────────────────────── 面向切面的方法,可以织入到所有的service中,而不需要改变service的代码 ──────────────────────────────────────────────── 权限校验 ────────────────────────────────────────────────────────────────────────────────────────────────────── 日志调用 ────────────────────────────────────────────────────────────────────────────────────────────────────── 调用时间 ────────────────────────────────────────────────────────────────────────────────────────────────────── 事务管理 ─────────────────────────────────────────────────────────────────────────────── ┌────────────────┬────────────────┬────────────────┬────────────────┬────────────────┬────────────────┐ | Service1 | Service2 | Service3 | Service4 | Service5 | Service6 | └────────────────┴────────────────┴────────────────┴────────────────┴────────────────┴────────────────┘─────────────────────── 面向切面的方法,可以织入到所有的Dao中,而不需要改变Dao的代码 ──────────────────────────────────────────────────────── 日志调用 ────────────────────────────────────────────────────────────────────────────────────────────────────── 调用时间 ─────────────────────────────────────────────────────────────────────────────── ┌────────────────┬────────────────┬────────────────┬────────────────┬────────────────┬────────────────┐ | Dao1 | Dao2 | Dao3 | Dao4 | Dao5 | Dao6 | └────────────────┴────────────────┴────────────────┴────────────────┴────────────────┴────────────────┘ 我只需要声明一个类,就可以该类中的方法插入到任意类的位置
有些人说,用代理模式就可以解决上面所说的编程时的一系列问题. 没错! 面向切面 ─── 是思想. 代理模式 ─── 是思想的实现.
二. 画个图片
上面画过了
三. 写个代码
只讲概念,后面讲例子
四. 说个人话
切面的几大概念
1.切面 (Aspect) :
拓展的类 概念 : Aspect 声明类似于Java中的类声明,在Aspect中会包含着一些切点以及相应的增强. 人话 : 创建一个类,这个类中的<增强>将插入到<目标对象>代码中.
2.连接点 (Joint point) :
调用点 概念 : 表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point. 人话 : 被<切点>所包含的内容, 如切点为 userService.add()方法 : 连接点就是userService.add() 如切点为 com.xxx包下的所有service : com.xxx包下的所有service的任意一个方法 如切点为 com.xxx包下的所有add开头的方法 : com.xxx包下的所有add开头的方法
3.切点 (Pointcut):
在哪里增强 概念 : 表示一组<连接点>,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的增强将要发生的地方.或是我们要织入的地方 人话 : 要往哪里插入 如 userService.add()方法 或 com.xxx包下的所有service方法 或 com.xxx包下的所有add开头的方法
4.增强 (Advice) :
拓展方法 概念 : Advice 定义了在<切点>里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个<连接点>之前、之后还是代替执行的代码. 人话 : 切面类中需要定义的,连接点方法调用前的拓展,之后的拓展,或者之前和之后的拓展.
5.目标对象 (Target) :
被增强的目标 概念 : 增强(Advice) 的目标对象.. 人话 : 被增强的目标
6.织入 (Weaving):
插入的动作 概念 : 将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程. 人话 : 将切面类插入进去
例如:我们要在service的add方法里里增加日志┌───────────────┐| Controller |└───────────────┘───────────────────────────────────── <-- com.xxx.service.UService ─── <切点>┌───────────────┐ ┌────────────┐ <-- log.info所在的类 ─── <切面>| UService.add | | log.info | <-- log.info方法就是 ─── <增强>├───────────────┤ └────────────┘| Dao |└───────────────┘UService.add的调用就是 ─── <连接点>UService ─── <目标对象>
到这里你应该理解什么是面向切面了,如果你还是不理解,不怕,可能是我的表达方式不适合你,全网有很多对于面向对象的优秀理解. 虽然筛选好文章需要时间,我可以帮你找出来,但其实在筛选的过程中,更是一个理解的过程.
关于如何用最通俗的方法讲spring中的AOP就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。