spirng实战(四:面向切面的 Spring)(未改)

    依赖注入(DI)管理和配置我们的应用对象。DI有助于应用对象之间的解耦,而AOP可以实现横切关注点与它们所影响的对象之间的解耦。

    切面提供了取代继承和委托的另一种可选方案,而且在很多场景下更清晰简洁。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了.

定义AOP术语

    描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point)。

1.通知(Advice)

在AOP术语中,切面的工作被称为通知。

Spring切面可以应用5种类型的通知:

    1.前置通知(Before):在目标方法被调用之前调用通知功能;
    2.后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
    3.返回通知(After-returning):在目标方法成功执行之后调用通知;
    4.异常通知(After-throwing):在目标方法抛出异常后调用通知;

    5.环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

2.连接点(Join point)

    我们的应用可能有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

3.切点(Poincut)

    一个切面并不需要通知应用的所有连接点。切点有助于缩小切面所通知的连接点的范围。如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些AOP框架允许我们创建动态的切点,可以根据运行时的决策(比如方法的参数值)来决定是否应用通知。

4.切面(Aspect)

    切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。

5.引入(Introduction)

    引入允许我们向现有的类添加新方法或属性。例如,我们可以创建一个Auditable通知类,该类记录了对象最后一次修改时的状态。只需一个方法,setLastModified(Date),和一个实例变量来保存这个状态。然后,这个新方法和实例变量就可以被引入到现有的类中,从而可以在无需修改这些现有的类的情况下,让它们具有新的行为和状态。

6.织入(Weaving)

    织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:

    1.编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
    2.类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入(load-timeweaving,LTW)就支持以这种方式织入切面。
    3.运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。

Spring对AOP的支持

Spring提供了4种类型的AOP支持:
    1.基于代理的经典Spring AOP;
    2.纯POJO切面;
    3.@AspectJ注解驱动的切面;
    4.注入式AspectJ切面(适用于Spring各版本)。

    前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此Spring对AOP的支持局限于方法拦截。

    借助Spring的aop命名空间,我们可以将纯POJO转换为切面。这些POJO只是提供了满足切点条件时所要调用的方法。但是这种技术需要XML配置,不过这的确是声明式地将对象转换为切面的简便方式。

    Spring借鉴了AspectJ的切面,以提供注解驱动的AOP。本质上,它依然是Spring基于代理的AOP,但是编程模型几乎与编写成熟的AspectJ注解切面完全一致。这种AOP风格的好处在于能够不使用XML来完成功能。

    如果你的AOP需求超过了简单的方法调用(如构造器或属性拦截),那么你需要考虑使用AspectJ来实现切面。在这种情况下,上文所示的第四种类型能够帮助你将值注入到AspectJ驱动的切面中。

    Spring在运行时通知对象:通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。



    直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext
从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。
    通过使用各种AOP方案可以支持多种连接点模型。因为Spring基于动态代理,所以Spring只支持方法连接点。AspectJ和JBoss,除了方法切点,它们还提供了字段和构造器接入点。Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,我们就无法在bean创建时应用通知。方法拦截可以满足绝大部分的需求。如果需要方法拦截之外的连接点拦截功能,那么我们可以利用Aspect来补充Spring AOP的功能。

1.通过切点来选择连接点

    在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。关于Spring AOP的AspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器(pointcut designator)的一个子集。Spring是基于代理的,而某些切点表达式是与基于代理的AOP无关的。

    在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgument-Exception异常。只有execution指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的。这说明execution指示器是我们在编写切点定义时最主要使用的指示器。而其他的指示器用来限制匹配的连接。

1.编写切点

假设有个接口:

使用AspectJ切点表达式来选择Performance的perform()方法

    execution()指示器选择Performance的perform()方法。方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。然后指定了全限定类名和方法名。对于方法参数列表,我们使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么。

假设我们需要配置的切点仅匹配concert包。表达式则可以改成

(使用within()指示器限制切点范围)

    “&&”操作符把execution()和within()指示器连接在一起形成与(and)关系(切点必须匹配所有的指示器)。类似地,我们可以使用“||”操作符来标识或(or)关系,而使用“!”操作符来标识非(not)操作。因为“&”在XML中有特殊含义,所以在Spring的XML配置里面描述切点时,我们可以使用and来代替“&&”。同样,or和not可以分别用来代替“||”和“!”。

2.在切点中选择bean

    Spring中引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标识bean。bean()使用bean ID或bean名称作为参数来限制切点只匹配特定的bean。

例如:

在执行Performance的perform()方法时应用通知,但限定bean的ID为woodstock。

还可以使用非操作为除了特定ID以外的其他bean应用通知:

切面的通知会被编织到所有ID不为woodstock的bean中。

2.使用注解创建切面

    使用注解来创建切面是AspectJ 5所引入的关键特性。AspectJ 5之前,编写AspectJ切面需要学习一种Java语言的扩展,但是AspectJ面向注解的模型可以非常简便地通过少量注解把任意类转变为切面。

1.定义切面

Audience类使用@AspectJ注解进行了标注。该注解表明Audience不仅仅是一个POJO,还是一个切面。Audience类中的方法都使用注解来定义切面的具体行为。AspectJ提供了五个注解来定义通知。


@Pointcut注解能够在一个@AspectJ切面内定义可重用的切点。

    在此处的Audience中,performance()方法使用了@Pointcut注解。为@Pointcut注解设置的值是一个切点表达式,就像之前在通知注解上所设置的那样。通过在performance()方法上添@Pointcut注解,我们实际上扩展了切点表达式语言,这样就可以在任何的切点表达式中使用performance()了,如果不这样做的话,你需要在这些地方使用那个更长的切点表达式。我们现在把所有通知注解中的长表达式都替换成了performance()。

    performance()方法的实际内容并不重要,在这里它实际上应该是空的。其实该方法本身只是一个标识,供@Pointcut注解依附。除了注解和没有实际操作的performance()方法,Audience类依然是一个POJO。我们能够像使用其他的Java类那样调用它的方法,它的方法也能够独立地进行单元测试,这与其他的Java类并没有什么区别。Audience只是一个Java类,只不过它通过注解表明会作为切面使用而已。

它也可以装配为Spring中的bean:

    但是如果只是这样它不会被视为切面(即使用了AspectJ注解),它只是一个Spring中的一个bean,它的注解不会被解析,也不会创建将其转换为切面的代理。

如果使用JavaConfig的话,可以在配置类的类级别上通过使用EnableAspectJ-AutoProxy注解启用自动代理功能。

假如在Spring中要使用XML来装配bean的话,那么需要使用Springaop命名空间中的<aop:aspectj-autoproxy>元素。

    不管使用的是JavaConfig还是XML,AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。

    Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是Spring基于代理的切面。这意味着尽管使用的是@AspectJ注解,但我们仍然限于代理方法的调用。如果想利用AspectJ的所有能力,我们必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面。

2.创建环绕通知

环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。


@Around注解表明watchPerformance()方法会作为performance()切点的环绕通知。
    这个新的通知方法,首先注意到的是它接受ProceedingJoinPoint作为参数。这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用ProceedingJoinPoint的proceed()方法。
    如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。可以不调用proceed()方法,从而阻塞对被通知方法的访问,与之类似,也可以在通知中对它进行多次调用。要这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。

3.处理通知中的参数

举个例子:

这个切面使用@Pointcut注解定义命名的切点,并使用@Before将一个方法声明为前置通知。


    切点表达式中的args(trackNumber)限定符。它表明传递给playTrack()方法的int类型参数也会传递到通知中去。参数的名称trackNumber也与切点方法签名中的参数相匹配。

    这个参数会传递到通知方法中,这个通知方法是通过@Before注解和命名切点trackPlayed(trackNumber)定义的。切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。

配置JavaConfig类

测试类如下:



4.通过注解引入新功能

当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。实际上,一个bean的实现被拆分到了多个类中。

将接口引入某一个bean中可以通过@DeclareParents注解


@DeclareParents注解由三部分组成:
1.value属性指定了哪种类型的bean要引入该接口。在本例中,也就是所有实现Performance的类型。(标记符后面的加号表示是Performance的所有子类型,而不是Performance本身。)
2.defaultImpl属性指定了为引入功能提供实现的类。在这里,我们指定的是DefaultEncoreable提供实现。
3.@DeclareParents注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是Encoreable接口。

和其他的切面一样,我们需要在Spring应用中将EncoreableIntroducer声明为一个bean:

    Spring的自动代理机制将会获取到它的声明,当Spring发现一个bean使用了@Aspect注解时,Spring就会创建一个代理,然后将调用委托给被代理的bean或被引入的实现,这取决于调用的方法属于被代理的bean还是属于被引入的接口。

    在Spring中,注解和自动代理提供了一种很便利的方式来创建切面。它非常简单,并且只涉及到最少的Spring配置。但是,面向注解的切面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到这一点,必须要有源码。

3.在XML中声明切面

    基于注解的配置要优于基于Java的配置,基于Java的配置要优于基于XML的配置。但是,如果你需要声明切面,但是又不能为通知类添加注解的时候,那么就必须使用XML配置了。

    Spring的aop命名空间中,提供了多个元素用来在XML中声明切面:


    <aop:aspectj-autoproxy>元素,它能够自动代理AspectJ注解的通知类。aop命名空间的其他元素能够让我们直接在Spring配置中声明切面,而不需要使用注解。

    将上面的代码去掉注解后:

1.声明前置和后置通知


   我们使用<aop:aspect>元素声明了一个简单的切面。ref元素引用了一个POJO bean,该bean实现了切面的功能——在这里就是audience。ref元素所引用的bean提供了在切面中通知所调用的方法。该切面应用了四个不同的通知。两个<aop:before>元素定义了匹配切点的方法执行之前调用前置通知方法—也就是Audience bean的takeSeats()和turnOffCellPhones()方法(由method属性所声明)。<aop:after-returning>元素定义了一个返回(after-returning)通知,在切点所匹配的方法调用之后再调用applaud()方法。同样,<aop:after-throwing>元素定义了异常(after-throwing)通知,如果所匹配的方法执行时抛出任何的异常,都将会调用demandRefund()方法。    

    Spring AOP配置元素,第一个需要注意的事项是大多数的AOP配置元素必须在<aop:config>元素的上下文内使用。这条规则有几种例外场景,但是把bean声明为一个切面时,我们总是从<aop:config>元素开始配置的。在<aop:config>元素内,我们可以声明一个或多个通知器、切面或者切点。


    在所有的通知元素中,pointcut属性定义了通知所应用的切点,它的值是使用AspectJ切点表达式语法所定义的切点。在基于AspectJ注解的通知中,当发现这种类型的重复时,我们使用@Pointcut注解消除了这些重复的内容。而在基于XML的切面声明中,我们需要使用<aop:pointcut>元素。如:


    <aop:pointcut>元素定义了一个id为performance的切点。同时修改所有的通知元素,用pointcut-ref属性来引用这个命名切点。如果想让定义的切点能够在多个切面使用,我们可以把<aop:pointcut>元素放在<aop:config>元素的范围内。

2.声明环绕通知

    使用环绕通知,我们可以完成前置通知和后置通知所实现的相同功能,而且只需要在一个方法中实现。因为整个通知逻辑是在一个方法内实现的,所以不需要使用成员变量保存状态。

在XML中使用<aop:around>元素声明环绕通知

<aop:around>指定了一个切点和一个通知方法的名字。在这里,我们使用跟之前一样的切点,但是为该切点所设置的method属性值为watchPerformance()方法。

3.为通知传递参数

    切点表达式中包含了一个参数,这个参数会传递到通知方法中。如果你将这个表达式与上文中的参数传递的表达式进行对比会发现它们几乎是相同的。唯一的差别在于这里使用and关键字而不是“&&”(因为在XML中,“&”符号会被解析为实体的开始)。

3.通过切面引入新功能

    使用Spring aop命名空间中的<aop:declare-parents>元素,可以为被通知的方法神奇地引入新的方法。

    <aop:declare-parents>声明了此切面所通知的bean要在它的对象层次结构中拥有新的父类型。具体到本例中,类型匹配Performance接口(由types-matching属性指定)的那些bean在父类结构中会增加Encoreable接口(由implement-interface属性指定)。最后要解决的问题是Encoreable接口中的方法实现要来自于何处。这里有两种方式标识所引入接口的实现。在本例中,我们使用default-impl属性用全限定类名来显式指定Encoreable的实现。或者,我们还可以使用delegate-ref属性来标识。

    delegate-ref属性引用了一个Spring bean作为引入的委托。这需要在Spring上下文中存在一个ID为encoreableDelegate的bean。

    使用default-impl来直接标识委托和间接使用delegate-ref的区别在于后者是Spring bean,它本身可以被注入、通知或使用其他的Spring配置。

4.小结

    通过AspectJ,我们现在可以把之前分散在应用各处的行为放入可重用的模块中。我们显示地声明在何处如何应用该行为。这有效减少了代码冗余,并让我们的类关注自身的主要功能。Spring提供了一个AOP框架,让我们把切面插入到方法执行的周围。比如如何把通知织入前置、后置和环绕方法的调用中,以及为处理异常增加自定义的行为。

    通过使用@AspectJ注解和简化的配置命名空间,在Spring中装配通知和切点变得非常简单。最后,当Spring AOP不能满足需求时,我们必须转向更为强大的AspectJ。



参考:《Spring实战》


猜你喜欢

转载自blog.csdn.net/qq_37598011/article/details/80658375