各位小猿,程序员小猿开发笔记,希望大家共同进步。
使用注解来创建切面是AspectJ 5所引入的关键特性。AspectJ 5之前, 编写AspectJ切面需要学习一种Java语言的扩展,但是AspectJ面向注解 的模型可以非常简便地通过少量注解把任意类转变为切面。

我们已经定义了Performance接口,它是切面中切点的目标对象。 现在,让我们使用AspecJ注解来定义切面。

【Spring实战【面向切面的Spring】1.5使用注解创建切面_磁道

1.5.1 定义切面

流程图:

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

程序清单4. 1展现了Audience类,它定义了我们所需的一个切面。

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

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

    @AfterThrowing("execution(**concert.Performance.perform(..))")
    public void demandRefund() {
        System.out.println("CLAP CLAP CLAP!!!");
    }
    }

程序清单4.1 Audience类:观看演出的切面

1. @AspectJ注解的作用是什么?

Audience类使用@AspectJ注解进行了标注。

该注解表明Audience不仅仅是一个POJO,还是一个切面。
Audience类中的方法都使用注解来定义切面的具体行为。

【Spring实战【面向切面的Spring】1.5使用注解创建切面_System_02

可以看到,这些方法都使用了通知注解来表明它们应该在什么时候调用。AspectJ提供了五个注解来定义通知,如表4.2所示。

【Spring实战【面向切面的Spring】1.5使用注解创建切面_spring_03

4.2 Spring使用AspectJ注解来声明通知方法

Audience使用到了前面五个注解中的三个。

takeSeats ()和 silence CellPhones ()方法都用到了@Before注解,表明它们应 该在演出开始之前调用。

applause ()方法使用了@AfterReturning注解,它会在演出成功返回后调用。

demandRefund()方法上添加了@AfterThrowing注解,这表 明它会在抛出异常以后执行。

【Spring实战【面向切面的Spring】1.5使用注解创建切面_System_04

2.我们这时候会发现,这些注解都有一个切点表达式作为值?

所有的这些注解都给定了一个切点表达式作为它的值,同时,这四个方法的切点表达式都是相同的。反之,它们可以设置成不同的切点表达式,但是在这里,这个切点表达式就能满足所有通知方法的需求。

3.切点表达式用了四次,存在一定的问题?

触发时间:在Performance的perform()方法执行时触发。

缺点:在任务执行前后,触发了四次,虽然能达到预期功能,但是给人怪怪的感觉。是不是可以定义一次呢,在每次需要的时候引用它,是一个很好的方案。

这时候,我们可以使用@PointCut注解,在@Aspect内定义可重用的切点。

package com.spring.cut;

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

@Aspect
public class Audience {
    /**
     * 定义命名的切点
     */
    @Pointcut("execution(**concert.Performance.perform(..))")
    public void performace() {
    }

    /**
     * 表演之前
     */
    @Before("performace() ")
    public void silenceCellPhones() {
        System.out.println("Silencing cell phones");
    }

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

    /**
     * 表演之后
     */
    @AfterReturning("performace() ")
    public void applause() {
        System.out.println("CLAP CLAP CLAP!!!");
    }

    /**
     * 表演失败之后
     */
    @AfterThrowing("performace() ")
    public void demandRefund() {
        System.out.println("CLAP CLAP CLAP!!!");
    }
    }

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

4. 使用@Pointcut注解代替切面表达式,应该怎么做?

1.在Audience类中,我们使用了@Pointcut注解来定义performance()方法。

2.@Pointcut注解接受一个切点表达式作为参数,就像我们之前在通知注解中所使用的那样。

5.这样做的意义在哪里?

通过在performance()方法上添加@Pointcut注解,我们扩展了切点表达式语言的能力,这样我们可以在任何需要使用performance()的地方使用它,而不必再使用更长的切点表达式。我们将所有通知注解中的长表达式都替换成了performance()。

实际上,performance()方法的具体内容并不重要,在这里它是空的。该方法只是一个标识,供@Pointcut注解使用。

6.Audience的意义何在?

Audience只是一个通过注解表明将用作切面的Java类。

像其他的Java类一样,它可以装配为Spring中的bean:

【Spring实战【面向切面的Spring】1.5使用注解创建切面_spring_05

@Bean
public Audience. audience(){
 return new Audience();
}
7.Audience自动代理会怎么样?

如果你就此止步的话,Audience只会是Spring容器中的一个bean。 即便使用了AspectJ注解,但它并不会被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理。

8.如何启动自动代理功能?

如果你使用JavaConfig的话,可以在配置类的类级别上通过使用EnableAspectJ-AutoProxy注解启用自动代理功能。

程序清单 4.3展现了如何在JavaConfig中启用自动代理。

@Configuration
@EnableAspectJAutoproxy//启用AspectJ自动代理
@ComponentScan
public class ConcertConfig{
   @Bean//声明Audience bean
   public Audience audience(){
      return new Audience();
   }
}

程序清单4.3JavaConfig中启用AspectJ注解的自动代理

假如你在Spring中要使用XML来装配bean的话,那么需要使用Spring aop命名空间中的<aop:aspectj-autoproxy>元素。下面的XML 配置展现了如何完成该功能。

程序清单4.4XML中,通过Springaop命名空间启用AspectJ自 动代理

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

    <aop:aspectj-autoproxy/>

    <!--声明Audience bean-->
    <bean class="com.spring.cut.Audience"/>

</beans>

不管你是使用JavaConfig还是XML,AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面 的切点所匹配的bean。在这种情况下,将会为Concertbean创建一个 代理,Audience类中的通知方法将会在perform()调用前后执行。

9.Spring的Aspect自动代理特性?

Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。

在本质上,它依然是 Spring基于代理的切面。换句话说虽然使用的 是@AspectJ注解,仍然限于代理方法的调用。

10.如何充分利用Aspect的能力?

如果想利用 AspectJ的所有能力,我们必须在运行时使用AspectJ并且不依赖Spring 来创建基于代理的切面。

到现在为止,我们的切面在定义时,使用了不同的通知方法来实现前 置通知和后置通知。但是表4.2还提到了另外的一种通知:环绕通知 (around advice ) 。环绕通知与其他类型的通知有所不同,因此值得 花点时间来介绍如何进行编写。

1.5.2 创建环绕通知

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

为了阐述环绕通知,我们重写Audience切面。这次,我们使用一个 环绕通知来代替之前多个不同的前置通知和后置通知。

程序清单4.5 使用环绕通知重新实现Audience切面

package com.spring.cut;

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 Audience {
    /**
     * 定义命名的切点
     */
    @Pointcut("execution(**concert.Performance.perform(..))")
    public void performace(){}

    /**
     * 环绕通知方法
     */
    @Around("performace()")
    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 a refund");
        }
    }
}

1.@ around注解在上述方法的意义何在?代表了什么?

在这里,@Around注解表明watchPerformance ()方法会作为performance ()切点的环绕通知。

2.在这程序中,执行流程是怎么样的?

【Spring实战【面向切面的Spring】1.5使用注解创建切面_磁道_06

在这个通知中,观众在演出之 前会将手机调至静音并就坐,演出结束后会鼓掌喝彩。像前面一样, 如果演出失败的话,观众会要求退款。

3.通知的执行效果与前置通知和后置通知效果怎么样?

这个通知所达到的效果与之前的前置通知和后置通知是一样的。但是,现在它们位于同一个方法中,不像之前那样分散在四个不同的通知方法里面。

4.关于这个新的通知方法,参数是怎么样的?

这个通知方法,它接受ProceedingJoinPoint作为参数。这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。

5.通知方法中控制权转移?

通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用ProceedingJoinPoint的proceed()方法。

6.为什么需要调用proceed()方法?

需要注意的是,别忘记调用proceed()方法。如果不调这个方法的 话,那么你的通知实际上会阻塞对被通知方法的调用。有可能这就是你想要的效果,但更多的情况是你希望在某个点上执行被通知的方 法。

7.如果不调用proceed方法会怎么样?

有意思的是,你可以不调用proceed()方法,从而阻塞对被通知方法的访问,与之类似,你也可以在通知中对它进行多次调用。要这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。

1.5.3 处理通知中的参数
1.我们切面如今的参数?

到目前为止,我们的切面都很简单,没有任何参数。

唯一的例外是我们为环绕通知所编写的watchPerformance ()示例方法中使用了  ProceedingJoinPoint作为参数。
   除了环绕通知,我们编写的其他通知不需要关注传递给被通知方法的任意参数。这很正常,因为我们所通知的perform()方法本身没有任何参数。
2.如果切面所通知的方法确实有参数该怎么办呢?切面能访问和使用传递给被通知方法的参数吗?

假设你想记录每个磁道被播放的次数。一种方法就是修改playTrack()方法,直接在每次调用的时候记录这个数量。但是,记录磁道的播放次数与播放本身是不同的关注点,因此不应该属 于playTrack()方法。看起来,这应该是切面要完成的任务。

为了记录每个磁道所播放的次数,我们创建了TrackCounter类, 它是通知playTrack()方法的一个切面。下面的程序清单展示了这 个切面。

程序清单4.6 使用参数化的通知来记录磁道播放的次数

package com.spring.cut;

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

import java.util.HashMap;
import java.util.Map;

/**
 * @author huyang
 */
@Aspect
public class TrackCounter {

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

    /**
     * 通知play-Track()方法,切面使用@Pointcut注解定义命名 的切点
     *
     * @param trackNumbers
     */
    @Pointcut("execution(* com.spring.cut(int))" + "&& args(trackNumbers)")
    public void trackPlayed(int trackNumbers) {
    }

    /**
     * 在播放前,为该磁道计数
     *用@Before将一个方法声明为前置通知
     *
     * @param trackNumbers
     */
    @Before("trackPlayed(trackNumbers)")
    public void countTrack(int trackNumbers) {
        int currentCount = getPlayCount(trackNumbers);
        trackCounts.put(trackNumbers, currentCount + 1);
    }

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


}

图4.6将切点 表达式进行了分解,以展现参数是在什么地方指定的。

图4 .6 在切点表达式中声明参数,这个参数传入到通知方法中

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

现在,我们可以在Spring配置中将BlankDisc和TrackCounter定 义为bean,并启用AspectJ自动代理,如程序清单4.7所示。

程序清单4.7 配置TrackCount记录每个磁道播放的次数

package com.spring.cut;

import com.spring.CompactDisc;
import com.spring.Constant.BlankDisc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import java.util.ArrayList;
import java.util.List;

/**
 * @author huyang
 */
@Configuration
@EnableAspectJAutoProxy
public class TrackCounterConfig {

    @Bean
    public CompactDisc sgtPeppers() {
        BlankDisc cd = new BlankDisc();
        cd.setTitle("Sgt");
        cd.setArtist("The Beatles");
        List<String> tracks = new ArrayList<String>();
        tracks.add("Sgt");
        tracks.add("With a Little Help from my Friends");
        tracks.add("Getting Better");
        tracks.add("Fixing a Hole");
        cd.setTracks(tracks);
        return cd;
    }

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

最后,为了证明它能正常工作,你可以编写如下的简单测试。它会播 放几个磁道并通过TrackCounter断言播放的数量。

程序清单4.8 测试TrackCounter切面

package com.spring.cut;

import com.spring.CompactDisc;
import org.aspectj.lang.annotation.AfterThrowing;
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;

import static junit.framework.TestCase.assertEquals;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TrackCounterConfig.class)
public class TrackCounterConfigTest {

    @Autowired
    private CompactDisc cd;

    @Autowired
    private TrackCounter counter;

    @Test
    public void testTrackCounter(){
        cd.playTrack(1);
        cd.playTrack(2);
        cd.playTrack(3);
        cd.playTrack(3);
        cd.playTrack(7);
        
        assertEquals(1,counter.getPlayCount(1));
        assertEquals(1,counter.getPlayCount(1));
        assertEquals(1,counter.getPlayCount(1));
        assertEquals(1,counter.getPlayCount(1));
    }
}

到目前为止,在我们所使用的切面中,所包装的都是被通知对象的已 有方法。但是,方法包装仅仅是切面所能实现的功能之一。让我们看 一下如何通过编写切面,为被通知的对象引入全新的功能。

1.5.4 通过注解引入新功能

一些编程语言,例如Ruby和Groovy,有开放类的理念。它们可以不用 直接修改对象或类的定义就能够为对象或类增加新的方法。不过, Java并不是动态语言。一旦类编译完成了,我们就很难再为该类添加 新的功能了。

1.如何切面为现有的方法增加额外的功能?

当 然,我们还没有为对象增加任何新的方法,但是已经为对象拥有的方法添加了新功能。如果切面能够为现有的方法增加额外的功能,为什 么不能为一个对象增加新的方法呢?

2.如何使用AOP增加新方法?

实际上,利用被称为引入的AOP 概念,切面可以为Springbean添加新方法。

3.如果实现新接口,代理暴露新接口的话会怎么样?

回顾一下,在Spring中,切面只是实现了它们所包装bean相同接口的 代理。那样的话,切面所通知的bean看起来像是实现了新的接口,即便 底层实现类并没有实现这些接口也无所谓。

图4.7展示了它们是如何 工作的。

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

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

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

package com.spring.cut;

/**
 * @author huyang
 */
public interface Encoreable {
    void performEncore();
}
4.我们需要有 一种方式将这个接口应用到Performance实现中?

我们现在假设你 能够访问Performance的所有实现,并对其进行修改,让它们都实 现Encoreable接口。

5.从设计的角度来看,有什么问题?

从设计的角度来看,这并不是最好的做 法,并不是所有的Performance都是具有Encoreable特性的。另外一方面,有可能无法修改所有的Performance实现,当使用第三 方实现并且没有源码的时候更是如此。

6.我们这时候引入AOP功能,会怎么样?

值得庆幸的是,借助于AOP的引入功能,我们可以不必在设计上妥协 或者侵入性地改变现有的实现。为了实现该功能,我们要创建一个新 的切面:

【Spring实战【面向切面的Spring】1.5使用注解创建切面_磁道_07

  • 引入AOP功能没有不太清楚,希望再研究一下。
package com.spring.cut;

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

/**
 * @author huyang
 */
@Aspect
public class EncoreableIntroducer {

    @DeclareParents(value = "com.spring.cut",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="concert.EncoreableIntroducer"/>

Spring的自动代理机制将会获取到它的声明,当Spring发现一个bean使 用了@Aspect注解时,Spring就会创建一个代理,然后将调用委托给 被代理的bean或被引入的实现,这取决于调用的方法属于被代理的 bean还是属于被引入的接口。

在Spring中,注解和自动代理提供了一种很便利的方式来创建切面。 它非常简单,并且只涉及到最少的Spring配置。但是,面向注解的切 面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到 这一点,必须要有源码。&emsp;欢迎程序员关注程序员小猿小胡工作之家,大家一起交流进步。