第四章 面向切面的Spring

来源:互联网 发布:仿真画笔手绘软件 编辑:程序博客网 时间:2024/05/18 00:13

在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。


4.1 什么是面向切面编程



图4.1展现了一个被划分为模块的典型应用。每个模块的核心功能都是为特定业务领域提供服务,但是这些模块都需要类似的辅助功能,例如安全和事务管理。
如果要重用通用功能的话,最常见的面向对象技术是继承(inheritance)或委托(delegation)。但是,如果在整个应用中都使用相同的基类,继承往往会导致一个脆弱的对象体系;而使用委托可能需要对委托对象进行复杂的调用。

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


4.1.1 定义AOP术语


通常术语有:

  • 通知(advice)
  • 切点(pointcut)
  • 连接点(join point)

通知(Advice)

通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?

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

  • 前置通知(Before):在目标方法被调用之前调用通知功能
  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
  • 返回通知(After-returning):在目标方法成功执行之后调用通知
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

连接点(Join point)

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

切点(Pointcut)

切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些AOP框架允许我们创建动态的切点,可以根据运行时的决策(比如方法的参数值)来决定是否应用通知。

切面(Aspect)

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

引入(Introduction)

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

织入(Weaving)

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

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

4.1.2 Spring对AOP的支持


并不是所有的AOP框架都是相同的,它们在连接点模型上可能有强弱之分。有些允许在字段修饰符级别应用通知,而另一些只支持与方法调用相关的连接点。它们织入切面的方式和时机也有所不同。但是无论如何,创建切点来定义切面所织入的连接点是AOP框架的基本功能。

Spring提供了4种类型的AOP支持:

  • 基于代理的经典Spring AOP
  • 纯POJO切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面(适用于Spring各版本)

Spring 通知是Java编写的

Spring所创建的通知都是用标准的Java类编写的。这样的话,我们就可以使用与普通Java开发一样的集成开发环境(IDE)来开发切面。而且,定义通知所应用的切点通常会使用注解或在Spring配置文件里采用XML来编写,这两种语法对于Java开发者来说都是相当熟悉的。

Spring 在运行时通知对象

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

直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。

Spring 只支持方法级别的连接点

因为Spring基于动态代理,所以Spring只支持方法连接点。这与一些其他的AOP框架是不同的,例如AspectJ和JBoss,除了方法切点,它们还提供了字段和构造器接入点。Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,我们就无法在bean创建时应用通知。

但是方法拦截可以满足绝大部分的需求。如果需要方法拦截之外的连接点拦截功能,那么我们可以利用Aspect来补充Spring AOP的功能。


4.2 通过切点来选择连接点


在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。如果你已经很熟悉AspectJ,那么在Spring中定义切点就感觉非常自然。但是如果你一点都不了解AspectJ的话,本小节我们将快速介绍一下如何编写AspectJ风格的切点。如果你想进一步了解AspectJ和AspectJ切点表达式语言,我强烈推荐Ramniva Laddad编写的《AspectJ in Action》第二版(Manning,2009,www.manning.com/laddad2/)。
关于Spring AOP的AspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器(pointcut designator)的一个子集。让我们回顾下,Spring是基于代理的,而某些切点表达式是与基于代理的AOP无关的。

表4.1 Spring AOP所支持的AspectJ切点指示器:

AspectJ指示器 描述 arg() 限制连接点匹配参数为指定类型的执行方法 @args() 限制连接点匹配参数由指定注解标注的执行方法 execution() 用于匹配是连接点的执行方法 this() 限制连接点匹配AOP代理的bean引用为指定类型的类 target 限制连接点匹配目标对象为指定类型的类 @target() 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 within() 限制连接点匹配指定的类型 @within() 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里) @annotation 限定匹配带有指定注解的连接点

在Spring中尝试使用AspectJ其他指示器时,会抛出IllegalArgumentException


4.2.1 编写切点


我们定义一个接口:

public interface Performance {    public void perform();}

假设我们想编写Performance的perform()方法触发的通知:

我们使用execution()指示器选择Performance的perform()方法。方法表达式以*号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。对于方法参数列表,我们使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么。
现在假设我们需要配置的切点仅匹配concert包。在此场景下,可以使用within()指示器来限制匹配,如图4.5所示。

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


4.2.2 在切点中选择bean


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

例如:

execution(* concert.Performance.perform()) and bean('woodstock')

在这里,我们希望在执行Performance的perform()方法时应用通知,但限定bean的ID为woodstock

或者指定除了特定bean以为的其他bean:

execution(* concert.Performance.perform()) and !bean('woodstock')

4.3 使用注解创建切面


4.3.1 定义切面


我们继续使用Performance,新建一个Audience。从演出的角度来看,观众是非常重要的,但是对演出本身的功能来讲,它并不是核心,这是一个单独的关注点。因此,将观众定义为一个切面,并将其应用到演出上就是较为明智的做法。

package concert;import org.aspectj.lang.annotation.AfterReturning;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;@Aspectpublic class Audience {    @Before("execution(** concert.Performance.perform(..))")    public void silenceCellPhones() {        System.out.println("Silencing cell phones");    }    @Before("execution(** concert.Performance.perform(..))")    public void takeSeats() {        System.out.println("Taking seats");    }    @AfterReturning("execution(** concert.Performance.perform(..))")    public void applause() {        System.out.println("CLAP CLAP CLAP!!!");    }    @AfterReturning("execution(** concert.Performance.perform(..))")    public void demandRefund() {        System.out.println("Demanding a refund");    }}

Audience类使用@AspectJ注解进行了标注。该注解表明Audience不仅仅是一个POJO,还是一个切面。Audience类中的方法都使用注解来定义切面的具体行为。
Audience有四个方法,定义了一个观众在观看演出时可能会做的事情。在演出之前,观众要就坐takeSeats()并将手机调至静音状态silenceCellPhones()。如果演出很精彩的话,观众应该会鼓掌喝彩applause()。不过,如果演出没有达到观众预期的话,观众会要求退款demandRefund()

表4.2 Spring使用AspectJ注解来声明通知方法
|注解|通知|
|@After|通知方法会在目标方法返回或抛出异常后调用|
|@AfterReturning|通知方法会在目标方法返回后调用|
|@AfterThrowing|通知方法会在目标方法抛出异常后调用|
|@Around|通知方法会将目标方法封装起来|
|@Before|通知方法会在目标方法调用之前执行|

你可能已经注意到了,所有的这些注解都给定了一个切点表达式作为它的值,同时,这四个方法的切点表达式都是相同的。其实,它们可以设置成不同的切点表达式,但是在这里,这个切点表达式就能满足所有通知方法的需求。让我们近距离看一下这个设置给通知注解的切点表达式,我们发现它会在Performance的perform()方法执行时触发。
如果我们只定义这个切点一次,然后每次需要的时候引用它,那么这会是一个很好的方案。@Pointcut注解能够在一个@AspectJ切面内定义可重用的切点。

package concert;import org.aspectj.lang.annotation.AfterReturning;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;@Aspectpublic class Audience {    @Pointcut("execution(** concert.Performance.perform(..))")    public void performance() {}    @Before("performance()")    public void silenceCellPhones() {        System.out.println("Silencing cell phones");    }    @Before("performance()")    public void takeSeats() {        System.out.println("Taking seats");    }    @AfterReturning("performance()")    public void applause() {        System.out.println("CLAP CLAP CLAP!!!");    }    @AfterReturning("performance()")    public void demandRefund() {        System.out.println("Demanding a refund");    }}

然后进行配置:

package concert;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;import org.springframework.stereotype.Component;@Configuration@EnableAspectJAutoProxy@Componentpublic class ConcertConfig {    @Bean    public Audience audience() {        return new Audience();    }}

XML中:

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xmlns:aop="http://www.springframework.org/schema/aop"    xmlns:context="http://www.springframework.org/schema/context"    xsi:schemaLocation="http://www.springframework.org/schema/aop        http://www.springframework.org/schema/aop/spring-aop-3.2.xsd        http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/context        http://www.springframework.org/schema/context/spring-context.xsd">    <context:component-scan base-package="context" />    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>    <bean class="concert.Audience" /></beans>

不管你是使用JavaConfig还是XML,AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。在这种情况下,将会为Concert bean创建一个代理,Audience类中的通知方法将会在perform()调用前后执行。
我们需要记住的是,Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是Spring基于代理的切面。这一点非常重要,因为这意味着尽管使用的是@AspectJ注解,但我们仍然限于代理方法的调用。如果想利用AspectJ的所有能力,我们必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面。


4.3.2 创建环绕通知


package concert;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;@Aspectpublic class Audience {    @Pointcut("execution(** concert.Performance.perform(..))")    public void performance() {}    @Around("performance()")    public void watchPerformance(ProceedingJoinPoint jp) {        try {            System.out.println("Silencing cell phone");            System.out.println("Taking seats");            jp.proceed();            System.out.println("CLAP CLAP CLAP!!!");        } catch (Throwable e) {            System.out.println("Demanding a refund");        }    }}

在这里,@Around注解表明watchPerformance()方法会作为performance()切点的环绕通知。在这个通知中,观众在演出之前会将手机调至静音并就坐,演出结束后会鼓掌喝彩。像前面一样,如果演出失败的话,观众会要求退款。
可以看到,这个通知所达到的效果与之前的前置通知和后置通知是一样的。但是,现在它们位于同一个方法中,不像之前那样分散在四个不同的通知方法里面。
关于这个新的通知方法,你首先注意到的可能是它接受ProceedingJoinPoint作为参数。这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用ProceedingJoinPoint的proceed()方法。
需要注意的是,别忘记调用proceed()方法。如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。有可能这就是你想要的效果,但更多的情况是你希望在某个点上执行被通知的方法。你可以不调用proceed()方法,从而阻塞对被通知方法的访问,与之类似,你也可以在通知中对它进行多次调用。要这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。


4.3.3 处理通知中的参数


让我们重新看一下BlankDisc样例。

package soundsystem;import java.util.List;public class BlankDisc implements CompactDisc {    private String title;    private String artist;    private List<String> tracks;    public BlankDisc() {    }    public BlankDisc(String title, String artist, List<String> tracks) {        this.title = title;        this.artist = artist;        this.tracks = tracks;    }    public void setTitle(String title) {        this.title = title;    }    public void setArtist(String artist) {        this.artist = artist;    }    public void setTracks(List<String> tracks) {        this.tracks = tracks;    }    @Override    public void play() {        for (int i = 0; i < tracks.size(); i++) {            playTrack(i);        }    }    public void playTrack(int trackNumber) {        System.out.println(title + " - " + artist);        System.out.println("----" + tracks.get(trackNumber % tracks.size()));    }}

play()方法会循环所有的磁道并调用playTrack()方法。但是,我们也可以通过playTrack()方法直接播放某一个磁道中的歌曲。
假设你想记录每个磁道被播放的次数。一种方法就是修改playTrack()方法,直接在每次调用的时候记录这个数量。但是,记录磁道的播放次数与播放本身是不同的关注点,因此不应该属于playTrack()方法。看起来,这应该是切面要完成的任务。
为了记录每个磁道所播放的次数,我们创建了TrackCounter类,它是通知playTrack()方法的一个切面。

package soundsystem;import java.util.HashMap;import java.util.Map;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;@Aspectpublic class TrackCounter {    private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();    @Pointcut("execution(* soundsystem.CompactDisc.playTrack(int)) && args(trackNumber)")    public void trackPlayed(int trackNumber) {}    @Before("trackPlayed(trackNumber)")    public void countTrack(int trackNumber) {        int currentCount = getPlayCount(trackNumber);        trackCounts.put(trackNumber, currentCount + 1);    }    public int getPlayCount(int trackNumber) {        return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;    }}

像之前所创建的切面一样,这个切面使用@Pointcut注解定义命名的切点,并使用@Before将一个方法声明为前置通知。但是,这里的不同点在于切点还声明了要提供给通知方法的参数。

中需要关注的是切点表达式中的args(trackNumber)限定符。它表明传递给playTrack()方法的int类型参数也会传递到通知中去。参数的名称trackNumber也与切点方法签名中的参数相匹配。这个参数会传递到通知方法中,这个通知方法是通过@Before注解和命名切点trackPlayed(trackNumber)定义的。切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。
现在,我们可以在Spring配置中将BlankDisc和TrackCounter定义为bean,并启用AspectJ自动代理:

package soundsystem;import java.util.ArrayList;import java.util.List;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration@EnableAspectJAutoProxypublic class TrackCounterConfig {    @Bean    public CompactDisc compactDisc() {        BlankDisc cd = new BlankDisc();        cd.setTitle("Sgt. Pepper's Lonely Hearts Club Band");        cd.setArtist("The Beatles");        List<String> tracks = new ArrayList<String>();        tracks.add("Sgt. Pepper's Lonely Hearts Club Band");        tracks.add("With a Little Help from My Friends");        tracks.add("Lucy in the Sky with Diamonds");        tracks.add("Getting Better");        tracks.add("Fixing a Hole");        cd.setTracks(tracks);        return cd;    }    @Bean    public TrackCounter trackCounter() {        return new TrackCounter();    }}

然后测试:

package soundsystem;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(classes=TrackCounterConfig.class)public class TrackCounterTest {    @Autowired    private CompactDisc cd;    @Autowired    private TrackCounter counter;    @Test    public void testTrackCounter() {        cd.playTrack(0);        cd.playTrack(2);        cd.playTrack(3);        cd.playTrack(3);        cd.playTrack(2);        cd.playTrack(2);        cd.playTrack(7);    }}

4.3.4 通过注解引入新功能


在Spring中,切面只是实现了它们所包装bean相同接口的代理。如果除了实现这些接口,代理也能暴露新接口的话,会怎么样呢?那样的话,切面所通知的bean看起来像是实现了新的接口,即便底层实现类并没有实现这些接口也无所谓。

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

为了验证该主意能行得通,我们为示例中的所有的Performance实现引入下面的Encoreable接口:

package concert;public interface Encoreable {    void performEncore();}

我们需要有一种方式将这个接口应用到Performance实现中。我们现在假设你能够访问Performance的所有实现,并对其进行修改,让它们都实现Encoreable接口。但是,从设计的角度来看,这并不是最好的做法,并不是所有的Performance都是具有Encoreable特性的。另外一方面,有可能无法修改所有的Performance实现,当使用第三方实现并且没有源码的时候更是如此。
值得庆幸的是,借助于AOP的引入功能,我们可以不必在设计上妥协或者侵入性地改变现有的实现。为了实现该功能,我们要创建一个新的切面:

package concert;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.DeclareParents;@Aspectpublic class EncoreableIntroducer {    @DeclareParents(value = "concert.Performance+", defaultImpl = DefaultEncoreable.class)    public static Encoreable encoreable;}

可以看到,EncoreableIntroducer是一个切面。但是,它与我们之前所创建的切面不同,它并没有提供前置、后置或环绕通知,而是通过@DeclareParents注解,将Encoreable接口引入到Performance bean中。
@DeclareParents注解由三部分组成:

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

同样,我们需要将EncoreableIntroducer声明为一个bean:

<bean clas="concert.EncoreableIntroducer" />

Spring的自动代理机制将会获取到它的声明,当Spring发现一个bean使用了@Aspect注解时,Spring就会创建一个代理,然后将调用委托给被代理的bean或被引入的实现,这取决于调用的方法属于被代理的bean还是属于被引入的接口。
在Spring中,注解和自动代理提供了一种很便利的方式来创建切面。它非常简单,并且只涉及到最少的Spring配置。但是,面向注解的切面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到这一点,必须要有源码。
如果你没有源码的话,或者不想将AspectJ注解放到你的代码之中,Spring为切面提供了另外一种可选方案。让我们看一下如何在Spring XML配置文件中声明切面。


4.4 在XML中声明切面


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

表4.3 Spring的AOP配置元素能够以非侵入性的方式声明切面

AOP配置元素 定义AOP通知器 <aop:advisor> 定义AOP通知器 <aop:after> 定义AOP后置通知(不管被通知的方法是否执行成功) <aop:after-returning> 定义AOP返回通知 <aop:after-throwing> 定义AOP异常通知 <aop:around> 定义AOP环绕通知 <aop:aspect> 定义一个切面 <aop:aspectj-autoproxy> 启用@AspectJ注解驱动的切面 <aop:before> 定义一个AOP前置通知 <aop:config> 顶层的AOP配置元素。大多数的<aop:*>元素必须包含在<aop:config>元素内 <aop:declare-parents> 以透明的方式为被通知的对象引入额外的接口 <aop:pointcut> 定义一个切面

我们重新看一下Audience类:

package concert;import org.aspectj.lang.annotation.Aspect;@Aspectpublic class Audience {    public void silenceCellPhones() {        System.out.println("Silencing cell phones");    }    public void takeSeats() {        System.out.println("Taking seats");    }    public void applause() {        System.out.println("CLAP CLAP CLAP!!!");    }    public void demandRefund() {        System.out.println("Demanding a refund");    }}

4.4.1 声明前置通知和后置通知


<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xmlns:aop="http://www.springframework.org/schema/aop"    xmlns:context="http://www.springframework.org/schema/context"    xsi:schemaLocation="http://www.springframework.org/schema/aop        http://www.springframework.org/schema/aop/spring-aop-3.2.xsd        http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/context        http://www.springframework.org/schema/context/spring-context.xsd">    <context:component-scan base-package="context" />    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>    <bean id="audience" class="concert.Audience" />    <bean class="concert.PerformanceImpl" />    <aop:config>        <aop:aspect ref="audience">            <aop:before pointcut="execution(** concert.Performance.perform(..))" method="silenceCellPhones" />            <aop:before pointcut="execution(** concert.Performance.perform(..))" method="takeSeats" />            <aop:after-returning pointcut="execution(** concert.Performance.perform(..))" method="applause" />                      <aop:after-throwing pointcut="execution(** concert.Performance.perform(..))" method="demandRefund" />        </aop:aspect>    </aop:config></beans>

关于Spring AOP配置元素,第一个需要注意的事项是大多数的AOP配置元素必须在<aop:config>元素的上下文内使用。这条规则有几种例外场景,但是把bean声明为一个切面时,我们总是从<aop:config>元素开始配置的。
<aop:config>元素内,我们可以声明一个或多个通知器、切面或者切点。在程序清单4.9中,我们使用<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()方法。图4.8展示了通知逻辑如何织入到业务逻辑中。

在所有的通知元素中,pointcut属性定义了通知所应用的切点,它的值是使用AspectJ切点表达式语法所定义的切点。
你或许注意到所有通知元素中的pointcut属性的值都是一样的,这是因为所有的通知都要应用到相同的切点上。
在基于AspectJ注解的通知中,当发现这种类型的重复时,我们使用@Pointcut注解消除了这些重复的内容。而在基于XML的切面声明中,我们需要使用<aop:pointcut>元素。如下的XML展现了如何将通用的切点表达式抽取到一个切点声明中,这样这个声明就能在所有的通知元素中使用了。

<aop:config>    <aop:aspect ref="audience">        <aop:pointcut expression="execution(** concert.Performance.perform(..))" id="performance"/>        <aop:before pointcut-ref="performance" method="silenceCellPhones" />        <aop:before pointcut-ref="performance" method="takeSeats" />        <aop:after-returning pointcut-ref="performance" method="applause" />                    <aop:after-throwing pointcut-ref="performance" method="demandRefund" />    </aop:aspect></aop:config>

4.4.2 声明环绕通知


考虑watchPerformance()

package concert;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Aspect;@Aspectpublic class Audience {    public void watchPerformance(ProceedingJoinPoint jp) {        try {            System.out.println("Silencing cell phone");            System.out.println("Taking seats");            jp.proceed();            System.out.println("CLAP CLAP CLAP!!!");        } catch (Throwable e) {            System.out.println("Demanding a refund");        }    }}

我们所需要做的仅仅是使用<aop:around>元素。

<aop:config>    <aop:aspect ref="audience">        <aop:pointcut expression="execution(** concert.Performance.perform(..))" id="performance"/>        <aop:around pointcut-ref="performance" method="watchPerformance"/>    </aop:aspect></aop:config>

4.4.3 为通知传递参数


<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"    xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"    xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xmlns:aop="http://www.springframework.org/schema/aop"    xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/beans        http://www.springframework.org/schema/spring-beans.xsd        http://www.springframework.org/schema/util        http://www.springframework.org/schema/util/spring-util.xsd        http://www.springframework.org/schema/aop        http://www.springframework.org/schema/aop/spring-aop.xsd">    <!-- configuration details go here -->    <util:list id="trackList">        <value>Sgt. Pepper's Lonely Hearts Club Band</value>        <value>With a Little Help from My Friends</value>        <value>Lucy in the Sky with Diamonds</value>        <value>Getting Better</value>        <value>Fixing a Hole</value>    </util:list>    <bean id="compactDisc" class="soundsystem.BlankDisc"        c:_0="Sgt. Pepper's Lonely Hearts Club Band" c:_1="The Beatles"        c:_2-ref="trackList" />    <bean id="trackCounter" class="soundsystem.TrackCounter" />    <aop:config>        <aop:aspect ref="trackCounter">            <aop:pointcut expression="execution(* soundsystem.CompactDisc.playTrack(int)) and args(trackNumber)" id="trackPlayed"/>            <aop:before pointcut-ref="trackPlayed" method="countTrack" />        </aop:aspect>    </aop:config></beans>

由于&在XML中是特殊符号,所以我们使用and


4.4.4 通过切面引入新功能


使用<aop:declare-parents>可以引入新的方法:

<aop:aspect>    <aop:declare-parents types-matching="concert.Performance+"        implement-interface="concert.Encoreable" default-impl="concert.DefaultEncoreable" /></aop:aspect>

然后测试:

public static void main(String[] args) throws Exception {    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("concert-config.xml");    Performance performance = context.getBean(Performance.class);    performance.perform();    Encoreable encoreable = context.getBean(Encoreable.class);    encoreable.performEncore();    context.close();}

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

<aop:aspect>    <aop:declare-parents types-matching="concert.Performance+"        implement-interface="concert.Encoreable" delegate-ref="encoreableDelegate" /></aop:aspect>

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

<bean id="encoreableDelegate" class="concert.DefaultEncoreable" />

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


4.5 注入AspectJ切面


AspectJ提供了Spring AOP所不能支持的许多类型的切点。

当我们需要在创建对象时应用通知,构造器切点就非常方便。不像某些其他面向对象语言中的构造器,Java构造器不同于其他的正常方法。这使得Spring基于代理的AOP无法把通知应用于对象的创建过程。

对于大部分功能来讲,AspectJ切面与Spring是相互独立的。虽然它们可以织入到任意的Java应用中,这也包括了Spring应用,但是在应用AspectJ切面时几乎不会涉及到Spring。

但是精心设计且有意义的切面很可能依赖其他类来完成它们的工作。如果在执行通知时,切面依赖于一个或多个类,我们可以在切面内部实例化这些协作的对象。但更好的方式是,我们可以借助Spring的依赖注入把bean装配进AspectJ切面中。

为了演示,我们为上面的演出创建一个新切面。具体来讲,我们以切面的方式创建一个评论员的角色,他会观看演出并且会在演出之后提供一些批评意见。下面的CriticAspect.aj就是一个这样的切面。

package concert;public aspect CriticAspect {    public CriticAspect() {}    pointcut performance() : execution(* perform(..));    after() returning() : performance() {        System.out.println(criticismEngine.getCriticism());    }    private CriticismEngine criticismEngine;    public void setCriticismEngine(CriticismEngine criticismEngine) {        this.criticismEngine = criticismEngine;    }}

CriticAspect的主要职责是在表演结束后为表演发表评论。程序中的performance()切点匹配perform()方法。当它与afterReturning()通知一起配合使用时,我们可以让该切面在表演结束时起作用。
程序有趣的地方在于并不是评论员自己发表评论,实际上,CriticAspect与一个CriticismEngine对象相协作,在表演结束时,调用该对象的getCriticism()方法来发表一个苛刻的评论。为了避免CriticAspect和CriticismEngine之间产生不必要的耦合,我们通过Setter依赖注入为CriticAspect设置CriticismEngine。

而CriticismEngineImpl则为:

package concert;public class CriticismEngineImpl implements CriticismEngine {    private String[] criticismPool;    @Override    public String getCriticism() {        int i = (int) (Math.random() * criticismPool.length);        return criticismPool[i];    }    public void setCriticismPool(String[] criticismPool) {        this.criticismPool = criticismPool;    }}

然后配置XML:

<bean id="criticismEngine" class="concert.CriticismEngineImpl">    <property name="criticismPool">        <list>            <value>Wrost performance ever!</value>            <value>I laughed, I cried, then I realized I was at the wrong show.</value>            <value>A must see show!</value>        </list>    </property></bean>

我们现在有了一个要赋予CriticAspect的Criticism-Engine实现。剩下的就是为CriticAspect装配CriticismEngineImple。
在展示如何实现注入之前,我们必须清楚AspectJ切面根本不需要Spring就可以织入到我们的应用中。如果想使用Spring的依赖注入为AspectJ切面注入协作者,那我们就需要在Spring配置中把切面声明为一个Spring配置中的<bean>。如下的<bean>声明会把criticismEnginebean注入到CriticAspect中:

<bean class="concert.CriticAspect" factory-method="aspectOf">    <property name="criticismEngine" ref="criticismEngine"></property></bean>

然后运行:

public static void main(String[] args) throws Exception {    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("concert-config.xml");    CriticismEngine criticismEngine = context.getBean(CriticismEngine.class);    System.out.println(criticismEngine.getCriticism());    context.close();}

很大程度上,<bean>的声明与我们在Spring中所看到的其他<bean>配置并没有太多的区别,但是最大的不同在于使用了factorymethod属性。通常情况下,Spring bean由Spring容器初始化,但是AspectJ切面是由AspectJ在运行期创建的。等到Spring有机会为CriticAspect注入CriticismEngine时,CriticAspect已经被实例化了。

因为Spring不能负责创建CriticAspect,那就不能在 Spring中简单地把CriticAspect声明为一个bean。相反,我们需要一种方式为Spring获得已经由AspectJ创建的CriticAspect实例的句柄,从而可以注入CriticismEngine。幸好,所有的AspectJ切面都提供了一个静态的aspectOf()方法,该方法返回切面的一个单例。所以为了获得切面的实例,我们必须使用factory-method来调用asepctOf()方法而不是调用CriticAspect的构造器方法。

简而言之,Spring不能像之前那样使用<bean>声明来创建一个CriticAspect实例——它已经在运行时由AspectJ创建完成了。Spring需要通过aspectOf()工厂方法获得切面的引用,然后像元素规定的那样在该对象上执行依赖注入。