Spring AOP 编程

软件系统中的一些功能,可能会被应用到程序的多个地方,这些功能被称为横切关注点(cross-cutting concern)但是在开发的过程中,不希望在具体的业务逻辑中明确的调用这些横切关注点,把横切关注点域业务逻辑相分离的思想,正是面向切面编程(AOP 编程)

1、什么是面向切面编程

上图展示了一个被划分为模块开发的典型应用,每个模块的核心功能都是为了特定的业务提供服务,但这些模块都需要类似的辅助功能,例如安全和事务管理。

如果要重用通用功能,最常见的面向对象技术是继承或者是委托,但是如果在整个应用中都使用相同的基类,继承往往会导致一个脆弱的对象体系;而委托可能需要对委托对象进行复杂的调用。

通过Spring的AOP编程,可以实现在一个地方定义通用的功能,但是可以不修改代码和明确调用的情况下,通过声明的方式定义这个功能要以何种方式何处何时去应用这些功能。横切关注点可以被模块化为特殊的类,这些类被称为切面。这样有两个好处:

  1. 每个关注点都集中于一个地方,而不是分散到多出代码中;
  2. 业务逻辑模块更加的简洁;

2、AOP 术语

2.1、通知(Advice)

在AOP编程中,切面的工作被称为通知,通知定义了切面是什么以及何时使用,除了描述切面要完成的工作,通知还解决了何时执行。在Spring AOP编程中,通知有以下五种类型:

  1. 前置通知(Before):在目标方法被调用之前调用通知功能。
  2. 后置通知(After):在目标方法完成之后调用通知通能,不会关心方法的输出结果
  3. 返回通知(After-returning):在目标方法完成之后调用通知通能;
  4. 异常通知(After-throwing):在目标方法跑出异常后调用的通知;
  5. 环绕通知(Around):通知包裹了被通知的方法,在被通知方法调用之前和调用之后执行自定义的行为。

2.2、连接点(Join point)

应用中可能存在许许多多的地方应用通知,这些地方被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以使调用方法时、抛出异常时。切面代码可以利用这些点插入到应用的正常流程中,并添加新的行为。简而言之,连接点就是程序中可以被增强的方法。

2.3、切点(Pointcut)

一个切面并不需要通知应用的所有连接点,切点有助于缩小切面所通知的连接点范围。通知定义了切面的什么何时,切点就定义了何处。切点的定义会匹配通知所要织入的一个或多个连接点。简而言之,切点就是实际开发过程中被实际增强的方法。

2.3.1、编写切点

为了阐述Spring的切面,我们需要定义切面中的切点。为此,定义一个Performance类:

package com.aop;

public class Performance {
    public void perform() {
        System.out.println("ready to perform ... ");
    }
}

如果此时想要编写的Performance类中的perform()方法触发通知,需要编写一个切点的表达式,这个表达式能够设置当perform()方法执行时触发通知的调用。

execution(* com.aop.Performance.perform(..))

我们使用execution()指示器选择Performnaceperform()方法,**方法的表达式以 “*”号开始,表明了方法的返回值为任意类型。然后指定了perform()方法所属类的全限定类名以及方法名称。在方法参数中,使用(..)来表示任意的参数。**

2.4、切面(Aspect)

切面是通知和切点的集合,通知和切点共同定义了切面的全部内容。

2.5、引入(Introduction)

引入就是类层面的增强,它允许我们向现有的类添加新方法或属性。

2.6、织入(Weaving)

织入就是把切面应用到目标对象并创建新的代理对象的过程。

3、Spring 对 AOP 编程的支持

目前,Spring提供了简洁和干净的面向切面编程方式。引入了简单的声明式AOP和基于注解的AOP。通过Spring的aop命名空间,可以将纯粹的POJO转换为切面。

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

如下图所示:此时皇帝类作为被代理的类,此时皇帝需要妃子侍寝这业务需求,通过告知代理类该项业务逻辑,代理类增强侍寝业务发生前和发生后的功能,如在是侍寝前通知妃子需要沐浴、化妆之类的其他事情。然后接妃子到皇帝处,皇帝执行侍寝业务需求。业务逻辑执行完结,被代理类皇帝告知代理类太监侍寝结束,将妃子送回原处。在该程序中,皇帝侍寝业务逻辑为核心业务逻辑,其他的有关侍寝业务逻辑的功能和方法交由代理类来完成。在被代理类执行业务逻辑前,先执行代理类的方法。

3.1、基于简单声明式的AOP编程

如果不希望在编写的源代码中将AspectJ注解带入到代码中,可以使用Spring XML配置文件的方式声明切面。

3.1.1、Spring的AOP配置元素

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

3.1.2、编写通知

package com.aop;

public class Audience {

    public void silenceCellPhones() {
        System.out.println("Please, silencing cell phones ...");
    }

    public void takeSeats() {
        System.out.println("Please, Taking seats ...");
    }

    public void applause() {
        System.out.println("CLAP CLAP CLAP ...");
    }

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

此时,Audience类并没有任何特别之处,就是一个含有几个方法的普通Java类。通过在Spring的applicationContext.xml文件上配置,将Audience类注册为Spring应用上下文中的bean,然后在切点中选择bean,就可以让Audience称为AOP的通知类。

1、注册Spring应用上下文
<?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"
    xmlns:tx="http://www.springframework.org/schema/tx"
    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
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx.xsd">

    <context:annotation-config/>
    <bean id="audience" class="com.aop.Audience"></bean>
    <bean id="performance" class="com.aop.Performance"></bean>
</beans>
2、在切点中选择bean

关于Spring中的AOP配置元素,第一个需要注意的事项是大多数的AOP配置元素必须在<aop:config>元素的上下文内使用。

<?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"
    xmlns:tx="http://www.springframework.org/schema/tx"
    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
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx.xsd">

<context:annotation-config/>

    <bean id="audience" class="com.aop.Audience"></bean>
    <bean id="performance" class="com.aop.Performance"></bean>
    <aop:config>
        <!--
            定义切点:
                配置需要进行增强的方法
         -->
        <aop:pointcut expression="execution(* com.aop.Performance.perform(..))" id="enhancerPerform" />
    </aop:config>
</beans>

3.1.3、声明前置、后置和异常通知

<aop:config>元素中,我们可以声明一个或多个通知器、切面或者切点。

<aop:config>
        <!--
            定义切点:
                配置需要进行增强的方法
         -->
    <aop:pointcut expression="execution(* com.day02.aop.Performance.perform(..))" id="enhancerPerform" />
    <aop:aspect ref="audience">
            <!-- 定义前置通知 -->
        <aop:before method="silenceCellPhones" pointcut="execution(* com.day02.aop.Performance.perform(..))"/>
        <aop:before method="takeSeats" pointcut="execution(* com.day02.aop.Performance.perform(..))"/>
            <!-- 定义返回通知 -->
        <aop:after-returning method="applause" pointcut="execution(* com.day02.aop.Performance.perform(..))"/>
            <!-- 定义异常通知 -->
        <aop:after-throwing method="demandRefund" pointcut="execution(* com.day02.aop.Performance.perform(..))"/>
    </aop:aspect>
</aop:config>

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

3.1.4、声明环绕通知

相对于前置和后置通知,环绕通知可以完成前置通知和后置通知所实现的相同功能,而且只需要在一个方法中实现。因为整个通知逻辑是在一个方法内实现的。

Audience类中添加watchPerformance()方法,在该方法中实现了在观众进场前关闭手机和表演技术后鼓掌的功能需求。

package com.day02.aop;

import org.aspectj.lang.ProceedingJoinPoint;

public class Audience {

    public void watchPerformance(ProceedingJoinPoint joinPoint) {
        try {
            // 完成前置通知的功能
            silenceCellPhones();
            takeSeats();
            joinPoint.proceed();
            // 完成后置通知的功能
            applause();
        } catch (Throwable e) {
            // 实现异常通知的功能
            demandRefund();
        }
    }
}

可以看到,这个通知所达到的效果与之前的前置通知、后置通知和异常通知配合使用的效果是一样的,但是通过使用环绕通知,可以让他们位于同一个方法中。关于环绕通知,在通知中,接受了ProceedingJoinPoint类型的参数,该类型的对象可以在通知中来调用被通知的方法,通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,调用ProceedingJoinPointproceed()方法即可。

在观众的切面中,watchPerformance()方法包含了之前四个通知方法的所有功能。不过,所有的方法功能都放在一个方法中,因此这个方法还要负责自身的异常处理。声明环绕通知与声明其他类型的通知并没有太大的区别。我们所需要做的仅仅是使用<aop:around>元素。

<aop:config>
    <!-- 定义切点: 配置需要进行增强的方法 -->
    <aop:pointcut expression="execution(* com.day02.aop.Performance.perform(..))" id="enhancerPerform" />

    <aop:aspect ref="audience">
        <aop:around method="watchPerformance" pointcut-ref="enhancerPerform"/>
    </aop:aspect>
</aop:config>

像其他通知的XML元素一样,<aop:around>指定了一个切点和一个通知方法的名字,为该切点所设置的method属性值为watchPerformance()方法。

3.1.5、为通知传递参数

在以上的案例中,切面都相对简单,除了环绕通知以外,所编写的通知中都没有任何的参数。但是如果切面所通知的方法确实有参数,切面能访问和使用传递给被通知方法的参数吗?

<?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" xmlns:tx="http://www.springframework.org/schema/tx"
    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
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx.xsd">

    <context:annotation-config />
    <!-- 启用@AspectJ注解驱动的切面 -->
    <aop:aspectj-autoproxy />

    <bean id="blankDiscAspectJ" class="com.aop.config.BlankDiscAspectJ"></bean>
    <bean id="blankDisc" class="com.aop.config.BlankDisc">
        <property name="discTitle" value="张学友怀旧金曲"/>
        <property name="artist" value="张学友"/>
        <property name="tracks">
            <list>
                <value>爱你痛到不知痛</value>
                <value>如果这都不算爱</value>
                <value>寂寞的男人</value>
                <value>结束不是我要的结果</value>
                <value>你说的</value>
            </list>
        </property>
    </bean>
    <aop:config>
        <aop:aspect ref="blankDiscAspectJ">
            <aop:pointcut expression="execution(* com.aop.config.BlankDisc.playTrackById(int)) and args(trackId)" id="playByTrackId"/>
            <aop:after-returning method="PlayTrack" pointcut-ref="playByTrackId"/>
        </aop:aspect>
    </aop:config>
</beans>

程序清单:BlankDisc类

package com.aop.config;

import java.util.List;

public class BlankDisc {
    private List<String> tracks;

    public List<String> getTracks() {
        return tracks;
    }
    public void setTracks(List<String> tracks) {
        this.tracks = tracks;
    }

    public void playTrackById(int trackId) {
        System.out.println("请播放曲目:" + trackId);
    }
}

BlankDisc类中存在playTrackById(int trackId)方法,通过传入的参数来选播放磁带中指定下表的歌曲,可以看到我们使用了和前面相同的AOP命名空间XML元素,他们会将POJO声明为切面,但是唯一的差别是在于切点的表达式中包含了一个参数。通过and args(参数名称)这个参数会传递到通知的方法中。

<aop:pointcut expression="execution(* com.aop.config.BlankDisc.playTrackById(int)) and args(trackId)" id="playByTrackId"/>

以上的配置,可以将被通知方法中的trackId参数传递到切面方法中。

程序清单:BlankDiscAspectJ类,接收被通知方法中的参数

package com.aop.config;

import javax.annotation.Resource;

import org.aspectj.lang.annotation.Aspect;

@Aspect
public class BlankDiscAspectJ {

    @Resource(name = "blankDisc")
    private BlankDisc blankDisc;

    public void PlayTrack(int trackId) {
        int totalTracks = blankDisc.getTracks().size();
        if (trackId < 0) {
            System.out.println("输入的序号不合法 ... ");
        } else {
            if (trackId > totalTracks) {
                System.out.println("输入的序号不合法 ... ");
            } else {
                System.out.println("正在播放曲目:《" + blankDisc.getTracks().get(trackId - 1) + "》");
            }
        }
    }
}

程序清单:测试获取被通知方法中的参数

package com.aop.config;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestBlankDisc {

    @Resource(name="blankDisc")
    private BlankDisc blankDisc;

    @Test
    public void testDemo01() {
        blankDisc.playTrackById(3);
        // 运行结果:
        // 请播放曲目:3
        // 正在播放曲目:《寂寞的男人》
    }
}

3.2、基于注解的AOP编程

使用注解来进行Spring AOP编程是AspectJ引入的关键特性。AspectJ面向注解的模型可以非常简便的通过少量注解把任意类转换为切面。

3.2.1、编写切面

如果需要使用注解的方式开发AOP,那么首先需要在Spring的配置文件中启用@AspectJ注解驱动的切面

<!-- 使用@Aspect注解驱动的切面 -->
<aop:aspectj-autoproxy />

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

程序清单:Performance类:演出类

package com.aop;

public class Performance {

    public void perform() {
        System.out.println("演出开始 ...");
    }
}

程序清单:AudienceAnno类:观众观看演出的切面

package com.aop.config;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class AudienceAnno {

    // 前置通知——表演之前
    @Before("execution(* com.aop.Performance.perform(..))")
    public void silenceCellPhones() {
        System.out.println("将手机调至静音...");
    }
    // 前置通知——表演之前
    @Before("execution(* com.aop.Performance.perform(..))")
    public void takeSeats() {
        System.out.println("请按座位号入座...");
    }
    // 返回通知——表演之后
    @AfterReturning("execution(* com.aop.Performance.perform(..))")
    public void applause() {
        System.out.println("节目表演精彩,鼓掌...");
    }
    // 异常通知——表演失败之前
    @AfterReturning("execution(* com.aop.Performance.perform(..))")
    public void demandRefund() {
        System.out.println("演出出现问题,赶紧退票...");
    }
}

AudienceAnno类使用@Aspect注解进行了标注,该注解表明AudienceAnno不仅仅是一个POJO,同时还是一个切面。AudienceAnno类中的方法都使用注解来定义切面的具体行为。

AudienceAnno有四个方法,分别定义了一个观众在观看演出时可能会做出的事情:

  1. silenceCellPhones():演出之前,观众将手机静音;
  2. takeSeats():演出之前,观众就座;
  3. applause():演出之后,如果演出精彩,观众进行鼓掌喝彩;
  4. demandRefund():演出出现异常,没有达到观众预期,观众要求退票;

3.2.2、Spring中AspectJ注解类型

在AspectJ中,提供了以下五个注解来定义通知:

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

AudienceAnno使用了前面五个注解中的三个。takeSeats()silenceCellPhones()都用到了@Before注解,表明他们应该在演出开始之前调用。applause()方法使用了@AfterReturning注解,它会在演出成功返回后调用。demandRefund()方法添加了@AfterThrowing注解,这表明它会在抛出异常以后执行。

3.2.3、使用@Pointcut注解声明切点表达式

AudienceAnno中所有的注解都给定了一个切点表达式作为它的值,同时,这四个方法的切点表示都相同。但是在实际的开发中,可以设置成不同的切点表达式。但是在小项目中,会发现只要Performance类中的perform()方法执行时就会触发并调用AudienceAnno类中的相关方法即通知。发现在AudienceAnno中相同的切点表达式使用了四次,在实际开发过程中,可以通过使用@Pointcut注解来定义切点,然后每次需要的时候就引用该切点。

程序清单:通过@Pointcut注解声明频繁使用的切点表达式

package com.aop.config;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AudienceAnno {

    @Pointcut("execution(* com.aop.Performance.perform(..))")
    public void performance() {}

    // 前置通知——表演之前
    @Before("performance()")
    public void silenceCellPhones() {
        System.out.println("前置通知---将手机调至静音...");
    }
    // 前置通知——表演之前
    @Before("performance()")
    public void takeSeats() {
        System.out.println("前置通知---请按座位号入座...");
    }
    // 返回通知——表演之后
    @AfterReturning("performance()")
    public void applause() {
        System.out.println("返回通知---节目表演精彩,鼓掌...");
    }
    // 异常通知——表演失败之前
    @AfterThrowing("performance()")
    public void demandRefund() {
        System.out.println("异常通知---演出出现问题,赶紧退票...");
    }
}

AudienceAnno中,performance()方法使用了@Pointcut注解,将其设置称为一个切点表达式。该方法中的实际内容并不重要,实际上,该方法本身只是一个标识,供@Pointcut注解依附。需要注意的是:除了注解和没有实际操作意义的performance()方法以外,AudienceAnno类依然是一个POJO,我们能够像使用其他Java类那样调用它的方法。

3.2.4、创建环绕通知

环绕通知能够让所要执行的业务逻辑被通知的目标方法完全包裹起来,实际上就像在一个通知方法中同时编写前置通知和后置通知。

程序清单:AudienceAnno类,创建环绕通知

package com.aop.config;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AudienceAnno {

    @Pointcut("execution(* com.aop.config.Performance.perform(..))")
    public void performance() {
    }

    /**
     * 之前的通知方法略
     */

    // 环绕通知方法
    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint) {
        try {
            // 调用之前前置通知
            takeSeats();
            silenceCellPhones();
            joinPoint.proceed();
            // 调用之前的返回通知
            applause();
        } catch (Throwable e) {
            // 调用之前的异常通知
            demandRefund();
        }
    }
}

在这里使用了@Around注解,该注解表明了watchPerformance()方法会作为performance()切点的环绕通知。通过接受ProceedingJoinPoint类型作为参数,通过该类型对象的proceed()方法,来调用被通知的方法。

3.2.5、处理通知中的参数

除去环绕通知,之前编写的通知方法中都没有去关注方法参数的情况。如何将切面所通知方法中的参数传递给切面呢?如以下程序清单提供的案例:

程序清单:BlankDisc类:

package com.aop.config;

import java.util.List;

public class BlankDisc {

    private List<String> tracks;

    public List<String> getTracks() {
        return tracks;
    }
    public void setTracks(List<String> tracks) {
        this.tracks = tracks;
    }

    public void playTrackById(int trackId) {
        System.out.println("请播放曲目:" + trackId);
    }
}

BlankDisc类中的playTrackById()方法,在一个磁带中有许多歌曲,将歌曲的序号作为参数传递给该方法中,然后播放磁带中具有指定下标位置的歌曲。

程序清单:BlankDiscAspectJ类,接收被通知方法中的参数

package com.aop.config;

import javax.annotation.Resource;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class BlankDiscAspectJ {

    @Resource(name="blankDisc")
    private BlankDisc blankDisc;

    @Pointcut("execution (* com.aop.config.BlankDisc.playTrackById(int)) && args(trackId)")
    public void playTrackById(int trackId){}

    @AfterReturning("playTrackById(trackId)")
    public void beforePlayTrack(int trackId) {
        int totalTracks = blankDisc.getTracks().size();
        if (trackId < 0) {
            System.out.println("输入的序号不合法 ... ");
        } else {
            if (trackId > totalTracks) {
                System.out.println("输入的序号不合法 ... ");
            } else {
                System.out.println("正在播放曲目:《"+blankDisc.getTracks().get(trackId -1 )+"》");
            }
        }
    }
}

与之前所创建的切面一样,这个切面使用@Pointcut注解定义明明的切点,并使用@AfterReturning将一个方法声明为返回通知。但是这里不同的地方在与切点还声明了要提供给通知方法的参数。以下图将切面的表单时进行了分解:

在切点表达式中args(trackId)限定符表明传递给playTrackById()方法的int类型参数也会传递到通知中去,参数的名称trackId也与切点方法签名中的参数相匹配。这个参数会传递到通知方法中,这个通知方法是通过@AfterReturning注解和命名切点playTrackById(trackId)定义的。切点定义中的参数与切点方法中的参数名称是一样的,这就完成了从命名切点盗通知方法参数转移。

程序清单:在applicationContext.xml中配置BlankDisc和BlankDiscAspectJ类

<?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" xmlns:tx="http://www.springframework.org/schema/tx"
    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
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx.xsd">

    <context:annotation-config />
    <!-- 启用@AspectJ注解驱动的切面 -->
    <aop:aspectj-autoproxy />

    <bean id="blankDiscAspectJ" class="com.aop.config.BlankDiscAspectJ"></bean>
    <bean id="blankDisc" class="com.aop.config.BlankDisc">
        <property name="discTitle" value="张学友怀旧金曲"/>
        <property name="artist" value="张学友"/>
        <property name="tracks">
            <list>
                <value>爱你痛到不知痛</value>
                <value>如果这都不算爱</value>
                <value>寂寞的男人</value>
                <value>结束不是我要的结果</value>
                <value>你说的</value>
            </list>
        </property>
    </bean>
</beans>

程序清单:测试BlankDiscAspectJ切面

package com.aop.config;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestBlankDisc {

    @Resource(name="blankDisc")
    private BlankDisc blankDisc;

    @Test
    public void testDemo01() {
        blankDisc.playTrackById(5);
        // 运行结果:
        // 请播放曲目:5
        // 正在播放曲目:《你说的》
    }
}
01-21 22:33