在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。
如果要重用通用功能的话,最常见的面向对象技术是继承(inheritance)或委托(delegation)。但是,如果在整个应用中都使用相同的基类,继承往往会导致一个脆弱的对象体系;而使用委托可能需要对委托对象进行复杂的调用。
切面提供了取代继承和委托的另一种可选方案,而且在很多场景下更清晰简洁。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。
与大多数技术一样,AOP已经形成了自己的术语。描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point)。下图展示了这些概念是如何关联在一起的。
在AOP术语中,切面的工作被称为通知。
通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?
Spring切面可以应用5种类型的通知:
我们的应用可能有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些AOP框架允许我们创建动态的切点,可以根据运行时的决策(比如方法的参数值)来决定是否应用通知。
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。
引入允许我们向现有的类添加新方法或属性。
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
小结:
通知包含了需要用于多个应用对象的横切行为;连接点是程序执行过程中能够应用通知的所有点;切点定义了通知被应用的具体位置(在哪些连接点)。其中关键的概念是切点定义了哪些连接点会得到通知。
并不是所有的AOP框架都是相同的,它们在连接点模型上可能有强弱之分。有些允许在字段修饰符级别应用通知,而另一些只支持与方法调用相关的连接点。它们织入切面的方式和时机也有所不同。但是无论如何,创建切点来定义切面所织入的连接点是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来完成功能。
通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。如下图所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。
直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。
因为Spring基于动态代理,所以Spring只支持方法连接点。这与一些其他的AOP框架是不同的,例如AspectJ和JBoss,除了方法切点,它们还提供了字段和构造器接入点。Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,我们就无法在bean创建时应用通知。
切点用于准确定位应该在什么地方应用切面的通知。通知和切点是切面的最基本元素。在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。
关于Spring AOP的AspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器(pointcut designator)的一个子集。让我们回顾下,Spring是基于代理的,而某些切点表达式是与基于代理的AOP无关的。下表列出了Spring AOP所支持的AspectJ切点指示器。
在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgument-Exception异常。
当我们查看如上所展示的这些Spring支持的指示器时,注意只有execution指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的。这说明execution指示器是我们在编写切点定义时最主要使用的指示器。在此基础上,我们使用其他指示器来限制所匹配的切点。
例:
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可以分别用来代替“||”和“!”。
除了上表所列的指示器外,Spring还引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标识bean。bean()使用bean ID或bean名称作为参数来限制切点只匹配特定的bean。
例如,考虑如下的切点:
execution(* concert. Per formance. perform() )
and bean( 'woodstock' )
在这里,我们希望在执行Performance的perform()方法时应用通知,但限定bean的ID为woodstock。
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自动代理
<-- 启用AspctJ自动代理 -->
不管是使用JavaConfig还是XML,AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。在这种情况下,将会为Concertbean创建一个代理,Audience类中的通知方法将会在perform()调用前后执行。
Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是Spring基于代理的切面。这一点非常重要,因为这意味着尽管使用的是@AspectJ注解,但我们仍然限于代理方法的调用。如果想利用AspectJ的有能力,我们必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面。
环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。
例:
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()方法。如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。有可能这就是你想要的效果,但更多的情况是你希望在某个点上执行被通知的方法。
例:
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 trackCounts =
new HashMap();
// 通知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 tracks = new ArrayList();
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));
}
}
例:
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注解由三部分组成:
和其他的切面一样,我们需要在Spring应用中将EncoreableIntroducer声明为一个bean:
Spring的自动代理机制将会获取到它的声明,当Spring发现一个bean使用了@Aspect注解时,Spring就会创建一个代理,然后将调用委托给被代理的bean或被引入的实现,这取决于调用的方法属于被代理的bean还是属于被引入的接口。
在Spring中,注解和自动代理提供了一种很便利的方式来创建切面。它非常简单,并且只涉及到最少的Spring配置。但是,面向注解的切面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到这一点,必须要有源码。
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通知的所有条件。我们再稍微帮助它一把,它就能够成为预期的通知了。
例:通过XML将无注解的Audience声明为切面
在
Audience切面包含四种通知,它们把通知逻辑织入进匹配切面切点的方法中:
如下的XML展现了如何将通用的切点表达式抽取到一个切点声明中,这样这个声明就能在所有的通知元素中使用了。
例:使用
现在切点是在一个地方定义的,并且被多个通知元素所引用。
到目前为止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()方法包含了之前四个通知方法的所有功能。不过,所有的功能都放在了这一个方法中,因此这个方法还要负责自身的异常处理。
声明环绕通知与声明其他类型的通知并没有太大区别。我们所需要做的仅仅是使用
例:在XML中使用
例:无注解的TrackCounter
package soundsystem;
import java.util.HashMap;
import java.util.Map;
public class TrackCounter{
private Map trackCounts =
new HashMap();
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配置为参数化的切面
aaa
bbb
ccc
ddd
在前面我们借助AspectJ的@DeclareParents注解为被通知的方法神奇地引入新的方法。但是AOP引入并不是AspectJ特有的。使用Spring aop命名空间中的
如下的XML代码片段与之前基于AspectJ的引入功能是相同:
顾名思义,
这里有两种方式标识所引入接口的实现。在本例中,我们使用default-impl属性用全限定类名来显式指定Encoreable的实现。或者,我们还可以使用delegate-ref属性来标识。
delegate-ref属性引用了一个Spring bean作为引入的委托。这需要在Spring上下文中存在一个ID为encoreableDelegate的bean。
使用default-impl来直接标识委托和间接使用delegate-ref的区别在于后者是Spring bean,它本身可以被注入、通知或使用其他的Spring配置。
虽然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。
Worst performance ever !
I laughed, I cried, then I realized I was at the
wrong show.
A must see show!
如果想使用Spring的依赖注入为AspectJ切面注入协作者,那我们就需要在Spring配置中把切面声明为一个Spring配置中的
很大程度上,
因为Spring不能负责创建CriticAspect,那就不能在 Spring中简单地把CriticAspect声明为一个bean。相反,我们需要一种方式为
Spring获得已经由AspectJ创建的CriticAspect实例的句柄,从而可以注入CriticismEngine。幸好,所有的AspectJ切面都提供了一个静态的aspectOf()方法,该方法返回切面的一个单例。所以为了获得切面的实例,我们必须使用factory-method来调用asepctOf()方法而不是调用CriticAspect的构造器方法。
简而言之,Spring不能像之前那样使用