AOP的术语
-
通知(Advice)
在AOP术语中,切面的工作被称为通知。 通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题(方位信息)。它应该应用在某个方法被调用之 前?之后?之前和之后都调用?还是只在方法抛出异常时调用?
Spring切面可以应用5种类型的通知:- 前置通知(Before):在目标方法被调用之前调用通知功能;
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
- 返回通知(After-returning):在目标方法成功执行之后调用通知;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
-
连接点(join point)
连接点就是程序执行的某个特定的位置,这个点可以是类开始初始化前、类初始化后、类的某个方法调用前、类的某个方法调用后、方法抛出异常后、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。连接点不属于切面的内容。Spring 只支持类的方法前、后、抛出异常后的连接点。 -
切点
如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用 明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些AOP框架允许我们创建动态的切点,可以根据运 行时的决策(比如方法的参数值)来决定是否应用通知。 -
切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。 -
引入
引入允许我们向现有的类添加新方法或属性。例如,我们可以创建一个Auditable通知类,该类记录了对象最后一次修改时的状态。这很简 单,只需一个方法,setLastModified(Date),和一个实例变量来保存这个状态。然后,这个新方法和实例变量就可以被引入到现有的类 中,从而可以在无需修改这些现有的类的情况下,让它们具有新的行为和状态。 -
目标对象(Target)
目标对象就是我们需要对它进行增强的业务类~
如果没有AOP,那么该业务类就得自己实现需要的功能。 -
AOP代理(AOP proxy)
一个类被AOP织入后生成出了一个结果类,它是融合了原类和增强逻辑的代理类。 -
织入 ( Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增 强该目标类的字节码。AspectJ5的加载时织入(load-timeweaving,LTW)就支持以这种方式织入切面。
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。
小结: 通知包含了需要用于多个应用对象的横切行为;连接点是程 序执行过程中能够应用通知的所有点;切点定义了通知被应用的具体位置(在哪些连接点)。
spring对aop的支持
并不是所有的AOP框架都是相同的,它们在连接点模型上可能有强弱之分。它们织入切面的方式和时机也有所不同。但是无论如何,创建切点来定义切面所织入的连接点是AOP框架的基本功能。
Spring提供了4种类型的AOP支持:
- 基于代理的经典Spring AOP;
- 纯POJO切面;
- @AspectJ注解驱动的切面;
- 注入式AspectJ切面(适用于Spring各版本)。
对上面四种类型的说明:
1、Spring的经典AOP编程模型曾经它的确非常棒。但是现在Spring提供了更简洁和干净的面向切面编程方式。引入了简单的声明式AOP和基于注解的AOP之后,Spring 经典的AOP看起来就显得非常笨重和过于复杂,直接使用ProxyFactory Bean会让人感觉厌烦。
2、借助Spring的aop命名空间,我们可以将纯POJO转换为切面。实际上,这些POJO只是提供了满足切点条件时所要调用的方法。遗憾的是,这 种技术需要XML配置,但这的确是声明式地将对象转换为切面的简便方式。
3.Spring借鉴了AspectJ的切面,以提供注解驱动的AOP。本质上,它依然是Spring基于代理的AOP,但是编程模型几乎与编写成熟的AspectJ注解切面完全一致。这种AOP风格的好处在于能够不使用XML来完成功能。
4、如果你的AOP需求超过了简单的方法调用(如构造器或属性拦截),那么你需要考虑使用AspectJ来实现切面。在这种情况下,上文所示的第 四种类型能够帮助你将值注入到AspectJ驱动的切面中。
通过切点来选择连接点
1.在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。
2.Spring仅支持AspectJ切点指示器(pointcutdesignator)的一个子集(Spring是基于代理的,而某些切点表达式是与基于代理的AOP无关的)。
在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgument-Exception异常。
Spring AOP所支持的AspectJ切点指示器:
当我们查看如上所展示的这些Spring支持的指示器时,注意只有execution指示器是实际执行匹配的,而其他的指示器都是用来限制匹配 的。表明execution指示器是我们在编写切点定义时最主要使用的指示器。在此基础上,我们使用其他指示器来限制所匹配的切点。
编写切点
1)使用execution()指示器选择Performance的perform()方法。
2)方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。
3)然后,我们指定了全限定类名和方法名。
4)对于方法参数列表,我们使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的 入参是什么。
execution()可以和其他指示器一起使用:
请注意我们使用了“&&”操作符把execution()和within()指示器连接在一起形成与(and)关系(切点必须匹配所有的指示器)。类似地,我们可以使用“||”操作符来标识或(or)关系,而使用“!”操作符来标识非(not)操作。
因为“&”在XML中有特殊含义,所以在Spring的XML配置里面描述切点时,我们可以使用and来代替“&&”。同样,or和not可以分别用来代 替“||”和“!”。
编写切点
除了表4.1所列的指示器外,Spring还引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标识bean。bean()使用 bean ID或bean名称作为参数来限制切点只匹配特定的bean。
在这里,我们希望在执行Performance的perform()方法时应用通知,但限定bean的ID为woodstock。
还可以使用非操作为除了特定ID以外的其他bean应用通知:
使用注解创建切面
定义切面
Audience类:观看演出的切面
Audience类使用@AspectJ注解进行了标注。该注解表明Audience不仅仅是一个POJO,还是一个切面。Audience类中的方法都使用注 解来定义切面的具体行为。
AspectJ提供了五个注解来定义通知,如表4.2所示。
Audience类中的切点表达式重复了四遍,其实我们可以只定义这个切点一次,然后每次 需要的时候引用它。@Pointcut注解能够在一个@AspectJ切面内定义可重用的切点。
开启自动代理
如果不开启自动注解,一个类即使使用了Aspect注解也不是切面。
- 在javaConfig开启
- 在xml配置文件中开启
使用Spring aop命名空间中的aop:aspectj-autoproxy元素
创建环绕通知
环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通 知和后置通知。
1、@Around注解表明watchPerformance()方法会作为performance()切点的环绕通知
2、新的通知方法,你首先注意到的可能是它接受ProceedingJoinPoint作为参数。这个对象是必须要有的,因为你要在通知中通过 它来调用被通知的方法。 3、别忘记调用proceed()方法。如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。
4、也可以在通知中对proceed()方法进行多次调用。要这样做的 一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。
处理通知中的参数
1、切点表达式定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。
通过注解引入新功能
需要注意的是,当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。
借助于AOP的引入功能,我们可以不必在设计上妥协或者侵入性地改变现有的实现。为了实现该功能,我们要创建一个新的切面:
EncoreableIntroducer是一个切面。但是,它与我们之前所创建的切面不同,它并没有提供前置、后置或环绕通知,而是通过@DeclareParents注解,将Encoreable接口引入到Performance bean中。
@DeclareParents注解由三部分组成:
- value属性指定了哪种类型的bean要引入该接口。在本例中,也就是所有实现Performance的类型。(标记符后面的加号表示是Performance的所有子类型,而不是Performance本身。)
- defaultImpl属性指定了为引入功能提供实现的类。在这里,我们指定的是DefaultEncoreable提供实现。
- @DeclareParents注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是Encoreable接口。
和其他的切面一样,我们需要在Spring应用中将EncoreableIntroducer声明为一个bean:
Spring的自动代理机制将会获取到它的声明,当Spring发现一个bean使用了@Aspect注解时,Spring就会创建一个代理,然后将调用委托给被代理的bean或被引入的实现,这取决于调用的方法属于被代理的bean还是属于被引入的接口。
在XML中声明切面
如果你没有源码的话,或者不想将AspectJ注解放到你的代码之中,那么就必须转向XML配置了。
在Spring的aop命名空间中,提供了多个元素用来在XML中声明切面,如表4.3所示。
例如,我们重新看一下Audience类,这一次我们将它所有的AspectJ注解全部移除掉:
声明前置和后置通知
用Spring aop命名空间中的一些元素,将没有注解的Audience类转换为切面。
注意:
1、大多数的AOP配置元素必须在<aop:config元素的上下文内使用。这条规则有几种例 外场景,但是把bean声明为一个切面时,我们总是从<aop:config元素开始配置的。
2、在<aop:config元素内,我们可以声明一个或多个通知器、切面或者切点。在程序清单4.9中,我们使用<aop:aspect元素声明了一个 简单的切面。ref元素引用了一个POJObean,该bean实现了切面的功能——在这里就是audience。
3、该切面应用了四个不同的通知。
- 1)两个<aop:before元素定义了匹配切点的方法执行之前调用前置通知方法—也就是Audiencebean的takeSeats()和turnOffCellPhones()方法(由method属性所声明)
- 2)。<aop:after-returning元素定义了一个返回 (after-returning)通知,在切点所匹配的方法调用之后再调用applaud()方法。
- 3)同样,<aop:after-throwing元素定义了异常(after-throwing)通知,如果所匹配的方法执行时抛出任何的异常,都将会调用demandRefund()方法。
4、在所有的通知元素中,pointcut属性定义了通知所应用的切点,它的值是使用AspectJ切点表达式语法所定义的切点。
- 当多个通知元素的pointcut属性都一样时,可以使用<aop:pointcut抽取出来。
<aop:pointcut元素定义了一个id为performance的切点。同时修改所有 的通知元素,用pointcut-ref属性来引用这个命名切点。 - <aop:pointcut元素所定义的切点可以被同一个<aop:aspect元素之内的所有通知元素引用。如果想让定 义的切点能够在多个切面使用,我们可以把<aop:pointcut元素放在<aop:config元素的范围内。