面向切面编程

重要内容

  • 面向切面编程的基本知识
  • 为POJO创造切面
  • 使用@AspectJ注解
  • 为AspectJ的aspects注入看重关系

在南方没有暖气的冬季,太冷了,异常缅怀北方有暖气的冬日。为了取暖,很多朋友通过空调取暖,可是空调需要耗电,也就需要交不少电费。没家都会有一个电表,每隔一段时间都会有记录员来家里接受这段时间的电费。

近来做个比方:去掉电表和电费收取员,因而也从不人定期来家里收电费。这时就需要我们隔段时日主动去电力公司交电费,即便会有执着的家庭主妇会认真得记录每个月各样电器用了多少度电,并盘算出应有交由电力集团多少钱,可是多数人都做不到这般精确。基于诚信的电费统计系列对于顾客来说并不是坏事,可是对于电力公司来说真的灾难。

监察电力的损耗是异常重大的,但它并不是每个家庭主妇头脑中排在率先位的工作。买菜、打扫卫生、做饭、监督孩子的吃穿等等才是最要紧的业务。

软件系统中的某些意义就象是于大家家庭的电表。大家需要在使用中的每个节点应用这么些效率,不过每回都显式得调用它们又觉得太过啰嗦和浪费。日志、安全和事务管理的确特别重要,然而它们是不是应该跟工作逻辑写在联名?或者说,业务模块就活该注意于工作逻辑,而把上述这类的模块分别独立交给一个模块处理。

在软件开发中,将这类涉及使用中的五个模块的效用称为交叉关注点。遵照规矩,这多少个交叉关注点应该与事务逻辑代码剥离,可是其实常常是耦合在协同。面向切面编程要做的干活就是将这一个交叉关注点与业务逻辑代码分开。

这篇著作用于探索Spring框架对面向切面编程的支撑,包括怎么着定义需要被切面(aspect)覆盖的类,咋样采用注脚创立切面;这篇著作还将介绍AspectJ——第三方的AOP实现,看看哪些将AspectJ与Spring框架整合使用。

4.1 何为面向切面编程?

如作品起初所说,切面能够用于将接力关注点模块化。简单的话,交叉关注点值得是那么些影响一个使用中六个模块的通用效能。例如,安全处理是一个接力关注点,在动用中的很多模块中都急需选拔一定的安全检查,下图呈现了动用中穿插关注点与工作模块的涉及。

Aspects 用于模块化交叉关注点

这张图呈现的是一个卓绝的模块化应用,每个模块负责提供针对性某个特定领域(domain)的劳动,但是各样模块也需要有些均等的帮忙效用,例如安全、事务管理等等。

面向对象编程技术平常因而持续和信托实现代码复用。假设在动用中持有目的都持续自一个基类,这样的延续连串并不平静;使用委托,则在际遇复杂对象时突显相比笨重。

断面则提供了一个更领会、更轻量级的挑选。利用AOP,你可以将部分通用效用集中在一个模块中,并确定在哪个地方咋样时候将这个成效利用在业务模块上,而且不需要修改工作模块的代码。把交叉关注点模块化到某个特定的类,这多少个类就称为切面(aspects),这有两个亮点:

  • 关注点分离,而不是与工作逻辑代码混合在共同;
  • 工作模块更加显著,因为它们只需要关注业务逻辑部分;

4.1.1 定义AOP术语

和大部分技艺类似,AOP技术也有协调的行话。切面(Aspects)通常通过通告(advice)、切点(pointcuts)和织入点(join
points)来讲述。下图呈现了这些概念怎么样被联系在协同。

断面的效能(advice)通过一个或者两个织入点织入到使用的举办流程

洋洋AOP的术语都不太直观,我看成开发人士也是涉世一段时间的拔取将来才了解其幕后的意义。为了更好得学学AOP技术,最好先认真学习下一一术语的意义,然后带着问题去读书。

通知(ADVICE)

电表记录员来家里的目标是读取你家在过去一段时间所用的电量,并汇报给电力集团。他有亟待查阅记录的清单,并且这一个记录分外第一,可是事实上,记录用电量是电表记录员的重大工作。

断面也有它的目标——它真的要做的劳作,在AOP术语类别中,切面真正要做的干活称之为通知(advice)

布告负责定义切面的whatwhen——即那么些切面负责什么工作,以及几时实施这一个工作。应该在格局调用前举办切面的职责?仍然在点子调用后执行切面的任务?依然应该在章程调用此前和今后都执行切面的天职?依然仅仅在艺术调用抛出非常时实施切面的任务?

Spring切面协助以下五种布告:

  • Before——前置布告,在调用目的措施此前实施通知定义的任务;
  • After——后置通知,在目的措施执行完毕后,无论执行结果什么都执行公告定义的职责;
  • After-returning——后置公告,在目的措施执行完毕后,要是履行成功,则实施通告定义的天职;
  • After-throwing——非常通告,假设目的措施执行过程中抛出特别,则实施通告定义的任务;
  • Around——环绕通告,在对象措施执行前和举办后,都亟待履行通告定义的职责。

织入点(JOIN POINTS)

一个电力公司平时服务于一个城池,每家都有一个电表需要电表记录员定期去抄电表。同样,在应用中或者有为数不少个机会可以利用通告,那些机会就叫做织入点织入点仿佛一个插槽,通过织入点可以将切面织入到利用的实施流中。织入点可能是正值调用的点子、正在抛出的非凡或者是正在被改动的性质。

切点(POINTCUTS)

让一个电表记录员负责所有家庭的电赞扬着不太可能,实际处境是,每个电表记录员都有谈得来肩负的区域。同样,切面也不需要涵盖应用中的所有织入点,通过切点可以缩小切面接入应用时需要指定的界定。

假若说通告是概念了断面的whatwhen这六个地点,那么切点就定义了where。切点指定一个依旧六个织入点,而公告可以透过切点接入。平常状态下得以采用明确的类名和函数名或者定义了配合情势的正则表明式来定义切点;还有部分AOP框架帮助定义动态切点(dynamic
pointcuts),能够在运作时按照函数参数值决定是否接纳通知。

ASPECTS

当一名电表记录员开端一天的做事时,他很领会得知道自己要做什么(记录用电量)和去何地抄电表。同样地,布告和切点合起来就结成了断面——what、when和where。

INTRODUCTIONS

您可以经过introduction给现有的类增添方法如故性质。例如,可以定义一个布告类Auditable,用于保存某个对象被修改前的上一个处境——定义一个局部变量来保存这一个意况,然后采取setLastModified(Date)艺术设置意况。类似于设计形式中的装饰者模式——在不更改现有类的根底上为之扩张属性和方法。

WEAVING

编织值得是将切面应用于模板对象来创立代理类的经过,切面在指定的织入点被编织入目标对象。在目的对象生命周期的下列几个节点,可能爆发“编织”:

  • Compile
    time
    ——在编译过程中校切面织入到目的对象中,AspectJ的织入编译器是如此做的;
  • Class load
    time
    ——在将对象类加载到JVM时将切面织入到目标对象中,这亟需借助特定的ClassLoader,并且在织入在此之前修改目的对象的字节码文件。AspectJ
    5的load time weaving(LTW)支撑这种办法。
  • Runtime——在应用程序执行过程中将切面织入到目标对象中。一般而言,AOP容器会动态变化目的对象的代办,然后将切面织入到利用的实施过程。Spring
    AOP是这样做的。

以上就是有关AOP术语的基本介绍,接下去看看那一个概念在Spring中的实现。

4.1.2 Spring的AOP支持

二种AOP框架的要紧不同在于织入点模型:一些框架允许你在性质修改的层系应用文告,而任何框架则单纯辅助函数调用的层次应用通告;这一个框架在怎么样织入和哪天织入这两上边也势均力敌。即便存在这么些不同点,不过总体来讲AOP框架的效应是创制切点来定义织入点,使得切面可以被织入到应用程序的举办进程。

Spring对AOP的支撑来自以下四种样式:

  • 依照代理的Spring AOP
  • Pure-POJO aspects
  • 基于@AspectJ注解的aspects
  • 流入AspectJ aspects(所有版本的Spring都协助)

前二种属于Spring自己的AOP实现:Spring
AOP基于动态代理体制构建,也多亏因为那么些缘故,Spring
AOP仅仅协理函数调用级另外阻碍。

classic(经典)一词平常代表可以的创作,不过Spring的经典AOP编程模型并不是如此。该模型在它的一世是最好的,但是现在的Spring已经襄助更加清晰和易于的不二法门来面向切面编程。相对于更便于定义的AOP和基于注脚定义的AOP,Spring的经文AOP显得过于笨重和复杂了,因而在此地自己也不会详细介绍Spring
AOP。

通过Spring的aop名字空间,可以将pure
pojo转换成切面。实际上,这多少个POJO仅仅提供需在切点织入并施行的艺术,即便这需要基于XML配置文件,但这真的是一种阐明式得奖任何对象转换成切面的格局。

Spring借鉴AspectJ框架的筹划,引入了按照表明的AOP。本质上如故基于代理的AOP,可是编程模型则接近于AspectJ框架中被诠释修饰的断面。这种AOP情势的最大优点是不需要XML配置。

Spring
AOP技术可以完成简单的函数级拦截,例如构造函数、属性修改等等,可是只要需要贯彻更扑朔迷离的AOP效能,则应利用AspectJ框架。这篇随笔侧重介绍Spring
AOP技术,在上马在此以前,首先明白多少个举足轻重的点。

SPRING ADVICES IS WRITTEN IN JAVA

在Spring中创建的有着公告都是正规的Java类,切点可以透过声明或者XML文件定义,不过对于Java开发人士来说这二种形式都相比熟习。

即便AspectJ帮忙表明驱动的切面,它实质上是对Java的恢弘。这有好有坏:通过AOP-sepcific语言,你可以实现更全面的支配和更丰裕的效应,可是你也亟需学习一门新的工具和语法。

SPRING ADVICES OBJECT AT RUNTIME

在Spring
AOP框架中,通过在Spring管理的beans的外围包含一个代理类来将切面织入到这些beans。如下图所示,调用者跟代理类直接交流,代理类拦截函数调用,然后实施切面逻辑之后再调用真正的目的对象的章程。

基于代理体制落实AOP

唯有在应用需要运用被代理bean时,Spring才会创制代理对象。如若你利用ApplicationContext,代理的对象会在Spring从BeanFactory中加载bean的时候成立。由于Spring在运转时创制代理对象,因而Spring
AOP中不需要一定的编译器。

SPRING ONLY SUPPORTS METHOD JOIN POINTS

Spring
AOP仅仅补助函数级其它织入点,这不同于其他AOP框架,例如AspectJ和JBoss除了提供函数级此外织入点外,还援助属性和构造器级另外织入点。使用Spring
AOP无法实现细粒度的通告,例如拦截对某个属性的翻新;同样也无法在某个bean开始化的时候应用切通告。然而,基于函数级此外掣肘已经充裕满意开发者的大部需要了。

4.2 利用切点拔取织入点

正如在此以前提到的,切点的效率是指出切面的公告应该从哪个地方织入应用的执行流。和通报已于,切点也是整合切面的基本概念。

在Spring
AOP中,使用AspectJ的切点说明式语言定义切点。假诺您曾经熟悉使用AspectJ,那么在Spring中定义切点对您来说就很自然。假使你是AspectJ的新手,那么这节内容可以教会你怎么急速上手,写出AspectJ-style的切点。假如你指望详细学习AspectJ和AspectJ’s
expression language,那么我引进AspectJ inAction, Second
Edition

下列这个表格列出了在Spring AOP中可用的AspectJ的切点描述符:

Spring使用AspectJ的切点表明式语言定义切面

除此之外下面列出的之外,假设你打算利用AspectJ的另外描述符就会招致IllegalArgumentException非常。在地点这个描述符中,只有execution()实质上执行匹配操作,这是最关键的描述符,其他描述符用于协理。

4.2.1 编写切点

率先定义一个Performance接口:

package concert;

public interface Performance {
    public void perform();
}

Performance代表任何现场表演,例如舞台剧、电影或音乐会。就算你需要写一个断面,该切面会覆盖Performanceperform()情势。下图呈现了怎么着定义一个切点,满意这多少个切点定义的不二法门在推行时会触发公告任务履行。

拔取切点表明式接纳要影响的法子

在此间运用execution()叙述符接纳Performanceperform方法:第一个*表示不关心函数的归来类型;接下去需要列出总体的类签名和形式名;对于函数参数列表,使用”..”表示不关心函数的参数列表。

固然你需要限制切点的效劳范围仅在concert包种,可以利用within()描述符,如下图所示:

经过within()描述符限制切点的法力范围

使用&&标志代表与涉及,类似得,使用||代表或关系、使用!意味着非关系。在XML文件中采用andornot这五个记号。

4.2.2 在切点中引用bean

除外表4.1中列出的描述符,Spring还提供了一个bean()描述符,用于在切点表明式中引用bean。举个例子,如下所示的代码表示:你需要将切面应用于Performanceperform办法上,不过仅限于ID为woodstock的bean。

execution(* concert.Performance.perform(..))  and bean('woodstock')

一样也得以撤废指定的bean,例子代码如下:

execution(* concert.Performance.perform(..))  and !bean('woodstock')

4.3 利用阐明创造切面

在AspectJ 5中引入的最根本的特色就是选用注明创立切面。

4.3.1 定义切面

亚洲城ca88手机版下载地址,比方没有观众,一场演出不可以称为真正的演出。当您站着表演的角度考虑,观众是重中之重的,可是这并不是演出应该处理的最着重的做事,这五个关注点不同。由此,需要将观众定义为一个断面,然后使用在表演上。

Audience类的代码如下所示:

package com.spring.sample.concert;

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

@Aspect
public class Audience {
    @Before("execution(* com.spring.sample.concert.Performance.perform( .. ))")
    public void silenceCellPhones() {
        System.out.println("Silencing cell phones");
    }

    @Before("execution(* com.spring.sample.concert.Performance.perform( .. ))")
    public void takeSeats() {
        System.out.println("Taking seats");
    }

    @AfterReturning("execution(* com.spring.sample.concert.Performance.perform( .. ))")
    public void applause() {
        System.out.println("CLAP CLAP CLAP!!!");
    }

    @AfterThrowing("execution(* com.spring.sample.concert.Performance.perform( .. ))")
    public void demandRefund() {
        System.out.println("Demand a refund");
    }
}

在此处运用@Aspect诠释修饰Audience类,表示该类是一个断面,该类中定义的措施都用来实践该切面的不等效率。

Audience类中的几个办法定义了观众在见到表演时或许部分反应。在表演起先以前,观众应该按时就坐(takeSeats())并将手机静音(silenceCellPhones());假使表演很美丽,观众就会鼓掌(applause()),假若表演出现故障和奇怪情况,观众就会要求退票(demandRefund())。

那多少个点子都被打招呼注脚修饰,用于指定啥时候调用对应的法子。AspectJ提供了五种概念通告的注脚,如下表所示:

Spring使用AspectJ的笺注定义通告

Audience类用到了中间的二种,takeSeats()silenceCellPhones()主意都是由@Before诠释修饰,表示这两个主意应该在表演起始在此以前被调用;applause()方法被@AfterReturning讲明修饰,表示该格局是在演艺圆满截至之后被调用;demandRefund()方法被@AfterThrowing注脚修饰,表示一旦表演过程中出现意外,则会调用该形式。

具有那么些通告注脚都传出了一个切点表明式作为参数,这么些参数可能会不同,不过在咱们明日的这么些事例中是一律的,为了免去代码重复,可以行使@Pointcut注脚定义可重复使用的切点,下列是自己修改过后的Audience代码。

package com.spring.sample.concert;

import org.aspectj.lang.annotation.*;

@Aspect
public class Audience {
    @Pointcut("execution(* com.spring.sample.concert.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("Demand a refund");
    }
}

除了作为标志的performance()方法,Audience类完全是一个POJO,由此它也足以像平时Java类一样使用:

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

到此截止,Audience单独是位于Spring容器中的一个bean,尽管它被AspectJ阐明修饰,假设没有其它配置解释这一个注脚,并创立可以将它转换成切面的代办,则它不会被看作切面使用。

比方您拔取JavaConfig,则足以由此类级其余@EnableAspectJAutoProxy讲明开启自动代理体制,例子代码如下所示:

package com.spring.sample.concert;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy //开启AspectJ的自动代理机制
@ComponentScan
public class ConcertConfig {
    @Bean
    public Audience audience() { //定义Audience的bean
        return new Audience();
    }
}

只要您利用XML配置,则可以运用<aop: aspectj-autoproxy
/>
要素开启AspectJ的全自动代理体制,对应的布局代码如下:

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

       <context:component-scan base-package="com.spring.sample.concert" /> 

       <aop:aspectj-autoproxy />

       <bean class="com.spring.sample.concert.Audience" />
</beans>

无论是接纳JavaConfig依旧XML配置文件,AspectJ的电动代理体制使用由@Aspect诠释修饰的bean为这么些被切点指定的beans创立代理。在那个事例中,将会为Performance接口创造代理,并在perform()格局调用前如故调用后选拔切面中的通告方法。

特意要铭记:Spring中的AspectJ自动代理体制本质上依然Spring中基于代理的切面,因而,固然您采纳了@Aspect诠释,不过仍然仅能帮助函数调用级此外阻拦。假使您愿意接纳AspectJ的效益,那么你得使用AspectJ的运行时还要不要采用Spring创立基于代理的切面。

围绕通告(around advice)与另外通知类型不同,由此值得用一小节单独论述。

4.3.2 创设环绕通知

围绕公告精神上是将停放布告、后置通告和特别通告整合成一个单身的通报。为了演示环绕布告的用法,我们需要再行重写Audience断面——仅使用一个独立的通知,代替四个分其余通报方法。

package com.spring.sample.concert;

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

@Aspect
public class Audience {
    @Pointcut("execution(* com.spring.sample.concert.Performance.perform( .. ))")
    public void performance() {}

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("Silencing cell phones");
            System.out.println("Taking seats");
            joinPoint.proceed();
            System.out.println("CLAP CLAP CLAP!!!");
        } catch (Throwable e) {
            System.out.println("Demanding a refund");
        }
    }
}

@Around讲明表示watchPerformance()方法将用作环绕通告应用在与切点——performance()配合的主意上。这多少个主意实现的功用跟从前的两个函数完全相同,可是有好几例外,即该函数有一个参数——ProceedingJoinPoint实例,这里需要经过这些参数主动调用业务函数——joinPoint.proceed();。在围绕通告中必须调用proceed()主意,假如没有,则应用的执行会阻塞在通知方法中。

您还足以在一个文告中频繁调用proceed()主意,从而可以实现重试逻辑——业务逻辑可能破产,可以限制失利重试的次数。

4.3.3 处理通报中的参数

截至近期停止,大家编辑的切面都相当简单——没有接受输入参数。仅部分例外是围绕公告中需要动用ProceedingJoinPoint参数,除此之外其他通知都不曾带走任何参数传入被打招呼的模式中,这是因为perform()艺术本身不需要任何参数。

一旦你的切面要通报的是一个带参数的函数?切面是否能访问传入函数的参数并应用它们?
举个例子表明,BlankDisc类中有一个play()措施,该办法的意义是遍历所有的tracks并动用每个track对象调用playTrack()方法。

package com.spring.sample.soundsystem;

import org.springframework.stereotype.Component;
import java.util.List;

@Component
public class BlankDisc implements CompactDisc {
    private String title;
    private String artist;
    private List<String> tracks;

    public BlankDisc() {
    }

    public BlankDisc(String artist, String title, List<String> tracks) {
        this.artist = artist;
        this.title = title;
        this.tracks = tracks;
    }

    public void play() {
        System.out.println("Playing " + title + " by " + artist);
        for (String track: tracks) {
            System.out.println("-Track: " + track);
        }
    }

    public void playTrack(int num) {
        System.out.println("-Track: " + tracks.get(num));
    }
    //setter和getter在此处省略
}

前些天你希望记录每个track被调用的次数,一种办法是从来改动playTrack()艺术,通过一个全局变量(例如Map数据结构)记录每个track被调用的次数。可是,track-counting这些逻辑跟play
track实际上是多少个不等的关注点,由此应当考虑通过AOP实现。

率先定义一个断面,即TrackCounter类,并在playTrack()格局出举办通报,代码可列举如下:

package com.spring.sample.soundsystem;

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

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

    @Pointcut(
            "execution(* com.spring.sample.soundsystem.CompactDisc.playTrack( .. )) " +
            "&& args(trackNumber)")
    public void trackPlayed(int trackNumber) {}

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

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

跟上一小节创造的断面类似,首先利用@Pointcut注脚定义一个切点,然后使用@Before阐明定义前置公告。不同的地方在于切点的概念,除了指定被通报的法子,还点名了被打招呼方法需要的参数trackNumber。下图展现什么领会切点的概念。

关键在于args(trackNumber)标识符,那意味着每个传入业务函数playTrack()int参数也将被传播通告,而且,args()中参数的称谓必须跟切点方法的签署中的参数名称相同,例如:

@Pointcut(
        "execution(* com.spring.sample.soundsystem.CompactDisc.playTrack( .. )) " +
        "&& args(ex)")
public void trackPlayed(int ex) {}

同样,@Before诠释中使用切点函数定义的参数名称,也非得与公告方法签名中的参数完全相同,例如:

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

然后在Spring的部署文件中布局BlankDiscTrackCounter,并开启AspectJ自动代理体制,配置文件代码如下:

package com.spring.sample.soundsystem;

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;

@Configuration
@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("Lucky in the Sky with Diamonds");
        tracks.add("Getting Better");
        tracks.add("Fixing a Hole");
        tracks.add("testtest");
        tracks.add("hhhhhhhhhh");
        cd.setTracks(tracks);
        return cd;
    }

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

终极,为了表明大家的想法,需要写个单元测试用例举办表达,代码如下:

package com.spring.sample.soundsystem;

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 org.junit.Assert.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TrackCounterConfig.class)
public class TrackCounterTest {
    @Autowired
    private CompactDisc cd;

    @Autowired
    private TrackCounter counter;

    @Test
    public void testTrackCounter() {
        cd.playTrack(0);
        cd.playTrack(1);
        cd.playTrack(2);
        cd.playTrack(2);
        cd.playTrack(2);
        cd.playTrack(2);
        cd.playTrack(6);
        cd.playTrack(6);

        assertEquals(1, counter.getPlayCount(0));
        assertEquals(1, counter.getPlayCount(1));
        assertEquals(4, counter.getPlayCount(2));
        assertEquals(0, counter.getPlayCount(3));
        assertEquals(0, counter.getPlayCount(4));
        assertEquals(0, counter.getPlayCount(5));
        assertEquals(2, counter.getPlayCount(6));
    }
}

TrackCounter这些切面可以在显存函数的根底上举办更进一步封装,可是除了函数封装,还足以采用切面给被打招呼的对象引入新的功用。

4.3.4 使用基于阐明的切面引入新功效

在部分动态语言(Ruby、Groovy)中,存在开放类的表征,这种特点帮助在不修改原来类或者目的的底蕴上为此类添加新点子。不过,Java不是动态语言,一旦一个类被编译,你几乎无法再对它举行改动。

不过,仔细揣摩下,上述说的那些要求:在不修改原有类的基础上为此类添加新模式,这不正是切面可以成功的工作么?在上个小节的事例中我们是为原有类的艺术添加了新的效用,同样,也足以为原本的类添加新的情势。那里通过AOP引出一个新的概念引入(introductions),即由此切面为Spring的beans增添新形式。

Spring中切面的真相就是一个代理对象,这么些代理对象与眼前目的实现同一个接口。既然如此,那么可以扩张一下,假如代理对象实现新的接口呢?那样被这么些切面通知的bean就仿佛又实现了一个新的接口——扩展了新的效率,即便底层并没有改动原来的类。下图显示了这多少个思路:

透过Spring AOP可以给bean引入新的措施

当introduced接口的某部方法被调用时,代理对象会把那一个调用委托给一个实现了该introduced接口的目的。对于外部而言,就类似一个bean实现了四个接口。

举个例子,若是你要把下部那些Encoreable接口引入给Performance接口的其余实现。

public interface Encoreable {
    void performEncore();
}

你当然可以让原本Performance接口的贯彻也还要落实这么些接口,可是首假诺并不是有着的Performance落实都急需引入Encoreable;而且,从使用维护的角度看,全体改动Performance的兑现容易引入新的bug;另外,假使Performance接口来自第三方库,你也未尝艺术接触到源码。

这就是说利用Spring AOP如何操作呢?
先是准备一个introduced接口的默认实现类,代码如下:

package com.spring.sample.concert;

public class DefaultEncoreable implements Encoreable {
    public void performEncore() {
        System.out.println("perform the encore!");
    }
}

然后新建一个断面,即EncoreableIntroducer类,代码列举如下:

package com.spring.sample.concert;

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

@Aspect
public class EncoreableIntroducer {
    @DeclareParents(value = "com.spring.sample.concert.Performance+",
                    defaultImpl = DefaultEncoreable.class)
    public static Encoreable encoreable;
}

EncoreableIntroducer是一个断面,但和事先学过的切面不同在于它并未概念各样通知,它经过@DeclareParents注解将Encoreable接口引入到Performance接口的贯彻中。

@DeclareParents表明的构成包括三点:

  • value性能用于匹配那多少个beans需要被引入这多少个新的接口。在这多少个事例中是独具Performance的兑现都被引入了新的接口(最终的十分+表示,所有Performance的子类型,除了Performance自己)。
  • defaultImpl特性用于指定一个新引入的接口的贯彻,在那边我们提供了DefaultEncoreable类;
  • 引入的新接口被定义为public static的性能,这里引入了Encoreable接口

跟其他切面的用法类似,需要在Spring应用上下文中定义EncoreableIntroducer
bean,假设拔取JavaConfig,则代码如下:

@Bean
public EncoreableIntroducer encoreableIntroducer() {
    return  new EncoreableIntroducer();
}

Spring
的自发性代理体制从这里收获这多少个bean。当Spring发现一个被@Aspect诠释修饰的bean,就会自动为它创制一个代理对象,负责将表面的函数调用委托给目的bean或者新引入接口的落实,至于由哪个实现负责履行,取决于这多少个函数属于原接口依然新引入的接口。

书中没有的
要是那个小节只说到这,你或许会有疑惑,那你说的那么些引入新接口这么牛,什么情状下怎么使用呢?针对这些疑惑,我写了一个单元测试,代码如下:

package com.spring.sample.soundsystem;

import com.spring.sample.concert.ConcertConfig;
import com.spring.sample.concert.Encoreable;
import com.spring.sample.concert.Performance;
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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ConcertConfig.class)
public class EncoreIntroducerTest {
    @Autowired
    private Performance musicPerformance;

    @Test
    public void testEncore() {
        Encoreable encoreable = (Encoreable)musicPerformance; //使用方法
        encoreable.encore();
    }
}

可以看出,本来musicPerformancePerformance的兑现,通过强转,我可以调用新接口中的方法了,而且从不改动原来的类和接口;而当中负责将函数调用委托给不同的兑现目标的职责就是由切面自动完成。

4.4 在XML文件中定义切面

倘诺不经过注脚定义切面和通报,那么就不得不采用使用XML文件。Spring的aop名字空间提供了下列那么些元素定义切面。

Spring AOP的部署元素

Spring AOP的布置元素(续)

在此以前我们曾经理解过可以动用<aop:
aspectj-autoproxy>
要素启用AspectJ自动代理体制。这里将了然下其他与注解定义格局十分的XML元素的用法。

第一,将事先的Audience中的阐明都去掉,留下的代码如下:

package com.spring.sample.concert;

public class Audience {
    public void silecneCellPhones() {
        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 a refund");
    }
}

可以看出,现在的Audience就是一个平时的Java类,倘若不定义额外的通告和切点,就无奈让Audience作为一个断面去起成效。

4.4.1 定义前置和后置通告

在XML文件中定义前置和后置公告的代码如下:

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

先是需要精通,大部分Spring
AOP配置元素需要在<aop:config>要素的前后文中使用。除了定义切面对应的bean,否则一般皆以<aop:config>开始。

<aop:config>中,一般需要定义一个或者五个关照,切面和切点。在上头的代码中,首先定义了一个断面,该切面引用了audience这多少个bean;在切面中定义了放置公告、后置公告和这个文告:method特性指定某个公告对应的措施,pointcut用来指定切点,即在什么地方应用通告。下图演示了什么将这多少个通告编织进现实的事情逻辑。

涵盖三个通知的切面奥迪ence将通告的逻辑织入到事情方法的推行进程

@Pointcut讲明对应的XML元素是<aop:
pointcut>
,可以消除重复代码,下列的XML配置可以实现均等的机能:

<aop:config>
       <aop:aspect ref="audience">
           <aop:pointcut id="performance" expression="execution(* com.spring.sample.concert.Performance.perform( .. ))"/>
           <aop:before method="silecneCellPhones"
                       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>要素定义了一个切点,然后在通知中利用pointcut-ref引用它。这里的切点定义在切面audience的成效范围内,也可以定义一个切点让多少个断面共用。

4.4.2 制造环绕公告

围绕通告@Around在XML这边的附和元素是<aop: around>
首先修改Audienc类,代码如下:

package com.spring.sample.concert;

import org.aspectj.lang.ProceedingJoinPoint;

public class Audience {
    public void watchPerformance(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("Silencing cell phones");
            System.out.println("Taking seats");
            joinPoint.proceed();
            System.out.println("CLAP CLAP CLAP!!!");
        } catch (Throwable e) {
            System.out.println("Demanding a refund");
        }
    }
}

接下来在XML配置文件中修改,用环绕通知代替此前的七个通告,在此以前用过的切点performance可以采纳,method属性改成watchPerformance即可,配置代码如下:

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

4.4.3 给通告传递参数

在4.3.3中,可以使用AspectJ注脚创设一个断面,用于记录每个track被调用的次数,同样可以利用XML完成这多少个意义。

首先将TrackCounter中的阐明全体去掉,剩下的POJO的代码如下所示:

package com.spring.sample.soundsystem;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
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 = getPlayCount(trackNumber);
        trackCounts.put(trackNumber, currentCount + 1);
    }

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

原书是将bean的定义全体在xml中再度定义,我为了便利就无冕使用(可是是将AOP相关的配备放在XML文件中),bean的安排代码如下:

package com.spring.sample.soundsystem;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;import java.util.List;

@Configuration
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("Lucky in the Sky with Diamonds");
        tracks.add("Getting Better");
        tracks.add("Fixing a Hole");
        tracks.add("testtest");
        tracks.add("hhhhhhhhhh");
        cd.setTracks(tracks);
        return cd;
    }
    @Bean
    public TrackCounter trackCounter() {
        return new TrackCounter();
    }
}

接下来在XML文件中引入上述TrackCounterConfig配置文件,并定义AOP相关的配备,XML文件如下所示:

<?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-4.0.xsd"> 

   <bean class="com.spring.sample.soundsystem.TrackCounterConfig" /> 
   <aop:config>
              <aop:aspect ref="trackCounter">
                  <aop:pointcut id="trackPlayed"
                                expression="execution(* com.spring.sample.soundsystem.CompactDisc.playTrack(int))                                 and args(trackNumber) "/>
                  <aop:before method="countTrack"
                              pointcut-ref="trackPlayed"/>
              </aop:aspect>
    </aop:config>
    <aop:aspectj-autoproxy />
</beans>

如故接纳以前的测试用例TrackCounterTest展开测试,唯一需要转移的是:在测试用例头部导入xml配置文件。我们在这里将concer.xml文件作为总的配置文件,部分代码如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:concert.xml")
public class TrackCounterTest {
   ....
}

再有一个索要留意的,在XML配置文件中,一般接纳and、or和not代替Java文件中采纳的&&、||和!

4.4.4 使用切面引入新的效能

在4.3.4小节,我们介绍了怎么样使用Spring的AOP技术为现有类扩大额外的法门——通过@DeclareParents注脚给被布告的措施引入新的措施,能够行使<aop:
declare-parent>
要素实现平等的法力。

下列这么些XML代码片段跟在此以前的注释形式的引入功用雷同:

<aop:aspect>
    <aop:declare-parents types-matching="com.spring.sample.concert.Performance+"
                         implement-interface="com.spring.sample.concert.Encoreable"
                         default-impl="com.spring.sample.concert.DefaultEncoreable"/>
</aop:aspect>
  • types-match特性的功用和前边的value性能相同,用于指定被打招呼的bean;
  • implement-interface属性的效率和事先的静态变量相同,用于指定新接口;
  • defautl-impl属性的效果是指定新接口的一个默认实现类;那一个特性还足以采纳delegate-ref属性代替,然而需要在spring上下文中定义DefaultEncoreable的bean。

4.5 注入AspectJ的切面

Spring AOP的上述意义已经得以应付大部分需要,此处暂不深究。

4.6 总结

对于面向对象编程技术,AOP是一个效能强大的填补。利用切面,你可以将这个关系使用两个模块的通用功用集中在一个模块中。你可以定义在啥地方以及哪些行使这么些意义。这降低了代码重复,并且使得业务逻辑类专注于主题业务。

在pom文件中要加的借助有:

<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-aop</artifactId>
   <version>${spring.version}</version>
</dependency>
<!-- AspectJ -->
<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjrt</artifactId>
   <version>${aspectj.version}</version>
</dependency>
<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjweaver</artifactId>
   <version>${aspectj.version}</version>
</dependency>