对于一个企业级应用来说,主要处理的是整个核心业务流程,这是我们主要关注的。但是同时一个应用也需要很多非核心但是必不可少的功能,例如应用的安全性,日志,可以说对于一个应用的多个功能来说都需要这些辅助性的功能。现在我们想要这些辅助功能,同时又不想在每一个业务核心功能中都加入一次这些功能,那么是否有什么好的办法?AOP就是答案。
1 整体理解
AOP是aspect-oriented programming的简称,即面向切面编程。首先不用管面向切面这个概念,如上所述,在项目中像日志、安全和事务管理这些功能很重要,但是我们希望它们做的是辅助其他应用的功能从而使其他应用专注于自己领域的业务问题 。日志、安全和事务管理这些辅助性的功能我们称为横切关注点,将这些横切关注点独立出来进行模块化从而成为特殊的类,这些类就叫切面,为什么叫切面呢,如下图所示,安全、事务和其他某些辅助性功能跨越了应用中的多个模块,就像是一个切面一样,而它们每一个功能就是切面中的一个点,因此叫做横切关注点。
这样做有多个好处:首先,每个关注点都几种于一处,而不是分布到多个应用的代码中去,提高了复用性,这也是除了继承和实现接口之外又能够实现重用功能的面向对象技术;其次,对应到各个业务模块更加简洁,应为它们只包含核心功能的代码,而次要关注点的代码都转移到切面中了。
2 AOP术语
描述一个切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point),结合下图理解这几个术语。
为了方便理解,可以通过一个生活中的例子来讲解这些术语。对于一个上班族来说,每天的工作是他主要关注的内容,而打卡的行为就是必须要做的但却又无关紧要的流程性内容(呵呵,迟到一次就知道是不是无关紧要了),这里可以把每天的工作看成是一个核心功能模块,而打卡看成是辅助性功能来理解AOP术语。
通知(Advice)
我们每天打卡是为了记录自己的工作时间,让自己一天的辛苦搬砖有所收获,同样的,切面的存在也是为了实现某个目标完成某些工作,而通知就描述了切面要干的工作。
通知定义了切面是什么以及何时使用它。通知做了两件事情:1、告诉我们切面要完成的工作;2、什么时候执行这个工作。是在某个方法被调用之前?还是之后?还是之前和之后都要通知到?还是在方法抛出异常的时候?
从此可以看出切面的通知不止一个,通过时间划分Spring的切面可以应用5中类型的通知:
- Before——在方法被调用之前调用通知通知。
- After——在方法完成之后调用通知,无路方法执行是否成功。
- After-returning——在方法成功执行之后调用通知。
- After-throwing——在方法抛出异常后调用通知。
- Around——通知调用时间跨越了方法,也就是被通知的方法应当在通知中间的某个时间段内,而在调用这个方法之前和之后的时间通知也都做了一些事情的。
连接点(Joinpoint)
我们每天打卡一般都是在早上和晚上,如果有某些意外情况发生呢?今天大姨夫来了睡过头了,那就只能中午打卡了;下午女朋友说感冒了,放下工作也得去给她倒杯热水,那只能下午打卡;劳资是霸道总裁,那就想多会打卡就多会打卡。所有可以说每天的各个时刻都可能会打卡,这些时刻就是连接点。
同样地,一个应用也需要对数以千记的时机应用通知,这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段的时候。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
切点(Pointcut)
对于我们大多数苦逼程序员来说,想什么时候打卡什么时候打卡,这是及其不现实的。想在哪打卡就在哪打卡,这更是不现实的,你还得乖乖走到前台小妹那里,听到滴的一声后请留言。类似地,切面虽然有很多连接点,但是可能我们不需要通知一个应用所有的连接点。而切点就指的是我们恰好要通知应用的那一点。
上文说了,通知定义了切面的“what”和“when”,那么切点就定义了“where”。切点定义会指明通知所要织入的一个或多个连接点。通常使用明确的类和方法名来指定切点,或是利用正则表达式匹配出来的类和方法来指定切点。还有些AOP框架允许创建动态切点,即根据运行时的情况(如方法的参数值)来决定是否应用通知。
切面(Aspect)
学习一个公式:切面=通知+切点。
通知和切点共同定义了关于切面的全部内容——它是什么,在何时和何处完成功能。
引入(Introduction)
当切面切入一个应用的时候,还可以向这个切入的类添加新方法或者属性而无需修改这个类,看起来貌似与OOP思想相悖,其实这只是切面进行了一些配置来使这个类具有了新的状态和行为而已。
织入(Weaving)
织入是将切面应用到目标对象来创建一个新的包含两者功能的代理对象的过程。在目标对象的生命周期里有多个点可以进行织入。
- 编译期——切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载期——切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),AspectJ 5 的 LTW(load-time weaving)就以这种方式织入。
- 运行期——切面在应用运行的某个时刻被织入。一般情况下,在织入切面时AOP容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面的。
3 Spring对AOP的支持
3.1 主流AOP框架
AOP框架在连接点模型上有强弱之分。前面讲连接点的时候说到过切面织入的时机和方式可能是修改方法时,也可能是修改字段时,也可能是在构造器方法时,根据不同的连接点织入方式有三种主流框架。
- AspectJ
- JBoss AOP
- Spring AOP
其中 Spring AOP 仅支持方法调用的连接点。
3.2 Spring AOP 的主要内容
Spring的AOP支持主要有以下四种:
- 基于代理的经典AOP
- @AspectJ注解驱动的切面
- 纯POJO切面
- 注入式AspectJ切面
前 3 种都是Spring基于代理的AOP变体,因此,Spring对AOP的支持局限于方法拦截。如果AOP需求超过了简单方法拦截的范畴(如构造器或属性拦截),那么应当考虑在AspectJ里实现切面,利用Spring的DI把bean注入到AspectJ切面中。
3.3 Spring在运行期通知对象
Spring的切面织入的方式是在运行期才通知目标对象,即在运行期才将切面织入到Spring管理的bean中,此时Spring会利用动态代理原理创建一个代理对象。如下图所示,切面织入后产生的代理类封装了目标类,通知了某个方法要调用它的时候,这个代理会首先拦截到这个调用,然后将调用转发给真正的目标bean。在调用目标bean方法之前,代理会执行切面逻辑。
(这部分内容无法理解的话可以先看一下Java的代理机制。)