主要内容

  • 面向切面编程的基本知识
  • 为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 定义切面

如果没有观众,一场表演不能称之为真正的表演。当你站着表演的角度思考,观众是重要的,但是那并不是表演应该处理的最主要的工作,这两个关注点不同。因此,需要将观众定义为一个切面,然后应用在表演上。

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; @Aspectpublic 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.*;

@Aspectpublic 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类一样使用:

@Beanpublic 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的自动代理机制@ComponentScanpublic class ConcertConfig {
@Beanpublic Audience audience() { //定义Audience的beanreturn 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.*; @Aspectpublic 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; @Componentpublic 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; @Aspectpublic 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@EnableAspectJAutoProxypublic class TrackCounterConfig {
@Beanpublic 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;
} @Beanpublic 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,则代码如下:

@Beanpublic 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 {
@Autowiredprivate Performance musicPerformance; @Testpublic 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用于指定切点,即在哪里应用通知。下图演示了如何将这些通知编织进具体的业务逻辑。


包含四个通知的切面Audience将通知的逻辑织入到业务方法的执行过程

@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; @Configurationpublic class TrackCounterConfig {
@Beanpublic 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;
}
@Beanpublic 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>

Spring实战4:面向切面编程的更多相关文章

  1. Spring实战Day7面向切面编程术语介绍

    #### 面向切面编程 为什么需要切面? 有些功能需要在应用中的多个地方使用到,但是我们又不想在着每个地方都调用他们 切面术语 通知(advice):切面需要完成的工作 通知的类型(什么时间完成工作) ...

  2. Spring框架系列(4) - 深入浅出Spring核心之面向切面编程(AOP)

    在Spring基础 - Spring简单例子引入Spring的核心中向你展示了AOP的基础含义,同时以此发散了一些AOP相关知识点; 本节将在此基础上进一步解读AOP的含义以及AOP的使用方式.@pd ...

  3. Spring:AOP面向切面编程

    AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果. AOP是软件开发思想阶段性的产物,我们比较熟悉面向过程O ...

  4. Spring AOP:面向切面编程,AspectJ,是基于spring 的xml文件的方法

    导包等不在赘述: 建立一个接口:ArithmeticCalculator,没有实例化的方法: package com.atguigu.spring.aop.impl.panpan; public in ...

  5. Spring AOP:面向切面编程,AspectJ,是基于注解的方法

    面向切面编程的术语: 切面(Aspect): 横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象 通知(Advice): 切面必须要完成的工作 目标(Target): 被通知的对象 代理(Pr ...

  6. Spring框架 AOP面向切面编程(转)

    一.前言 在以前的项目中,很少去关注spring aop的具体实现与理论,只是简单了解了一下什么是aop具体怎么用,看到了一篇博文写得还不错,就转载来学习一下,博文地址:http://www.cnbl ...

  7. Spring AOP(面向切面编程)

    一.AOP简介 1.AOP概念:Aspect Oriented Programming 面向切面编程 2.作用:本质上来说是一种简化代码的方式 继承机制 封装方法 动态代理 …… 3.情景举例 ①数学 ...

  8. Spring的AOP面向切面编程

    什么是AOP? 1.AOP概念介绍 所谓AOP,即Aspect orientied program,就是面向方面(切面)的编程. 功能: 让关注点代码与业务代码分离! 关注点: 重复代码就叫做关注点: ...

  9. Spring中的面向切面编程(AOP)简介

    一.什么是AOP AOP(Aspect-Oriented Programming, 面向切面编程): 是一种新的方法论, 是对传统 OOP(Object-Oriented Programming, 面 ...

  10. 程序员笔记|Spring IoC、面向切面编程、事务管理等Spring基本概念详解

    一.Spring IoC 1.1 重要概念 1)控制反转(Inversion of control) 控制反转是一种通过描述(在java中通过xml或者注解)并通过第三方去产生或获取特定对象的方式. ...

随机推荐

  1. Core Java Volume I — 3.4. Variables

    3.4. VariablesIn Java, every variable has a type. You declare a variable by placing the type first, ...

  2. Python天天美味(13) - struct.unpack

    转载自:http://www.cnblogs.com/coderzh/archive/2008/05/04/1181462.html Python中按一定的格式取出某字符串中的子字符串,使用struc ...

  3. CentOS下解决”用户账户is not in the sudoers file“问题

    如上图,在当前用户cent(我的用户名)下使用sudo命令时,提示"cent is not in the sudoers file. This incident will be report ...

  4. ZOJ 1067 Color Me Less

    原题链接 题目大意:一道类似于简单图像压缩的题目.给定一个调色板,然后把24位真彩色按照就近原则聚类. 解法:每个像素的色彩都是RGB三个值,相当于三维空间的一个点.所以当一个新的像素进来时,分别和调 ...

  5. Web上下文配置【MvcConfig】

    基于Servlet3.0规范和SpringMVC4注解式配置方式,实现零xml配置,弄了个小demo,供交流讨论. 项目说明如下: 1.db.sql是项目中用到的表,数据库使用的是oracle11g ...

  6. 1-3-2 Windows应用程序常用消息

    主要内容:介绍Windows编程中常用的消息 1.WM_LBUTTONDOWN产生单击鼠标左键的消息 lParam: 低字节包含当前光标的X坐标值 X = LOWORD(lParam); 高字节包含当 ...

  7. poj2240 最短路判环

    题意:与poj1680一样,有不同的换钱渠道,可以完成特定两种货币的交换,并且有汇率,只不过此题是单向边,然后问是否能使财富增加 与poj1680一样,建图之后直接spfa判增值的环即可 #inclu ...

  8. Linux服务器上监控网络带宽的18个常用命令(转)

    本文介绍了一些可以用来监控网络使用情况的Linux命令行工具.这些工具可以监控通过网络接口传输的数据,并测量目前哪些数据所传输的速度.入站流量和出站流量分开来显示. 一些命令可以显示单个进程所使用的带 ...

  9. urllib2

    import urllib2response = urllib2.urlopen("http://www.baidu.com")print response.read() urlo ...

  10. 检测php网站是否已经被攻破

    from :http://www.gregfreeman.org/2013/how-to-tell-if-your-php-site-has-been-compromised/ http://drop ...