Spring系列(三)Spring AOP详解

1 什么是面向切面编程

在软件开发中,散布于应用中多处的功能被称为 横切关注点(cross-cutting concern)面向切面编程(AOP)解决的问题就是把横切关注点与业务逻辑相分离。

在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而且无需修改受影响的类。

横切关注点可以模块化特殊的类,这些类被称为切面(aspect)。这样做有两个好处:

  • 每个关注点都集中于一个地方,而不是分散到多处代码中;
  • 服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中。

简单来讲,Spring的AOP目的是为了解耦。AOP可以让一组类共享相同的行为。在OOP中只能通过继承类和实现接口,来使代码的耦合度增加,且类继承只能为单继承。

1.1 定义AOP术语

描述切面的常用术语有:

  • 通知:advice
  • 切点:pointcut
  • 连接点:join point

在一个或者多个连接点上,可以把切面的功能(通知)织入到程序的执行过程中。

通知定义了切面是什么以及何时使用。

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

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

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

切点(pointcut)定义了在何处插入切面,切点的定义会匹配通知所要织入的一个或者多个连接点。我们通常使用明确的类和方法名称或者正则表达式定义所匹配的类和方法名称来指定这些切点。

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

引入(Introduction)
引入允许我们向现有的类添加新的方法或属性。

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

编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器 就是以这种方式织入切面的。

类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器 (ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的 加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。

运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。**Spring AOP就是以这种方式织入切面的。 **

1.2 Spring对AOP的支持

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

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

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

借助Spring AOP 的命名空间,我们可以将纯POJO转换为切面。这些POJO只是提供了满足切点条件时所要调用的方法。这种技术需要使用XML配置。

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

Spring在运行时通知对象

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

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

总而言之,Spring的切面由包裹了目标对象的代理类实现。 代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法。

因为Spring基于动态代理,因此Spring只支持方法级别的连接点。

2 通过切点来选择连接点

切点用于准确定位应该在什么地方应用切面的通知。通知和切点是切面的最基本元素。

关于Spring AOP的AspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器(pointcut designator)的一个子集

Spring AOP所支持的AspectJ切点指示器如下所示

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

在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgument-Exception异常。

当我们查看如上所展示的这些Spring支持的指示器时,注意只有execution指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的。

这说明execution指示器是我们在编写切点定义时最主要使用的指示器。在此基础上,我们使用其他指示器来限制所匹配的切点。

2.1 编写切点

为了阐述Spring中的切面,我们需要有个主题来定义切面的切点。为此,我们定义一 个Performance接口:

public interface Performance {
    public void perform();
}

Performance可以代表任何类型的现场表演,如舞台剧、电影或音乐会。假设我们想编写Performance的perform()方法触发的通知。如下的表达式能够设置当perform()方法执行时触发通知的调用:

execution(* com.zjx.aspectj.Performance.perform(..))

解析:使用execution()指示器选择Performance的perform()方法执行时触发通知。方法表达式以“*”开始表示返回任意类型,也就是我们不关心方法返回值的类型;然后,指定全限定类名和方法名。对于方法参数列表(…)表示切点要选择任意的perform()方法,无论入参是什么。

假设我们要匹配的切点仅仅是com包,在此情景下,可以使用within()指示器来限定匹配:

execution(* com.zjx.aspectj.Performance.perform(..) && within(com.*))

这里使用了”&&“操作符把execution()和within()指示器连接在一起,形成与(and)关系(切点必须匹配所有的指示器),还可以使用‘||’操作符来标识或(or)关系,使用‘!’操作符来标识非(not)操作。

2.2 在切点中选择bean

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

execution(* com.zjx.aspectj.Performance.perform(..) and bean('woodstock')

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

在某些场景下,限定切点为指定的bean或许很有意义,但我们还可以使用非操作为除了特定ID 以外的其他bean应用通知:

execution(* com.zjx.aspectj.Performance.perform(..) and !bean('woodstock')

在此场景下,切面的通知会被编织到所有ID不为woodstock的bean中。

3 使用注解创建切面
3.1 定义切面

@AspectJ:表明该类不仅仅是一个POJO,还 是一个切面。

AspectJ提供了五个 注解来定义通知

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

如下定义了一个切面,Audience有四个方法,定义了一个观众在观看演出时可能会做的事情。在演出之前,观众要就坐(takeSeats())并将手机调至静音状态(silenceCellPhones())。如果演出很精彩的话,观众应该会鼓掌喝彩(applause())。不过,如果演出没有达到观众预期的话,观众会 要求退款(demandRefund())。

package com.zjx.aspectj;

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

/**
 * Created by zjx on 2018/6/15.
 */
@Aspect
public class Audience {

    //表演之前手机静音
    @Before("execution(* com.zjx.aspectj.Performance.perform(..))")
    public void silenceCellPhones(){
        System.out.println("silencing cell phones");
    }

    //表演之前就做
    @Before("execution(* com.zjx.aspectj.Performance.perform(..))")
    public void takeSeats(){
        System.out.println("Taking seats");
    }

    //表演成功之后鼓掌
    @AfterReturning("execution(* com.zjx.aspectj.Performance.perform(..)))")
    public void applause(){
        System.out.println("CLAP CLAP CLAP!");
    }

    //表演失败则要求退钱
    @AfterThrowing("execution(* com.zjx.aspectj.Performance.perform(..)))")
    public void demandRefund(){
        System.out.println("Demanding refund");
    }
    
}

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

所有的这些注解都给定了一个切点表达式作为它的值,同时,这四个方法的切点表达式都是相同的。可以使用 @Pointcut 注解在一个@AspectJ切面内定义可重用的切点,如下所示:

package com.zjx.aspectj;

import org.aspectj.lang.annotation.*;

/**
 * Created by zjx on 2018/6/15.
 */
@Aspect
public class AudienceUsePointCut {

    //定义切点表达式
    @Pointcut("execution(* com.zjx.aspectj.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!");
    }

    //表演失败则要求退钱
    @AfterThrowing("performance()")
    public void demandRefund(){
        System.out.println("Demanding refund");
    }

}

此时切面创建成功了的,但是由于没有启用自动代理,即便使用了AspectJ注解, 但它并不会被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理。

有两者方式可以启动自动代理:

  1. JavaConfig方式:在配置类的类级别上通过EnableAspectJAutoProxy注解启用自动代理功能。如下所示:
@EnableAspectJAutoProxy //启用AspectJ自动代理
@Configuration //声明一个配置类
@ComponentScan //启用Spring自动扫描
public class ConcertConfig {
    
    //声明Audience bean
    @Bean
    public Audience audience(){
        return new Audience();
    }

}
  1. 使用XML来装配bean:使用Spring aop命名空间中的 aop:aspectj-autoProxy 元素启动自动代理功能。如下所示:
<?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/spring-aop.xsd
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 定义要扫描的包 -->
    <context:component-scan base-package="com.zjx.aspectj"></context:component-scan>
    <!-- 启用自动代理 -->
    <aop:aspectj-autoproxy/>
    <!-- 声明audience bean -->
    <bean id="audience" class="com.zjx.aspectj.Audience"/>
</beans>

不管使用哪种方式,AspectJ自动代理都会为使用@AspectJ注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。

在这种情况下,将会为Concert bean创建一个代理,Audience类中的通知方法将会在perform()调用前后执行。

使用Spring的AspectJ自动代理创建的切面在本质上依然是Spring基于代理的切面。因此,想要利用AspectJ的所有能力,必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面。

3.2 创建环绕通知

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

如下所示重写了Audience类,它实现的功能和之前一样,但是把四个方法写在一个方法里面了:

package com.zjx.aspectj;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

/**
 * Created by zjx on 2018/6/15.
 */
@Aspect
public class AudienceUseAround {

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

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint jp){

        try {
            //表演之前手机静音
            System.out.println("silencing cell phones");
            //表演之前就做
            System.out.println("Taking seats");
            
            jp.proceed();
            
            //表演成功之后鼓掌
            System.out.println("CLAP CLAP CLAP!");
        } catch (Throwable e) {
            //表演失败则要求退钱
            System.out.println("Demanding refund");
        }

    }

}

它接受了ProceedingJoinPoint作为参数,这个参数是必须要有的,因为你需要在通知中通过它来调用被通知的方法通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用
ProceedingJoinPoint的 proceed()方法。

需要注意的是,别忘记调用proceed()方法。如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。当然,也可以不调用该方法,从而阻塞对被通知方法的访问。

3.3 处理通知中的参数

切面还能够访问和使用传递给被通知方法的参数,如下所示:

  @Pointcut("execution(* com.zjx.aspectj.Performance.perform(int) & args(number))")
  public void performance(int number){}
    
  @Before("performance(number)")
  public void silenceCellPhones(int number){
       ...
    }

    

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

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

3.4 通过注解引入新功能

使用Spring AOP,我们可以为bean引入新的方法。 代理拦截调用并委托给实现该方法的其他对象

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

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

package com.zjx.aspectj;

/**
 * Created by zjx on 2018/6/20.
 */
public interface Encoreable {
    void performance();
}

我们需要有一种方式将这个接口应用到Performance实现中,借助于AOP的引入功能,我们可以不必在设计上妥协或者侵入性地改变现有的实现。为了实现该功能,我们要创建一个新的切面:

package com.zjx.aspectj;

/**
 * Created by zjx on 2018/6/20.
 */

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;

@Aspect
public class EncoreableIntroducer {

    @DeclareParents(value = "com.zjx.aspectj.Performance+",
                    defaultImpl = DefaultEncoreable.class)
    public static Encoreable encoreable;
}

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

@DeclareParents注解由三部分组成:

  • value属性指定了哪种类型的bean要引入该接口。在本例中,也就是所有实现 Performance的类型。(标记符后面的加号表示是Performance的所有子类型,而不 是Performance本身。)

  • defaultImpl属性指定了为引入功能提供实现的类。在这里,我们指定的 是DefaultEncoreable提供实现。

  • @DeclareParents注解所标注的静态属性指明了要引入了接口。在这里,我们所引入 的是Encoreable接口。

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

 <bean class="com.zjx.aspectj.EncoreableIntroducer"></bean>
4. 在XML中声明切面

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

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

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

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

我们重新看一下Audience类,这一次我们将它所有的AspectJ注解全部移除掉:

public class AudienceUseXml {
    //表演之前手机静音
    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 refund");
    }
}

我们可以使用Spring aop 命名空间中的一些元素,将没有注解的Audience类转换为切面:

<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="audience" class="com.zjx.aspectj.AudienceUseXml"></bean>
    <aop:config>
        <!--引用audience bean -->
        <aop:aspect ref="audience">
            <!-- 定义前置通知,表演之前手机静音-->
            <aop:before pointcut="execution(* com.zjx.aspectj.Performance.perform(..))" method="silenceCellPhones"/>

            <!-- 定义前置通知,表演之前就坐-->
            <aop:before method="takeSeats" pointcut="execution(* com.zjx.aspectj.Performance.perform(..))"/>

            <!-- 定义返回通知,表演成功之后鼓掌-->
            <aop:after-returning method="applause" pointcut="execution(* com.zjx.aspectj.Performance.perform(..))"/>

            <!-- 定义异常通知,表演失败之后退钱-->
            <aop:after-throwing method="demandRefund" pointcut="execution(* com.zjx.aspectj.Performance.perform(..))"/>
        </aop:aspect>

    </aop:config>
</beans>

关于Spring AOP配置元素,第一个需要注意的是大多数AOP配置元素必须在aop:config元素的上下文应用。这条规则有几个例外的场景,但是把一个bean声明为一个切面时我们总是从aop:config中配置的。

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

ref元素引用了一个POJO bean,该bean实现了切面的功能,提供了在切面中通知所调用的方法。

在所有的通知元素中,pointcut属性定义了通知所应用的切点,它的值是使用AspectJ切点表达式语法所定义的切点。

使用aop:pointcut元素将通用的切点表达式抽取到一个切点声明中:

<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="audience" class="com.zjx.aspectj.AudienceUseXml"></bean>
    <aop:config>
        <!--引用audience bean -->
        <aop:aspect ref="audience">

            <!-- 定义通用切点 -->
            <aop:pointcut id="performance" expression="execution(* com.zjx.aspectj.Performance.perform(..))"/>

            <!-- 定义前置通知,表演之前手机静音-->
            <aop:before pointcut-ref="performance"  method="silenceCellPhones"/>

            <!-- 定义前置通知,表演之前就坐-->
            <aop:before method="takeSeats" pointcut-ref="performance"/>

            <!-- 定义返回通知,表演成功之后鼓掌-->
            <aop:after-returning method="applause" pointcut-ref="performance"/>

            <!-- 定义异常通知,表演失败之后退钱-->
            <aop:after-throwing method="demandRefund" pointcut-ref="performance"/>
        </aop:aspect>

    </aop:config>
</beans>

现在切点是在一个地方定义的,并且被多个通知元素所引用。aop:pointcut元素定义了一个id为performance的切点。同时修改所有的通知元素,用pointcut-ref属性来引用这个命名切点。

如果想让定义的切点能够在多个切面使用,我们可以把aop:pointcut元素放在aop:config元素的范围内。

4.2 声明环绕通知

使用环绕通知,我们可以完成 前置通知和后置通知所实现的相同功能,而且只需要在一个方法中 实现。如下所示watchPerformance()方法提供了AOP环绕通知:

public class AudienceUseAroundXml {

    public void watchPerformance(ProceedingJoinPoint jp){

        try {
            //表演之前手机静音
            System.out.println("silencing cell phones");
            //表演之前就做
            System.out.println("Taking seats");

            jp.proceed();

            //表演成功之后鼓掌
            System.out.println("CLAP CLAP CLAP!");
        } catch (Throwable e) {
            //表演失败则要求退钱
            System.out.println("Demanding refund");
        }
    }

}

声明环绕通知与声明其他类型的通知并没有太大区别。我们所需要做的仅仅是使 用aop:around元素。

    <bean id="audience" class="com.zjx.aspectj.AudienceUseAroundXml"></bean>
    <aop:config>
        <!--引用audience bean -->
        <aop:aspect ref="audience">

            <!-- 定义通用切点 -->
            <aop:pointcut id="performance" expression="execution(* com.zjx.aspectj.Performance.perform(..))"/>

            <!-- 定义前置通知,表演之前手机静音-->
            <aop:around pointcut-ref="performance"  method="watchPerformance"/>

        </aop:aspect>

    </aop:config>
4.3 为通知传递参数
    <bean id="audience" class="com.zjx.aspectj.Audience"></bean>
    
    <aop:config>
        <!--引用audience bean -->
        <aop:aspect ref="audience">

            <!-- 定义通用切点 -->
            <aop:pointcut id="performance" expression="execution(* com.zjx.aspectj.Performance.perform(int) 
            and args(number)"/>

            <!-- 定义前置通知,表演之前手机静音-->
            <aop:before pointcut-ref="performance"  method="silenceCellPhones"/>

        </aop:aspect>

    </aop:config>

我们使用了和前面相同的aop命名空间XML元素,它们会将POJO声明为切面。唯一明显的差别在于切点表达式中包含了一个参数,这个参数会传递到通知方法中。这个表达式与之前通过Java配置的表达式几乎是相同的。唯一的差别在于这里使用and关键字而不是“&&”(因为在XML中,“&”符号会被解析为实体的开始)。

4.4 通过切面引入新的功能

使用Spring aop命名空间中的aop:declare-parents元素,我们可以实现与@DeclareParents注解相同的功能。

    <aop:config>
        <!--引用audience bean -->
        <aop:aspect >
            <aop:declare-parents
                    types-matching="com.zjx.aspectj.Performance+"
                    implement-interface="com.zjx.aspectj.Encoreable"
                    default-impl="com.zjx.aspectj.DefaultEncoreable"/>
        </aop:aspect>

    </aop:config>

aop:declare-parents元素声明了此切面所通知的bean要在它的对象层次结构中拥有新的父类型。

types-matching属性指定要通知的bean,+表示该接口的实现类

implement-interface属性指定bean要增加的父类接口

default-impl属性指定父类接口的实现类(也可以使用delegate-ref属性来标识)。

5. 注入AspectJ切面

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

如果在执行通知时,切面依赖于一个或多个类,我们可以在切面内部实例化这些协作的对象。但更好的方式是,我们可以借助Spring的依赖注入把bean装配进AspectJ切面中。

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

public aspect CriticAspect {    
    private Worker worker;

    public Audience(){}

    //通过setter方法注入
    public void setWorker(Worker worker){
        this.worker = worker;
        System.out.println("工作人员已入场");
    }

    //定义piano构造器切点和后置通知
    pointcut piano():execution(concert.PianoPerform.new());
    after():piano(){
        worker.sendMsg("钢琴");
    }


    //定义不带参数方法切点和前置通知
    pointcut perform():execution(* concert.Performance.perform());
    before():perform(){
        worker.take();
    }

    //定义带两个参数的切点和后置通知
    pointcut finishPerform(String performer, String title):execution(* concert.Performance.finishPerform(String, String)) && args(performer, title);
    after(String performer, String title):finishPerform(performer, title){
        worker.broadcast(performer, title);
    }
}

AspectJ切面根本不需要Spring就可以织入到我们的 应用中。如果想使用Spring的依赖注入为AspectJ切面注入协作者,那我们就需要在Spring配置 中把切面声明为一个Spring配置中的。如下的声明会把 criticismEnginebean注入到CriticAspect中:

很大程度上,的声明与我们在Spring中所看到的其他配置并没有太多的区别,但是最大的不同在于使用了factory-method属性。通常情况下,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不能像之前那样使用声明来创建一个CriticAspect实例——它已经在运行时由AspectJ创建完成了。Spring需要通过aspectOf()工厂方法获得切面的引用,然后像元素规定的那样在该对象上执行依赖注入。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值