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>元素规定的那样在该对象上执行依赖注入。