Spring AOP

字数 2849阅读 582

定义

官方解释

AOP,Aspect Oriented Programming的缩写,意为面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。

AOP通俗理解:

一个组件A,不关心其它常用的服务组件B,但是A要使用组件B的时候,不是组件A自己去调用,而是通过配置等其他方式,比如Spring中的xml配置文件,这样A不需要知道组件B的任何事情,A只关心自己的业务逻辑,具体在A使用B的过程中,配置文件去做,与具体的A组件无关。

例子

要理解AOP(面向切面编程),首先我们要明白什么叫做切面。
  举个生活中的例子,我们都知道一个成功的老板背后一定有一个优秀的秘书,而秘书的职责就是负责调度和安排老板的日程。想象一下这样一个场景,今天秘书小M将事先安排好的一天日程向老板大B汇报后,老板大B表示同意。那么如无意外,老板大B今天将和往常一样,按照日程表一样一样完成,一天就过去了。但是,秘书小M突然接到通知,一个十分重要的会议临时决定在午后召开,无奈秘书小M只能将该会议强行插入老板大B已经确认过的日程安排中,然后默默等待老板大B的白眼。

image.png

我们注意到秘书小M将临时会议插入日程的动作,是不是很符合切面这个感觉呢?更形象一点,我们将老板大B确认过的日程想象成一条长面包,而秘书小M突然被告知还有一片火腿(即那个讨厌的临时会议),为了老板能够吃好,不得已只能用小刀将面包切开后硬生生将火腿塞入。
  可以这样理解,面向切面就是为了达到目的,动态地将某件事切入到某件事中

程序中的AOP

更容易理解的定义:运行时,动态地将代码切入到类的指定方法或者指定位置
此处介绍几个概念:
  通知(advice):切入到类指定方法或者指定位置的代码片段,即需要增加的功能代码,也就是上述的那片火腿(临时会议)。
  连接点(join point):程序运行过程中能够进行插入切面操作的时间点。例如方法调用、异常抛出或字段修改等,可以理解为上述的长面包(老板的整个日程安排)。
  切入点(pointcut):描述一个通知将被切入的一系列连接点的集合,即代码片段具体切入到哪些类、哪些方法,也就是上述面包切口处(特指午后的日程)。所以说,切入点规定了哪些连接点可以执行哪些通知。
  切面(aspect):AOP中的切面等同于OOP中的类(class),由通知(advice)和切入点(pointcut)组成,其中通知(advice)和切入点(pointcut)既可以是1对1的关系,也可以是1对多的关系。概括的说就是描述了何时何地干何事的基本单元,其中通知(advice)说明了切面干何事,而切入点则说明了切面何时何地切入。
织入(Weaving),AOP术语。把切面(aspect)连接到其它的应用程序类型或者对象上,并创建一个被通知(advised)的对象,这样的行为叫做织入。

关于几者的关系,我们可以这样理解,通知是在连接点上执行的,但是我们不希望通知应用到所有的连接点,所以引入了切入点来匹配特定的连接点,指名我们所希望通知应用的连接点。
  因此,所谓的AOP(面向切面编程)就是在程序运行过程中的某个时机将代码片段插入到某些类的指定方法和指定位置;换句话说,秘书接到临时会议通知时,将临时会议插入到老板的日程安排中去。

为什么要使用AOP

程序的最终目的就是实现业务,但是我们在进行编程的过程中经常会发现除了所谓的业务代码,还存在数量相当的公共代码,类似日志、安全验证、事物、异常处理等问题。这部分代码重要但是与我们编写程序要实现的功能没有关系,具有功能相似、重用性高、使用场景分散等特点。我们姑且称它们为共性问题
  对大多数程序而言,代码都是以纵向结构将各个业务模块串联从而完成功能的。我们提到的共性问题本身不属于业务范围,但是又散落在各个业务模块间,同实现主功能的代码相互杂糅在一起,即如下图所示:

image.png

试想一下,如果将共性问题部分的代码融入业务代码中,一旦涉及到对某个共性问题部分的代码进行更改的时候,例如日志部分发生需求变更,我们可能需要牵涉许许多多其他模块代码。这在小规模程序中也许是可以接受的,可能只修改1、2处;但是如果牵涉的地方数量过多,特别是应用在中大型规模程序中,我们甚至会为了小小的一个功能,修改上千、上万处。这样的方式是十分糟糕的,不仅费时费力,可能还会引起一些不必要的麻烦(回归错误、结构混乱等等)。
  AOP的就是为了解决这类共性问题,将散落在程序中的公共部分提取出来,以切面的形式切入业务逻辑中,使程序员只专注于业务的开发,从事务提交等与业务无关的问题中解脱出来

AOP的优点

解耦:AOP将程序中的共性问题进行了剥离,毫无疑问地降低了各个业务模块和共性问题之间的耦合。
重用性:共性问题散落于业务逻辑的各处,十分难维护,使用AOP进行提取后,能够将相似功能的共性问题收敛,减少重复代码,提高了代码的重用性。
拓展性:对于一个程序而言,迭代的重心一定在于业务和功能上。AOP使得每当发生变更时,可以只关注业务逻辑相关的代码,而减少共性问题上带来的变化,大大降低了程序未来拓展的成本。

Spring AOP的实现

通过上述介绍,我们了解到AOP的重心在于定义基本结构和完成切面切入两部分,解决了它们,我们就能方便快捷的使用AOP思想进行编程了。
  Spring AOP就是负责完成AOP相关工作的框架,它将切面所定义的横切逻辑切入到切面所指定的连接点中。框架的目的就是将复杂的事情变得简单易用,所以其主要工作分成了如下两点:

1.提供相应的数据结构来定义AOP所需基本结构,例如通知、连接点、切入点、切面等。
  2.封装切面切入的相关工作,提供相应接口给用户。封装的内容主要包括如何通过切面(切入点和通知)定位到特定的连接点;如何将切面中的功能代码植入到特定的连接点中等等;提供相应接口主要包括配置文件、注解等用户能够使用的工具。
  这里想提一下,在 Spring AOP 中,连接点(join point)总是方法的执行点, 即只有方法连接点。所以我们可以认为,在 Spring 中所有的方法都可以是连接点。

Spring AOP的实现举例

由于Spring对AOP的封装,使得我们可以十分方便的使用,我们只需要定义切面,即定义Advice通知和Pointcut切入点

(1)Spring AOP中Pointcut切入点
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?) 

其中,除ret-type-pattern和name-pattern之外,其他都是可选的,各部分具体含义如下:
  1.modifiers-pattern:方法的操作权限
  2.ret-type-pattern:返回值
  3.declaring-type-pattern:方法所在的包
  4.name-pattern:方法名
  5.parm-pattern:参数名
  6.throws-pattern:异常
  为了更加理解,举一个简单的execution示例,如下所示:

excution示例.png
(2)Spring AOP中Advice通知

before advice:前置通知,由@Before注解定义,表示在 join point 前被执行的advice。(虽然before advice是在 join point 前被执行,但是它并不能够阻止 join point 的执行, 除非发生了异常,即在before advice代码中, 不能人为地决定是否继续执行join point中的代码)
after return advice:后置通知,由@AfterReturning注解定义,表示在一个 join point 正常返回后执行的advice。
after throwing advice:异常通知,由@AfterThrowing注解定义,表示当一个 join point 抛出异常后执行的advice。
after(final) advice:最终通知,由@After注解定义,表示无论一个join point是正常退出还是发生了异常,都会被执行的advice。
around advice:环绕通知,由@Around注解定义,表示在join point 前和joint point退出后都执行的 advice,是最常用的advice。

AOP的实现

AOP的实现是基于代理机制的,根据不同实现方式主要分为两类:
  1.静态代理,AOP框架会在编译阶段生成AOP代理类,即在编译器和类装载期实现切入的工作,但是这种方式需要特殊的Java编译器和类装载器。AspectJ框架就是采用这种方式实现AOP。
  2.动态代理AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的连接点(切入点)做了添加通知(advice)处理,并回调原对象的方法
  与AspectJ的静态代理不同,Spring AOP使用动态代理,通过JDK Proxy和CGLIB Proxy两种方法实现代理。两种方式的选择与目标对象有关:

  • 如果目标对象没有实现任何接口,那么Spring将使用CGLIB来实现代理。CGLIB是一个开源项目,它是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。
  • 如果目标对象实现了一个以上的接口,那么Spring将使用JDK Proxy来实现代理,因为Spring默认使用的就是JDK Proxy,并且JDK Proxy是基于接口的。这也是Spring提倡的面向接口编程。当然,你也可以强制使用CGLIB来进行代理,但是这样可能会造成性能上的下降。

参考:https://www.jianshu.com/p/da55782965d9

推荐阅读更多精彩内容