Spring之面向切面编程(AOP)

1、面向切面编程

       在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。

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

       切面提供了取代继承和委托的另一种可选方案,而且在很多场景下更清晰简洁。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。

 

2、定义AOP术语

       与大多数技术一样,AOP已经形成了自己的术语。描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point)。下图展示了这些概念是如何关联在一起的。

 

(1)通知(Advice)

       在AOP术语中,切面的工作被称为通知

       通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?

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

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

(2)连接点(Join point)

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

(3)切点((Poincut)

       如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些AOP框架允许我们创建动态的切点,可以根据运行时的决策(比如方法的参数值)来决定是否应用通知。

(4)切面(Aspect)

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

(5)引入(Introduction)

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

(6)织入(Weaving)

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

  • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
  • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。

小结:

       通知包含了需要用于多个应用对象的横切行为;连接点是程序执行过程中能够应用通知的所有点;切点定义了通知被应用的具体位置(在哪些连接点)。其中关键的概念是切点定义了哪些连接点会得到通知。

 

3、Spring对AOP的支持

       并不是所有的AOP框架都是相同的,它们在连接点模型上可能有强弱之分。有些允许在字段修饰符级别应用通知,而另一些只支持与方法调用相关的连接点。它们织入切面的方式和时机也有所不同。但是无论如何,创建切点来定义切面所织入的连接点是AOP框架的基本功能。

(1)Spring提供了4种类型的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来完成功能。

(2)Spring在运行时通知对象

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

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

(3)SPring只支持方法级别的连接点

       因为Spring基于动态代理,所以Spring只支持方法连接点。这与一些其他的AOP框架是不同的,例如AspectJ和JBoss,除了方法切点,它们还提供了字段和构造器接入点。Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,我们就无法在bean创建时应用通知。

 

4、通过切点来选择连接点

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

关于Spring AOP的AspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器(pointcut designator)的一个子集。让我们回顾下,Spring是基于代理的,而某些切点表达式是与基于代理的AOP无关的。下表列出了Spring AOP所支持的AspectJ切点指示器。

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

       当我们查看如上所展示的这些Spring支持的指示器时,注意只有execution指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的。这说明execution指示器是我们在编写切点定义时最主要使用的指示器。在此基础上,我们使用其他指示器来限制所匹配的切点。

(1)编写切入点

例:

package concert;
public interface Performance {
    public void perform(} ;
}

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

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

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

       请注意我们使用了“&&”操作符把execution()和within()指示器连接在一起形成与(and)关系(切点必须匹配所有的指示器)。类似地,我们可以使用“||”操作符来标识或(or)关系,而使用“!”操作符来标识非(not)操作。

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

(2)在切点中选择bean

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

例如,考虑如下的切点:

execution(* concert. Per formance. perform() )
and bean( 'woodstock' )

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

 

5、使用注解创建切面

(1)定义切面

       AspectJ提供了五个注解来定义通知,如下表所示:

例:

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

    // 表演之前
    @Before(“performance()”)
    publict void dilenceCellPhones(){
        System.put.println(“Silence”);
    }

    // 表演之后
    @AfterReturning(“performance()”)
    public void applause(){
        System.put.println(“CLAP”);
    }

    // 表演失败之后
    @AfterThrowing(“performance()”)
    public void demandRefund(){
        System.put.println(“Demand”);
    }
}

       在Audience中,performance()方法使用了@Pointcut注解。为@Pointcut注解设置的值是一个切点表达式,就像之前在通知注解上所设置的那样。通过在performance()方法上添加@Pointcut注解,我们实际上扩展了切点表达式语言,这样就可以在任何的切点表达式中使用performance()了,如果不这样做的话,你需要在这些地方使用那个更长的切点表达式。我们现在把所有通知注解中的长表达式都替换成了performance()。

       performance()方法的实际内容并不重要,在这里它实际上应该是空的。其实该方法本身只是一个标识,供@Pointcut注解依附。需要注意的是,除了注解和没有实际操作的performance()方法,Audience类依然是一个POJO。我们能够像使用其他的Java类那样调用它的方法,它的方法也能够独立地进行单元测试,这与其他的Java类并没有什么区别。Audience只是一个Java类,只不过它通过注解表明会作为切面使用而已。

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

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

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

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

例:在JavaConfig中启用AspectJ注解的自动代理

// 启用AspctJ自动代理
@Configurantion
@EnableAspectJAutoProxy
@ComponetScan
public class ConcertConfig(
    @Bean
    public Audience audience() {
        return new Audience() ;
    }
)

例:在XML中,通过Spring的aop命名空间启用用AspectJ自动代理

<?xml version="1.0" encoding= °UTF -8*?>
<beans xmlns= "http: 1 /www。spr ingf ramework . org/ schena/beans”
    xmIns :xsi="http: / /www . w3 .org/ 2001 /XMLSchema- instance”
    xmlns: context= "http: / /www. spr ingfr amework。org/ schemal context *
    xmlns :aop= "http:/ /www . spr ingframework , org/ schema/aop"
    xsi :schemaLocation= "http: / /www. spr ingframework . org/schena/aop
        http: / /www. spr ing fr amework . org/ schema/ aop/ spring-aop .xsd
        http: / /www. spr ingfr amework. org/ schema/beans
        http://ww. springf ramework. org/ schema/beans/spring - beans。xsd
        http:/ /www. spr ingfr amework. org/ schema/ context
        http://ww. springframework . org/ schema/ context/ spring-context。xsd">
    <context : component- scan base -package=”concert" />
    <-- 启用AspctJ自动代理 -->
    <aop:aspectj-autoproxy />
    <bean class= "concert . Audience" />
</beans>

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

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

(2)创建环绕通知

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

例:

package concert ;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect ;
import org.aspectj.lang.annotation.Pointeut;

@Aspect
public class Audience {
    // 定义命名的切点
    @PointCut("execution(** concert.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 a refund");
        }
    }
}

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

(3)处理通知中的参数

例:

package soundsystem;
import java.util.HashMap;
import java.util.Map;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

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

    // 通知playTrack()方法
    @PointCut("execution (* soundsystem.CompactDisc.playTrack(int))" +
        "&& args(trackNumber)")
    public void trackPlayed(int trackNumber) {}

    @Before("trackPlayed(trackNumber)")
    public void countTrack(int trackNumber){
        int currentCount = getPayCount(trackNumber);
        trackNumber.put(trackNumber, currentCount + 1);
    }

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

       这个切面使用@Pointcut注解定义命名的切点,并使用@Before将一个方法声明为前置通知。下图将切点表达式进行了分解,以展现参数是在什么地方指定的。

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

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

       下面,我们可以在Spring配置中将BlankDisc和TrackCounter定义为bean,并启用AspectJ自动代理:

例:配置TrackCount记录每个磁道播放的次数

package soundsys tem;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean; .
import org.springframework.context.annotation.Conf iguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

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

    @Bean
    public CompactDisc sgtPeppers() {
        BlankDisc cd = new BlankDisc();
        cd.setTitle("Sgt.Pepper's Lonely Hearts Club Band");
        cd.setArtist("The Beatles");
        List<string> tracks = new ArrayList<string>();
        tracks.add("sgt.Pepper's Lonely Hearts Club Band");
        tracks.add("with a Little Help from My Friends");
        tracks.add("Lucy in the Sky with Di amonds");
        tracks.add("Getting Better");
        tracks.add("Fixing a Hole");

        cd.setTracks(tracks);
        return cd;
    }
        @Bean
        public TrackCounter trackCounterl) {
            return new TrackCounter();
        }
}

例:测试TrackCounter切面

package soundsystem;
import static org.junit.Assert.*;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.StandardoutputstreamLog;
import org.junit.runner.RunWith;
import org.springfr amework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@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(1);
            // 播放一些磁道
            cd.playTrack(2) ;
            cd.playTrack(3) ;
            cd.playTrack(3) ;
            cd.playTrack(3);
            cd.playTrack(3) ;
            cd.playTrack(7) ;
            cd.playTrack(7);

            // 断言期望的数量
            assertEquals(1, counter.getPlayCount(1));
            assertEquals(1, counter.getPlayCount(2));
            assertEquals(4, counter.getPlayCount(3));
            assertEquals(0, counter.getPlayCount(4));
    }
}

(4)通过注解引入新功能

例:

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

@Aspect
public class EncoreableInTroducer{
    @DeclareParents(value = "concert.Performance+",
                    defaultImpl = DeclareEncoreable.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配置。但是,面向注解的切面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到这一点,必须要有源码。

 

六、在XML中声明切面

       Spring的AOP配置元素能够以非侵入性的方式声明切面,如下表所示:

例:

package concert;
public class Audience{
    public void silencecellPhones() {
        System.out.println("silencing cell phones");
    }
    public void takeSeats() {
        System. out .println("Taking seats") ;
    }
    public void applausel() {
        System. out.println("CLAP CLAP CLAP! ! !") ;
    }
    public void demandRefund() {
        System.out.println("Demanding a refund");
    }
}

       Audience已经具备了成为AOP通知的所有条件。我们再稍微帮助它一把,它就能够成为预期的通知了。

(1)声明前置和后置通知

例:通过XML将无注解的Audience声明为切面

<aop:config>
    <aop:aspect ref="audience">
        <aop:before
            pointcut="execution(** concert.Performance.perform(..))"
            method="silencecellPhones"/>
        <aop:before
                pointcut="execution(** concert.Performance.perform(..))"
                method="takeSeats"/>
        <aop:after-returning
                pointcut="execution(** concert.Performance.perform(..))"
                method="applausel"/>
        <aop:after-throwing
                pointcut="execution(** concert.Performance.perform(..))"
                method="demandRefund"/>
    </aop:aspect>
</aop:config>

       在<aop:config>元素内,我们可以声明一个或多个通知器、切面或者切点。在以上程序中,我们使用<aop:aspect>元素声明了一个简单的切面。ref元素引用了一个POJO bean,该bean实现了切面的功能——在这里就是audience。ref元素所引用的bean提供了在切面中通知所调用的方法。

       Audience切面包含四种通知,它们把通知逻辑织入进匹配切面切点的方法中:

       如下的XML展现了如何将通用的切点表达式抽取到一个切点声明中,这样这个声明就能在所有的通知元素中使用了。

例:使用<aop:pointcut>定义命名切点

<aop:config>
    <aop:aspect ref="audience">
        <aop:pointcut id = "performance"
            expression="execution(** concert.Performance.perform(..))*"/>
        <aop:before pointcut-ref="performance"
                    method="silencecellPhones"/>
        <aop:before pointcut-ref="performance"
                    method="takeSeats"/>
        <aop:after-returning pointcut-ref="performance"
                    method="applausel"/>
        <aop:after-throwing pointcut-ref="performance"
                    method="demandRefund"/>
    </aop:aspect>
</aop:config>

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

       <aop:pointcut>元素所定义的切点可以被同一个<aop:aspect>元素之内的所有通知元素引用。如果想让定义的切点能够在多个切面使用,我们可以把<aop:pointcut>元素放在<aop:config>元素的范围内。

(2)声明环绕通知

       到目前为止Audience已经能完成大部分工作了,但其前置通知和后置通知有一些限制。具体来说,如果不使用成员变量存储信息的话,在前置通知和后置通知之间共享信息非常麻烦。

       使用环绕通知,我们可以完成前置通知和后置通知所实现的相同功能,而且只需要在一个方法中 实现。因为整个通知逻辑是在一个方法内实现的,所以不需要使用成员变量保存 状态。

例:watchPerformance 方法提供了AOP环绕通知

package concert;
import org.aspectj.lang.ProceedingJoinPoint;
public class Audience{
    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") ;
        }
    }
}

       watchPerformance()方法包含了之前四个通知方法的所有功能。不过,所有的功能都放在了这一个方法中,因此这个方法还要负责自身的异常处理。

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

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

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

(3)为通知传递参数

例:无注解的TrackCounter

package soundsystem;
import java.util.HashMap;
import java.util.Map;

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

    public void countTrack(int trackNumber){
        int currentCount = getPayCount(trackNumber);
        trackNumber.put(trackNumber, currentCount + 1);
    }

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

       下面借助一点Spring XML配置,我们能够让TrackCounter重新变为切面。

例:在XML中将TrackCounter配置为参数化的切面

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

    <bean id="trackCounter"
        class="soundsystem.TrackCounter"/>
    <bean id="cd"
          class="soundsystem.BlackDisc">
        <property name="list" value="abc"/>
        <property name="artlist" value="efg">
            <list>
                <value>aaa</value>
                <value>bbb</value>
                <value>ccc</value>
                <value>ddd</value>
            </list>
        </property>
    </bean>

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

</beans>

(4)通过切面引入新的功能

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

       如下的XML代码片段与之前基于AspectJ的引入功能是相同:

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

       顾名思义,<aop:declare-parents>声明了此切面所通知的bean要在它的对象层次结构中拥有新的父类型。具体到本例中,类型匹配Performance接口(由types-matching属性指定)的那些bean在父类结构中会增加Encoreable接口(由implement-interface属性指定)。最后要解决的问题是Encoreable接口中的方法实现要来自于何处。

       这里有两种方式标识所引入接口的实现。在本例中,我们使用default-impl属性用全限定类名来显式指定Encoreable的实现。或者,我们还可以使用delegate-ref属性来标识。

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

       delegate-ref属性引用了一个Spring bean作为引入的委托。这需要在Spring上下文中存在一个ID为encoreableDelegate的bean。

<bean id=”encoreableDelegate”
    class=”concert.DefaultEncoreable”/>

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

 

七、注入AspectJ切面

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

       例如,当我们需要在创建对象时应用通知,构造器切点就非常方便。不像某些其他面向对象语言中的构造器,Java构造器不同于其他的正常方法。这使得Spring基于代理的AOP无法把通知应用于对象的创建过程。

       对于大部分功能来讲,AspectJ切面与Spring是相互独立的。虽然它们可以织入到任意的Java应用中,这也包括了Spring应用,但是在应用AspectJ切面时几乎不会涉及到Spring。

       但是精心设计且有意义的切面很可能依赖其他类来完成它们的工作。如果在执行通知时,切面依赖于一个或多个类,我们可以在切面内部实例化这些协作的对象。但更好的方式是,我们可以借助Spring的依赖注入把bean装配进AspectJ切面中。

例:使用AspectJ实现表演的评论员

package conceret;
public aspect CriticAspect{
    public CriticAspect(){}
    pointcut performance() : execution(* perform(...));
    afterRururning() : performance(){
        System.out.println(criticismEngine.getCriticism() ) ;
    }

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

}

       CriticAspect与一个CriticismEngine对象相协作,在表演结束时,调用该对象的getCriticism()方法来发表一个苛刻的评论。为了避免CriticAspect和CriticismEngine之间产生不必要的耦合,我们通过Setter依赖注入为CriticAspect设置CriticismEngine。下图展示了此关系。

例:要注入到CriticismEngine的CriticismEngine 实现

package com.springinaction.springidol;
public class CriticismEngineImpl implements CriticismEngine {
    public CriticismEngineImpl() {}
    public String getCriticism() {
        int i = (int) (Math.random() * criticismPool.length);
        return criticismPoo1[i] ;
    }
    // injected
    private String[] criticismPool ;
    public void setCriticismPool (String[] criticismPool) {
        this. criticismPool = critici smPool ;
    }
}

       这个类可以使用如下的XML声明为一个Spring bean。

<bean id="critici smEngine"
      class=" com. spr inginaction. spr ingidol .Critici smEngineImpl">
    <property name= "criticisms ">
        <list>
            <value>Worst performance ever !</value>
            <value>I laughed, I cried, then I realized I was at the
                wrong show. </value>
            <value>A must see show!</value>
        </list>
    </property>
</bean>

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

<bean class= " com.springinact ion.springidol.CriticAspect"
    factory-method= "aspectof">
    <property name=" critici smEngine" ref= "criticismEngine" />
</bean>

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

 

猜你喜欢

转载自blog.csdn.net/King_weng/article/details/107305892