[原]Spring学习笔记(九) --- 在Spring中创建切面

祝一迪 17/10/23 21:52:09

本系列博客为spring In Action 这本书的学习笔记

在上一篇博客中, 我们了解了AOP的基本概念和Spring中的AOP, 那么本篇博客就来实际操练一下, 看看在Spring中如何创建一个切面.


一. 编写切点

通知和切点是切面的最基本的元素. 那么要创建一个切面, 我们就从定义切点开始吧. 切点定义了在哪些连接点来应用通知.

在Spring AOP中, 要使用AspectJ的切点表达式语言来定义切点. Spring仅支持AspectJ切点指示器的一个子集, 下面的表格列出了Spring AOP所支持的AspectJ切点指示器:

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

在上面这些指示器中, 只有execution指示器是实际执行匹配的, 其它的指示器是用来限制匹配的, 这也说明了execution指示器是我们在编写切点定义时最主要使用的指示器.

1. 定义切点

为了更好地说明在Spring中创建切面, 在本文里我们将以一个表演的例子来说明如何创建切面.
首先我们给出一个表演的Performance接口:

程序1: Performance接口

public interface Performance {
    public void perform();
}

Performance可以代表任何类型的表演, 如舞台剧/电影或音乐会. 为了处理在表演过程中的一些事情, 我们将切点定义为当perform()方法执行时触发通知, 执行切面逻辑.

那么这个切点表达式应该这样写:
execution(* Concert.Performance.perform(. .))

execution()指示器选择了以Concert.Performance.perform()为全限定方法名的方法, 其中的”*”说明了可以返回任意类型, 在方法参数列表中使用两个点号(. .)表明了切点要选择具有任意参数的perform()方法.


这就是一个简单的切点定义, 如果我们想进一步限制该切点匹配的的条件, 比如当前的切点表达式仅仅是在perform()方法被执行时触发通知, 这也就说我们可以在其他的包里来实现Performance接口的perform()方法, 从而触发通知, 那么现在我们要将触发通知的范围限制在Concert包里呢?

可以这样编写切点表达式:
execution(* Concert.Performance.perform(. .)) && within(Concert.*)

&&符的作用就是and的作用, 也就是说, 现在要触发通知的条件为Concert包及其子包下的类实现Performance的perform()方法并执行它时.

因为&在XML中有特殊含义, 所以切点表达式中的and为”&&”, and为”||”, not为”!”.

2. 在切点中选择Bean

除了上面提到的AspectJ中的指示器外, Spring还引入了一个新的bean()指示器, 它允许我们在切点表达式使用Bean的ID来标识Bean. bean()使用Bean ID或Bean的名称作为参数来限制切点只匹配特定的Bean.
例如下面的切点表达式:

execution(* Concert.Perform.perform()) and bean(‘JayZhou’)
这样就把在执行Performance的perform()方法时应用通知, 但限定Bean的ID为JayZhou.

我们还可以在bean()指示器上使用非(!)操作. 比如:

execution(* Concert.Perform.perform()) and !bean(‘JayZhou’)

二. 使用注解创建切面

前面提到的Performance接口, 它是切面中切点的目标对象, 现在我们就来使用AspectJ注解来创建切面, 并且在切面里面编写通知方法.

1. 编写切面类及通知方法

如果一场演出没有观众那就不能称为一场演出, 但是从演出的角度来看, 观众虽然重要, 但并不是核心, 甚至可以说, 没有观众演出也可以正常进行. 这就像是我们在Spring系列博客的第一篇里面讲到的, 一个骑士去拯救公主, 会有一个吟游诗人来歌颂他的事迹, 但是没有吟游诗人骑士也能去拯救公主. 所以, 演出里面的观众和骑士拯救公主里面的吟游诗人一样, 都是一个独立的关注点, 所以要将它们抽象为一个切面. 那么下面就来看一下抽象为切面的观众类Audience:

程序2: Audience类: 观看演出的切面

@Aspect
public class Audience {
    @Before("execution(* Concert.Performance.perform(..))")
    public void silenceCellPhone(){
        System.out.println("Sliencing 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!!!");
    }

    @AfterThrowing("execution(* Concert.Performance.perform(..))")
    public void demandRefund(){
        System.out.println("Demanding a refund");
    }
}

Audience有四个方法, 定义了一个观众在观看演出的时候可能会做的事情. 在演出之前要将手机调至静音(silenceCellPhone())并就坐(takeSeats()), 在演出圆满结束之后, 观众要进行鼓掌(applause()), 但如果表演搞砸了的话, 观众就会要求退款(demandRefund()).

上面的这四个方法都时通知方法, 并且使用了通知注解来表明它们应该在什么时候被调用. AspectJ提供了五个注解来定义通知方法, 如下表:

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

上面的这五种注解都需要给定一个切点表达式作为它的值.
而在Audience这个切面中, 四个通知方法的注解都使用了同一个切点表达式, 这是令我们不太满意的地方, 所以我们来优化一下, 只定义一次这个切点, 然后在每次需要的时候引用它就可以了.
在一个切面里面使用@Pointcut来定义可重用的切点.

程序3: 使用@Pointcut标注的Audience切面

@Aspect
public class Audience {
    @Pointcut("execution(* Concert.Performance.perform(..))")
    public void performance() {
    }

    @Before("performance()")
    public void silenceCellPhone() {
        System.out.println("Sliencing cell phones");
    }

    @Before("performance()")
    public void takeSeats() {
        System.out.println("Taking seats");
    }

    @AfterReturning("performance()")
    public void applause() {
        System.out.println("CLAP CLAP CLAP!!!");
    }

    @AfterThrowing("performance()")
    public void demandRefund() {
        System.out.println("Demanding a refund");
    }
}

performance()方法的时及内容并不重要, 实际上在这里它也应该是空的. 这个方法本身只是一个标识, 供@Pointcut注解依附.

我们还观察到, 除了注解和没有实际操作的performance()方法, Audience仍然是一个普通的POJO, 只不过它通过注解表明会作为切面使用而已. 我们可以像使用其它普通的Java类一样去使用它, 这也体现出Spring的最小侵略性编程的特点.

2. 启用AspectJ注解的自动代理功能

如果我们只写到这里, 那么Audience只会是Spring容器的一个Bean, 即使它使用了AspectJ注解, 但是它仍然不会被视为切面, 这些注解不会被解析, 也不会创建将其转化为切面的代理. 所以我们要通过JavaConfig或者XML配置来启用自动代理功能.

(1) 通过JavaConfig启用自动代理

在JavaConfig中, 通过使用@EnableAspectJAutoProxy注解来启用自动代理功能. 代码如下:

程序4: 在JavaConfig中启用自动代理

@Configuration
@EnableAspectJAutoProxy //启用AspectJ自动代理
@ComponentScan
public class ConcertConfig {

    //声明Audience Bean
    @Bean
    public Audience audience(){
        return new Audience();
    }
}

(2) 通过XML配置启用自动代理

程序5: 通过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:context="http://www.springframework.org/schema/context"
       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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--开启自动扫描-->
    <context:component-scan base-package="Concert" />

    <!--启用AspectJ自动代理-->
    <aop:aspectj-autoproxy />

    <!--声明Audience Bean-->
    <bean class="Concert.Audience" />

</beans>

不管使用哪种方式开启AspectJ自动代理功能, AspectJ自动代理都会为使用@Aspect注解的Bean创建一个代理, 这个代理会围绕着所该切面的切点所匹配的Bean.

需要注意的是, Spring的AspectJ自动代理仅仅是使用@AspectJ作为创建切面的指导, 但是本质上它仍然是Spring基于代理的切面, 这一点很重要. 因为这代表着尽管我们使用的是@AspectJ注解, 但是我们仍然受限于代理方法的调用, 如果我们想利用AspectJ的所有能力, 就必须使用AspectJ并且不依赖于Spring来创建切面.

3. 创建环绕通知

环绕通知是最强大的通知类型, 它能让你所编写的逻辑将被通知的目标方法完全包装起来. 实际上就像是在一个通知方法中同时编写前置通知(比如@Before标注的通知)和后置通知(比如@After标注的通知).
下面就使用环绕通知来重新编写一下观众切面:

程序6: 使用环绕通知重新实现Audience切面

@Aspect
public class AudienceAround {
    @Pointcut("execution(* Concert.Performance.perform(..))")
    public void performance() {
    }

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint){
        try {
            System.out.println("Sliencing cell phones");
            System.out.println("Taking seats");
            joinPoint.proceed();
            System.out.println("CLAP CLAP CLAP!!!");
        } catch (Throwable throwable) {
            System.out.println("Demanding a refund");
        }
    }
}

在上面这个环绕通知的方法中, 它接受了一个ProceedingJoinPoint的对象作为参数, 这个对象是必须要有的, 因为要在通知中通过它来调用被通知的方法, 并且还需要调用ProceedingJoinPoint的proceed()方法, 如果不调用这个方法的话, 那么你的通知实际上会阻塞对被通知方法的调用. 与之相似, 你也可以多次调用proceed()方法, 比如在重试逻辑里就需要这样做.

4. 处理通知中的参数

到目前为止, 我们所编写的切面都很简单, 除了我们在编写环绕通知的时候传入了ProceedingJoinPoint的对象作为参数, 其它的通知里面都没有任何参数. 这很正常, 因为我们所通知的perform()方法本身没有任何参数.

但是如果切面所通知的方法中确实有参数该怎么办呢? 切面能访问和使用传递给被通知方法的参数吗?

(1) 编写有参数的切面

为了说明这个问题, 让我们重新修改一下Spring学习笔记之通过XML装配Bean中的程序11.

程序7: MayDayDisc

public class MayDayDisc implements CompactDisc {
    private String title;
    private String artist;
    private List<String> tracks;

    public MayDayDisc(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;
    }

    public void play() {
        System.out.println("Playing " + title + " by " + artist);

        for(int i = 0; i < tracks.size(); i++){
            playTrack(i);
        }
    }

    public void playTrack(int track) {
        System.out.println("-Track: " + tracks.get(track));
    }
}

在这个程序中我们加入了playtrack()方法, 用来播放每一个磁道, 并且在play()方法中循环调用playtrack(). 我们也可以直接通过playTrack()来指定播放哪个磁道的音乐.
假如现在我们要记录每个磁道被播放的次数, 其中的一种方法就是直接修改playTrack()的方法, 在每次调用的时候记录这个数量. 但是, 记录磁道的播放次数于播放本身是不同的关注点, 因此不应该在playTrack()中实现这个功能, 而这看起来应该是切面要完成的任务.

为了记录每个磁道的播放次数, 我们创建TrackCounter类, 它是通知playTrack()方法的一个切面. 代码如下:

程序8: TrackCounter切面

@Aspect
public 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;
    }
}

在上面切面的切点表达式中的args(trackNumber)这部分, 它表明传递给playTrack()方法的int类型的参数也会传递到通知方法中去. 而这种传递方式就是通过切点方法trackPlayed()中的参数进行传递, 也就是说trackPlayed()中的参数和countPlayed()中的参数一致就可以了.

(2) 进行配置启用AspectJ自动代理

现在, 我们就在Spring配置中将MayDayDisc和TrackCounter定义为Bean, 并且启用ASpectJ自动代理.

程序9: 启用TrackCount切面的自动代理, 为每个磁道记录播放次数

@Configuration
@EnableAspectJAutoProxy
public class TrackCounterConfig {
    @Bean
    public CompactDisc mayDay(){
        List<String> tracks = new ArrayList<String>();

        MayDayDisc cd = new MayDayDisc();
        cd.setArtist("五月天");
        cd.setTitle("后青春期的诗");
        tracks.add("突然好想你");
        tracks.add("你不是真正的快乐");
        tracks.add("笑忘歌");
        tracks.add("如烟");
        tracks.add("我心中尚未崩坏的地方");
        tracks.add("生存以上生活以下");
        cd.setTracks(tracks);

        return cd;
    }

    @Bean
    public TrackCounter trackCounter(){
        return new TrackCounter();
    }
}

下面我们来测试一下TrackCounter切面:

程序10: 测试TrackCounter切面

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=TrackCounterConfig.class)
public class TrackCounterTest {
    @Rule
    public final StandardOutputStreamLog log = new StandardOutputStreamLog();

    @Autowired
    private CompactDisc cd;

    @Autowired
    private TrackCounter counter;

    @Test
    public void testTrackCounter(){
        cd.playTrack(0);
        cd.playTrack(0);
        cd.playTrack(0);
        cd.playTrack(0);
        cd.playTrack(4);
        cd.playTrack(4);
        cd.playTrack(4);
        cd.playTrack(5);
        cd.playTrack(5);

        assertEquals(4, counter.getPlayCount(0));
        assertEquals(3, counter.getPlayCount(4));
        assertEquals(2, counter.getPlayCount(5));
    }
}

到现在未知, 我们所使用的切面中, 所包装的都是被通知对象(目标对象)的已有方法, 但是, 这并不是切面的全部, 我们还可以通过切面引入目标对象里没有的新功能.

三. 通过注解为目标对象引入新功能

在动态编程语言中, 它们可以不用直接修改对象或类的定义就能够为类和对象添加新的功能, 虽然Java拥有反射机制, 使得它拥有了一丢丢动态语言的性质, 但是归根究底, 它还是非动态语言. 但是, 仔细想一想, 切面难道不就是一直在做动态语言的事情吗? 我们通过切面给对象中的一个方法加上了它原本没有的功能. 就比如表演类的perform()方法本身是没有观众这个功能的, 而切面为它添加了这个功能. 那现在既然我们能为方法添加新的功能, 那也能为一个对象添加新的方法. 这里就用到了前面我们提到的引入的概念. 来看一下在上一篇博客中我们对引入的定义:

引入允许我们通过切面向现有的Spring Bean添加新方法或者新属性.

当时可能有人不理解这句话, 那么下来我们所做的事情就全部是AOP里的引入给我们提供的功能.

下面我们来详细理解一下Sping是怎样应用引入功能的.


这里写图片描述

Spring的切面由包裹了目标对象的代理实现. 代理类处理方法的调用, 执行额外的切面逻辑, 并调用目标方法. 而现在对于引入而言, 除了可以执行目标对象已有的方法(已有的接口), 还可以执行新的方法(也就是目标对象没有的方法), 即新的接口, 即底层实现类(目标对象)并没有实现的方法(接口).

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

为了说明引入功能, 我们来举个例子. 现在要为前面示例中的Performance的实现引入下面的Encoreable接口(指演出结束之后观众要求返场表演):

程序11: Encoreable接口

public interface Encoreable {
    void performEncore();
}

现在假设我们需要所有的Performance接口的实现都必须实现Encoreable接口, 我们当然可以选择直接去修改Performance接口的所有实现, 但是这并不是最好的办法, 一来是因为是因为很复杂, 二来是因为我们有时候是没办法修改有的实现的. 我们可以使用AOP来实现这个需求, 而不必在设计上妥协或者侵入性地改变现有功能. 为了实现该功能, 我们需要创建一个新的切面.

程序12: EncoreableIntroducer切面

@Aspect
public class EncoreableIntroducer {
    @DeclareParents(value="Concert.Performance+",
                    defaultImpl = DefaultEncoreable.class)
    public static Encoreable encoreable;
}

在上面的代码中, 使用了@DeclareParents注解:

  • value属性指定了哪种类型的Bean要引入该接口. 在本例中就是Performance接口.
  • defaultImpl属性指定了为引入功能提供实现的类. 也就是说Encoreable接口的功能全部由DefaultEncoreable类来实现.
  • @DeclareParents注解所标注的静态属性指明了要引入的接口. 在本例中就是要引入Encoreable接口.

与其它切面一样, 我们需要在XML中将其声明为一个Bean.

    <bean class="Concert.EncoreableIntroducer" />

Spring的自动代理机制会获取到它的声明, 然后当Spring发现一个Bean中使用了@Aspect注解时, Spring就会创建一个代理, 然后由代理调用目标Bean的方法或是被引入接口的方法.


以上就是关于Spring的面向注解的切面声明, 它通过使用注解和自动代理机制来完成. 这样做的好处就是它涉及到最少的Spring配置, 但是面向注解的切面声明由一个明显的劣势, 就是我们必须能够为通知类添加注解, 这就要求我们必须拥有源码.
如果没有源码, 那么我们就要选择另外一种方案: 在XML配置文件中声明切面.

四. 在XML中声明切面

先来看一下在XML中声明切面需要用到的AOP配置元素:

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

1. 通过XML声明前置通知和后置通知

前面提到过, Spring的面向注解的声明切面功能也是体现出Spring最小侵略性编程的一方面, 所以现在我们将Audience类的所有注解都移除, 它不过就是一个再普通不过的Java类.

public class Audience {
    public void silenceCellPhone() {
        System.out.println("Sliencing 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");
    }
}

去掉注解的Audience类显然已经不是一个切面了, 但是它里面的这些方法其实已经具备成为切面的条件了, 现在只需要在XML中将它进行配置, 就能将它声明为一个切面了.

程序13: 通过XML将Audience声明为一个切面

  <!--通过XML将Audience声明为一个切面-->
    <aop:config>
        <aop:aspect ref="audience">
            <!--定义前置通知(表演之前)-->
            <aop:before pointcut="execution(* Concert.Performance.perform(..))"
                        method="silenceCellPhone" />
            <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>

与前面使用注解声明切面一样, 这里也将同一个切点表达式重复了四次, 现在我们对它来进行改进:

程序14: 使用<aop:pointcut>定义命名切点

    <aop:config>
        <aop:pointcut id="performance" 
                      expression="execution(* Concert.Performance.perform(..))" />
        <aop:aspect>
            <aop:before pointcut-ref="performance"
                        method="silenceCellphone" />
            <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>

2. 通过XML声明环绕通知

与上面去掉所有注解的Audience类一样, 现在我们也去掉AudienceAround环绕通知切面里面的所有注解:

public class AudienceAround {
    public void watchPerformance(ProceedingJoinPoint joinPoint){
        try {
            System.out.println("Sliencing cell phones");
            System.out.println("Taking seats");
            joinPoint.proceed();
            System.out.println("CLAP CLAP CLAP!!!");
        } catch (Throwable throwable) {
            System.out.println("Demanding a refund");
        }
    }
}

现在的AudienceAround类虽然不是一个切面, 但是它也具备了环绕通知切面的条件, 我们只需要使用一点XML配置就能将它声明成切面.

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


    <!--使用<aop:around>元素声明环绕通知切面-->
    <aop:config>
        <aop:pointcut id="performance"
                      expression="execution(* Concert.Performance.perform(..))" />
        <aop:aspect>
            <aop:around pointcut-ref="performance" method="watchPerformance" />
        </aop:aspect>
    </aop:config>

3. 在XML中为通知传递参数

在前面我们使用了@AspectJ注解创建了一个切面, 这个切面能够记录CD的每个磁道被播放的次数, 现在我们使用XML来配置参数化的切面.

同样的, 先移除掉TrackCounter上的所有的@AspectJ注解:

public class TrackCounter {
    private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();

    public void countTrack(int number){
        int currentCount = getPlayCount(number);
        trackCounts.put(number, currentCount+1);
    }

    public int getPlayCount(int trackNumber){
        return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
    }
}

现在在XML中来将它配置成一个切面:

程序16: 在XML中将TrackCounter配置为参数化的切面

    <aop:config>
        <aop:aspect ref="trackCounter">
            <aop:pointcut id="trackCounter" 
                          expression="execution(* SoundSystem.CompactDisc.playTrack(int)) 
                          and args(number)" />
            <aop:before pointcut-ref="trackCounter" method="countTrack" />
        </aop:aspect>
    </aop:config>

4. 在XML中通过切面引入新的功能

在Spring中, 我们不但可以使用@AspectJ为目标Bean引入新的功能, 在XML中使用<aop:declare-parents>元素来完成相同的功能.

对于上面程序12的功能, 我们用XML完成:

程序16: 在XML中为目标Bean引入新功能

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

也可以使用delegate-ref引用一个Bean来作为引入的委托:

<bean id="encoreableDelegate" class="Concert.DefaultEncoreable" />
    <aop:config>
        <aop:aspect>
            <aop:declare-parents types-matching="Concert.Performance+"
                                 implement-interface="Concert.Encoreable"
                                 delegate-ref="encoreableDelegate" />
        </aop:aspect>
    </aop:config>

五. 注入AspectJ切面

前面所讲的都是Spring AOP所提供的功能, 虽然Spring AOP在大多数时候能满足需求, 但是Spring AOP没办法提供方法拦截至外的连接点拦截功能. 例如我们需要在创建对象的时候应用通知, 构造器切点就会很方便. 但是由于Java的构造方法与普通方法是不同的, 使得AOP无法将通知应用于对象的创建过程. 所以我们就可以使用AspectJ切面来完成这部分的功能, 并借助Spring的依赖注入机制将Bean注入到AspectJ切面中.

举个例子, 现在需要给每场演出都创建一个评论员的角色, 他会观看演出并且会在演出结束之后提供一些批评意见. 那么下面的CriticAspect就是这样一个评论员的切面:

程序17: 使用AspectJ实现表演的评论员

//需要注意这是一个ASpectJ文件, 并不是普通的Java文件
public aspect CriticAspect {
    public CriticAspect(){ }

    //定义切点, 并且使得切点匹配perform()方法
    pointcut performamce() : execution(* perform(..));

    //定义通知类型, 并且匹配通知方法
    after() : performance() {
        System.out.println(criticismEngine.getCriticism());
    }

    //依赖注入
    //这里并不是评论员这个类本身来发表评论, 发表评论是由CriticismEngine这个接口的实现类提供的.
    //为了避免CriticAspect于CriticismEngine之间产生的不必要的耦合, 
    // 我们通过Setter依赖注入为CriticAspect设置CriticismEngine.
    private CriticismEngine criticismEngine;

    public void setCriticismEngine(CriticismEngine criticismEngine){
        this.criticismEngine = criticismEngine;
    }
 }

程序18: CriticismEngine接口

public interface CriticismEngine {
    String getCriticism();
}

程序19: CriticismEngineImpl类

//CriticismEngineImpl类实现了CriticismEngine, 通过从注入的评论池中随机选择一个苛刻的评论
public class CriticismEngineImpl implements CriticismEngine {
    public CriticismEngineImpl(){ }

    public String getCriticism() {
        int i = (int) (Math.random() * criticismPool.length);
        return criticismPool[i];
    }

    private String[] criticismPool;
    public void setCriticismPool(String[] criticismPool){
        this.criticismPool =criticismPool;
    }
}

如程序17中的注释所说, 切面也需要注入. 像其它Bean一样, Spring可以为ASpectJ切面注入依赖. 其关系如下图:
这里写图片描述

现在我们已经有了评论员切面CriticAspect, 也有了评论类的实现CriticismEngineImpl, 现在要做的事情就是在XML中声明CriticismEngineImpl, 然后再将CriticismEngine Bean注入到CriticAspect切面中:

程序20: 在XML中装配AspectJ切面

    <!--在XML中装配ASpectJ切面-->
    <bean id="criticismEngine" class="Concert.CriticismEngineImpl">
        <property name="criticismPool">
            <list>
                <value>个别演员的表情需要更丰富一点!</value>
                <value>背景音乐太难听了!</value>
                <value>演员肢体动作太僵硬!</value>
                <value>服装太丑了!</value>
            </list>
        </property>
    </bean>

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

在上面的代码中, 可以看到最大的不同就是使用了factory-method属性. 这是因为在通常情况下, Spring Bean由Spring容器初始化, 但是AspectJ切面是由AspectJ在运行期创建的. 也就是说, 等到Spring有机会为CriticAspect注入criticismEngine时, CriticAspect已经被实例化了. 所以Spring需要通过aspectOf()工厂方法获得切面的引用, 然后像<bean>元素规定的那样在该对象上执行依赖注入.

aspectOf()方法是所有的AspectJ切面都会提供的一个静态方法, 该方法返回切面的一个单例.


到这里就将Spring的的一部分讲完了, 第一部分是Spring的核心, 它向我们详尽地展现了Spring的核心特性, 比如依赖注入/面向切面等.
接下来的一部分就是学习web中的Spring, 我们会学习如何使用Spring MVC这个web框架, 如何将Spring的核心技术应用到web中.

好啦, 那就先到这里啦, 后面再接着学习吧~

作者:dela_ 发表于 2017/10/23 21:52:09 原文链接 http://blog.csdn.net/dela_/article/details/78323881
阅读:219 评论:2 查看评论