Spring——面向切面编程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011024652/article/details/80158045

本文主要依据《Spring实战》第四章内容进行总结

1、面向切面编程术语

1.1、横切关注点

散布于应用中多处的功能被称为横切关注点,通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑中),把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。AOP可以实现横切关注点与它们所影响的对象之间的解耦。

横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect),这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。

1.2、通知(Advice)

切面的工作被称为通知,通知定义了切面是什么以及何时使用。Spring切面可以应用5种类型的通知:

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

1.3、连接点(Join point)

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

1.4、切点(Pointcut)

一个切面并不需要通知应用的所有连接点,切点有助于缩小切面所通知的连接点的范围。如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。切点的定义会匹配通知所要织入的一个或多个连接点,我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。

1.5、切面(Aspect)

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

1.6、引入(Introduction)

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

1.7、织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中。

1.8、Spring对AOP的支持

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

  • 基于代理的经典Spring AOP;
  • 纯POJO切面;
  • @AspectJ注解驱动的切面;
  • 注入式AspectJ切面

Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截,也就是说,Spring只支持方法级别的连接点。

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

2、定义切点

切点用于准确定位应该在什么地方应用切面的通知,在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。

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

只有excution()指示器是实际执行匹配的,而其他指示器都是用来限制匹配的。

2.1、编写切点

我们定义一个Performance接口:

package concert;

public interface Performance {
    public void perform();
}

假设我们想编写Performance的perform()方法触发的通知,我们定义如下的切点:

execution(* concert.Performance.perform(..))

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

假如我们需要配置的切点仅匹配concert包,在此场景下,可以使用within()指示器来限制匹配,例如:

execution(* concert.Performance.perform(..)) && within(concert.*)

在这里我们使用了“&&”操作符把execution()和within()指示器连接在一起形成了与关系,我们也可以使用“||”操作符来标识或关系,使用“!”操作符来标识非操作。

因为“&”在XML中有特殊含义,所以在Spring的XML配置里描述切点时,我们可以使用and来代替“&&”,同样,or和not可以分别用来代替“||”和“!”。

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

execution(* concert.Performance.perform()) and bean(‘woodstock’)

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

3、使用注解创建切面

3.1、使用注解创建切面

我们定义如下一个Audience类,它定义了一个切面:

@Aspect
public class Audience {

    @Before("execution(** concert.Performance.perform(..))")
    public void silenceCellPhones() {
        System.out.println("请关闭手机");
    }

    @Before("execution(** concert.Performance.perform(..))")
    public void takeSeats() {
        System.out.println("请入座");
    }

    @AfterReturning("execution(** concert.Performance.perform(..))")
    public void applause() {
        System.out.println("热烈鼓掌");
    }

    @AfterThrowing("execution(** concert.Performance.perform(..))")
    public void demandRefund() {
        System.out.println("要求退款");
    }

}

可以看到,Audience类使用了@Aspect注解进行标注,@Aspect注解用来表明这个类不仅是一个POJO,还是一个切面。这个类有四个方法,定义了一个观众在观看表演时可能会做的事,在演出之前需要入座(taksSeats)、关闭手机(silenceCellPhones),演出很精彩的话,观众会鼓掌喝彩(applause),如果演出没有达到预期效果,观众会要求退款(demandRefund)。这些方法都使用了通知注解表明它们应该在什么时候调用,AspectJ提供了五个注解来定义通知:

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

在上面这个类里,每个通知注解都给定了一个切点表达式作为它的值,但是可以注意到,它们的切点表达式是相同的,也就是说相同的切点表达式在一个切面中定义了多次,这不是一个很好的方案。我们可以使用@Pointcut注解来定义一个可重用的切点:

@Aspect
public class Audience {

    @Pointcut("execution(** concert.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("要求退款");
    }

}

可以看到,@Pointcut注解设置的值是一个切点表达式,在performance()方法上添加@Pointcut注解实际上是扩展了切点表达式语言,performance()方法的实际内容并不重要,在这里它实际上是空的,该方法只是一个标识,供@Pointcut注解依附。

在前面的例子中,我们使用@Aspect将Audience类声明为一个切面,但是如果要在Spring中使用这个切面,还需要将其装配为Spring中的bean:

@Aspect
@Component
public class Audience {
    ……
}

在这里,我们使用@Component注解将Audience声明为Spring中的一个bean,但是要注意的是,这样配置也只会在Spring容器中声明一个bean,即使它使用了@Aspect注解,它也不会被视为切面。这些注解不会生效,需要配置使切面生效。

如果使用JavaConfig配置Spring,可以在配置类的类级别上通过使用@EnableAspectJAutoProxy注解启动自动代理功能:

@Configuration
@ComponentScan(basePackageClasses={Audience.class, Performance.class})
@EnableAspectJAutoProxy
public class AspectConfig {
}

如果是使用XML来装配bean的话,那么就需要使用Spring aop命名空间中的<aop:aspectj-autoproxy> :

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

    <context:component-scan base-package="concert,aspect" />

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


</beans>

3.1、创建环绕通知

环绕通知是最为强大的通知类型,它能够让你所编写的逻辑将被通知的目标方法完全包装起来,实际上就像在一个通知方法中同时编写前置通知和后置通知。例如我们可以使用环绕通知代替之前多个不同的前置通知和后置通知:

@Aspect
public class AroundAudience {

    @Pointcut("execution(** concert.Performance.perform(..))")
    public void performance() {

    }

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint jp) {  
        try {
            System.out.println("请关闭手机");
            System.out.println("请入座");
            jp.proceed();
            System.out.println("热烈鼓掌");
        } catch (Throwable e) {
            System.out.println("要求退款");
        }   
    }

}

在这里,@Around注解表明watchPerformance()方法会作为performance()切点的环绕通知,在这个通知中,观众在演出之前关闭手机并就坐,演出结束后会鼓掌喝彩,像前面一样,如果演出失败的话,观众会要求退款。

watchPerformance()这个通知方法会接受ProceedingJoinPoint作为参数,这个对象是必须要有的,因为要在通知中通过它来调用被通知的方法,通知方法中可以做任何事情,当要将控制权交给被通知方法时,它需要调用ProceedingJoinPoint的proceed()方法,如果不调用这个方法的话,实际上会阻塞被通知方法的调用。

3.2、处理通知中的参数

目前我们所写的通知方法不需要关注传递给被通知方法的任意参数,但是,如果切面所通知的方法确实有参数该怎么办呢?切面能访问和使用传递给被通知方法的参数吗?

我们可以通过以下实例来讲解如何处理通知中的参数,我们定义一个BlankDisc类,在这个类中通过playTrack()方法来播放指定磁道中的歌曲:

public class BlankDisc {
    private String title;
    private String artist;
    private List<String> tracks;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getArtist() {
        return artist;
    }

    public void setArtist(String artist) {
        this.artist = artist;
    }

    public List<String> getTracks() {
        return tracks;
    }

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

    public void playTrack(int trackNum) {
        System.out.println(tracks.get(trackNum - 1));
    }
}

现在我们想记录每个磁道的被播放次数,一种方法是修改playTrack()方法,直接在每次调用的时候记录播放次数,但是记录播放次数和播放是不同的关注点,因此可以使用切面来完成,所以我们创建TrackCounter类,它是通知playTrack()方法的一个切面:

@Aspect
public class TrackCounter {

    private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();

    @Pointcut("execution(* concert.BlankDisc.playTrack(int)) && args(trackNum)")
    public void trackPlayed(int trackNum) {
    }

    @Before("trackPlayed(trackNum)")
    public void countTrack(int trackNum) {
        int currenttCount = getPlayCount(trackNum);
        trackCounts.put(trackNum, currenttCount + 1);
    }

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

在这里,我们使用@Pointcut声明了一个切点,在这个切点声明中,除了使用execution()表达式来指定匹配的执行方法,我们还使用了args()表达式来表明传递给playTrack()方法的int类型参数也会传递到通知中去,参数的名称也与切点方法签名中的参数相匹配,同时,切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法参数的转移。通俗点说,就是args()限定符中的参数名称、切点定义中的参数名称和切点方法中的参数名称三者需要统一,这样就能使被通知方法的实参传递给通知方法。接下来我们就来验证一下,我们首先将这两个bean声明为Spring中的bean:

@Configuration
@EnableAspectJAutoProxy
public class AspectConfig {
    @Bean
    public BlankDisc blankDisc() {
        BlankDisc cd = new BlankDisc();

        cd.setTitle("音乐名称");
        cd.setArtist("音乐作者");
        List<String> tracks = new ArrayList<String>();
        tracks.add("音乐1");
        tracks.add("音乐2");
        tracks.add("音乐3");
        tracks.add("音乐4");
        tracks.add("音乐5");
        tracks.add("音乐6");
        cd.setTracks(tracks);

        return cd;
    }

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

在这里我们默认为BlankDisc添加了几首歌曲,接下来使用JUnit进行测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=AspectConfig.class)
public class AspectTest {
    @Autowired
    private BlankDisc cd;

    @Autowired
    private TrackCounter t;

    @Test
    public void test() {
        cd.playTrack(1);
        cd.playTrack(1);
        cd.playTrack(3);
        cd.playTrack(4);
        cd.playTrack(4);
        cd.playTrack(4);
        cd.playTrack(5);
        cd.playTrack(6);
        cd.playTrack(6);

        for(String s : cd.getTracks()) {
            System.out.println(s + "播放" + t.getPlayCount(cd.getTracks().indexOf(s) + 1) + "次");
        }
    }

}

执行测试方法,运行结果如下:

音乐1
音乐1
音乐3
音乐4
音乐4
音乐4
音乐5
音乐6
音乐6
音乐1播放2次
音乐2播放0次
音乐3播放1次
音乐4播放3次
音乐5播放1次
音乐6播放2次

可以看到,TrackCounter是能够正确记录相应磁道被播放的次数的。

3.3、通过注解引入新功能

在Spring中,切面只是实现了它们所包装bean相同接口的代理,如果除了实现这些接口,代理也能暴露新接口的话,会怎么样呢?那样的话,切面所通知bean看起来像是实现了新的接口,即便底层实现类并没有实现这些接口也无所谓。当引入接口的方法被调用时,代理会把此类调用委托给实现了新接口的某个其他对象,实际上,一个bean的实现被拆分到多个类中。我们看一下下面这个例子,我们定义一个接口Encoreable:

public interface Encoreable {
    public void performEncore();
}

这个接口定义了一个返场演出的方法,接下来我们在切面中新增一段代码:

    @DeclareParents(value="concert.Performance",defaultImpl=DefaultEncoreable.class)
    public static Encoreable encoreable;

这段代码将@DeclareParents注解将Encoreable接口引入到Performance bean中,@DeclareParents注解由三部分组成:

  • value属性指定了那种类型的bean要引入该接口,在这里我们使用的是concert.Performance,我们也可以使用concert.Performance+,在这里加号表示是Performance的所有子类型,而不是Performance本身。
  • defaultImpl属性指定了为引入功能提供实现的类。
  • @DeclareParents注解所标注的静态属性指明了要引入的接口。

这样声明之后,Spring的自动代理机制将会获取到它的声明,当Spring发现一个bean被调用时,Spring会将调用委托给被代理的bean或被引入的实现,这取决于调用的方法属于被代理的bean还是属于被引入的接口。我们这样进行测试:

    @Autowired
    private Performance p;

    @Test
    public void test(){
        p.perform();
        Encoreable e = (Encoreable) p;
        e.performEncore();
    }

当需要调用Performance的perform()方法时,这个调用会被传递给Performance bean,如果要调用performEncore()方法,Spring会将这个调用委托给DefaultEncoreable。

这一节主要介绍了使用注解创建切面,使用注解的切面声明有一个明显的劣势:必须能够为通知类添加注解,为了做到这一点,必须要有源码,如果没有源码的话,我们可以使用XML声明切面。

4、在XML中声明切面

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

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:config> 顶层AOP配置元素,大多数的<aop:*>元素必须包含在<aop:config>元素内
<aop:declare-parents> 以透明的方式为被通知的对象引入额外的接口
<aop:pointcut> 定义一个切点
<aop:before> 定义一个AOP前置通知

我们已经看过了<aop:aspectj-autoproxy> 元素,它能够自动代理AspectJ注解的通知类,aop命名空间的其他元素能够让我们直接在Spring配置中声明切面,而不需要使用注解。例如,之前定义的Audience,我们将其中的AspectJ注解全部移除掉:

public class Audience {

    public void silenceCellPhones() {
        System.out.println("请关闭手机");
    }

    public void takeSeats() {
        System.out.println("请入座");
    }

    public void applause() {
        System.out.println("热烈鼓掌");
    }

    public void demandRefund() {
        System.out.println("要求退款");
    }

}

可以看到,Audience就是一个简单的Java类,我们可以像其它类一样将它注册为Spring应用上下文中的bean,尽管看起来并没有什么差别,但Audience已经具备了成为AOP通知的所有条件,我们可以通过aop命名空间的元素,将其声明为一个切面。

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

我们可以使用aop命名空间的元素声明前置通知和后置通知,下面就是我们通过XML元素将无注解的Audience声明为一个切面:

<aop:config>
        <aop:aspect ref="audience">
            <aop:before method="silenceCellPhones" pointcut="execution(** concert.Performance.perform(..))"/>

            <aop:before method="takeSeats" pointcut="execution(** concert.Performance.perform(..))"/>

            <aop:after-returning method="applause" pointcut="execution(** concert.Performance.perform(..))"/>

            <aop:after-throwing method="demandRefund" pointcut="execution(** concert.Performance.perform(..))"/>
        </aop:aspect>
</aop:config>

值得注意的是,大多数AOP配置元素必须在<aop:aspect> 元素的上下文内使用,把bean声明为一个切面时,我们总是从<aop:aspect> 元素开始配置的。在<aop:aspect> 元素内,我们可以声明一个或多个通知器、切面或者切点。

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

可以看到,如果在通知定义中使用pointcut元素定义切点,如果多个通知的切点是相同的,那么就会出现重复定义的情况,为了消除重复定义切点,在基于XML的切面声明中,我们需要使用<aop:pointcut> 元素。我们修改上面的XML配置:

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

            <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>

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

4.2、声明环绕通知

我们也可以使用XML声明环绕通知,环绕通知的一个明显的优势就是我们只需一个方法就可以完成前置通知和后置通知所实现的相同功能。我们重新定义Audience类,使用watchPerformance()方法提供AOP环绕通知的功能:

public class Audience {
    public void watchPerformance(ProceedingJoinPoint jp) {

        try {
            System.out.println("请关闭手机");//表演之前
            System.out.println("请入座");//表演之前
            jp.proceed();   //执行被通知的方法
            System.out.println("热烈鼓掌");//表演成功之后
        } catch (Throwable e) {
            System.out.println("要求退款");//表演失败之后
        }

    }
}

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

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

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

4.3、为通知传递参数

在上节中,我们使用AspectJ注解创建了一个切面,这个切面能够记录每个磁道的播放次数,现在,我们使用XML来配置切面来完成相同的任务。

首先我们声明切面TrackCounter,它是无注解的切面:

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

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

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

接下来我们使用XML配置切面:

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

    <bean id="blankDisc" class="concert.BlankDisc" >
        <property name="title" value="音乐标题" />
        <property name="artist" value="音乐作者" />
        <property name="tracks">
            <list>
                <value>音乐1</value>
                <value>音乐2</value>
                <value>音乐3</value>
                <value>音乐4</value>
                <value>音乐5</value>
                <value>音乐6</value>
            </list>
        </property>
    </bean>

    <bean id="trackCounter" class="aspect.TrackCounter" />

    <aop:config>
        <aop:aspect ref="trackCounter">
            <aop:pointcut expression="execution(* concert.BlankDisc.playTrack(int)) and args(trackNum)" id="trackPlayed"/>

            <aop:before method="countTrack" pointcut-ref="trackPlayed"/>
        </aop:aspect>
    </aop:config>

</beans>

首先我们使用<bean> 元素将BlankDisc和TrackCounter声明为Spring应用上下文中的bean,然后我们使用aop命名空间声明切面,这里声明的切面和之前使用XML元素声明切面是类似的,唯一明显的差别在于切点表达式中包含了一个参数,这个参数会传递到通知方法中。

4.4、通过切面引入新的功能

在上一节,我们使用@DeclareParents注解为被通知的方法引入新的方法,但是AOP引入并不是AspectJ特有的,使用Spring aop命名空间中的<aop:declare-parents> 元素,我们可以实现相同的功能:

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

顾名思义,<aop:declare-parents> 声明了此切面所通知的bean要在它的对象层次结构中拥有新的父类型。在本例中,类型匹配Pefrormance(由types-matching属性指定)在父类结构中会增加Encoreable接口(由implement-interface属性指定),最后由default-impl指定默认实现类。

在XML配置中,有两种方式来标识所引入接口的实现,我们使用default-impl属性用全限定类名来显式指定Encoreable的实现,还可以使用delegate-ref引用一个Spring bean作为引入的委托:

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

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

5、注入AspectJ切面

虽然Spring AOP能够满足许多应用的切面需求,但是与AspectJ相比,Spring AOP还是一个功能比较弱的AOP解决方案,AspectJ提供了Spring AOP所不能支持的许多类型的切点,例如构造器切点。

许多精心设计且有意义的切面很有可能依赖其它类来完成它们的工作,如果执行通知时,切面依赖于一个或多个类,我们可以在切面内部实例化这些协作对象,但更好的方式是,我们可以借助Spring的依赖注入把bean装配进入AspectJ切面中。下面我们通过一个例子看一下,我们使用AspectJ的方式创建一个评论员切面,他会在演出之后提供一些批评意见,下面是这样一个切面:

public aspect CriticAspect {

    pointcut performance() : execution(* aop.Performance.perform(..));

    after() returning() : performance() {
        System.out.println(criticismEngine.getCriticism());
    }

    private CriticismEngine criticismEngine;

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

可以看到,这个切面不是普通的Java类,我们将这段代码复制到Eclipse中也不能通过编译,这是一段AspectJ风格的代码,为了能够在Eclipse中开发AspectJ代码,我们需要导入AJDT插件,安装方法参考:https://blog.csdn.net/aitangyong/article/details/50770085 这篇文章,首先我们要查看Eclipse版本,点击Help->About Eclipse:

这里写图片描述

在这里,我们的Eclipse是4.4的版本,接下来我们需要在AJDT下载网站 中查找适合相应Eclipse版本的插件,然后在Eclipse中下载相应的插件:

这里写图片描述

下载完成后,重启Eclipse后我们就可以创建AspectJ风格的代码了:

这里写图片描述

说回上面的切面定义,我们首先定义了一个切点performance(),这个切点在Performance的perform()方法正确返回之后执行,它会输出评论员的评论,在这里,评论员可以通过属性注入的方式注入进这个切面,我们先来看一下评论员的定义:

public class CriticismEngine {

    private String[] critics = {"表演太精彩了!", "表演很失败!", "表演一般般"};

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

}

这只是一个普通的Java类,它能够随机输出一个评论,然后我们再看看在XML中的配置:

    <bean id="criticismEngine" class="aop.CriticismEngine" />

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

我们首先将CriticismEngine声明为一个Spring上下文中的bean,然后使用<bean> 配置CriticAspect,通过属性注入将CriticismEngine注入进来,乍看下来,这里的配置和普通的XML配置没有什么不同,但是值得注意的是,CriticAspect使用了factory-method属性,通常情况下,Spring bean由Spring容器初始化,但是AspectJ切面是由AspectJ在运行期间创建的,等到Spring有机会为CriticAspect注入到CriticismEngine时,CriticAspect已经被实例化了。

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

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

猜你喜欢

转载自blog.csdn.net/u011024652/article/details/80158045