3.2 创建环绕通知
环绕通知是最为强大的通知类型。它能够让你编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。下面重写Audience
切面:
package concert;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@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()
切面的环绕通知。在这个通知中,观众在演出之前会将手机调至静音并就坐,演出结束后会鼓掌喝彩。这和前面一样。关于这个新的通知方法,首先注意到的可能是它接收ProceedingJoinPoint
作为参数。这个对象是必须要有的,因为要在通知中通过它来调用被通知的方法。通知方法中可以做任何事情,当要将控制权交给被通知的方法时,它需要调用proceed()
方法。
3.3 处理通知中的参数
以上的例子中都没有任何参数,唯一例外的是我们为环绕通知所编写的watchPerformance()
示例方法中使用了ProceedingJoinPoint
作为参数。这是因为我们所通知的perform()
方法本身没有任何参数,如果有呢?这里通过例子说明,之前有过一个BlankDisc
类:
package soundsystem;
import java.util.List;
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
private List<String> tracks;
public BlankDisc(String title, String artist, List<String> tracks) {
this.title = title;
this.artist = artist;
this.tracks = tracks;
}
public void play() {
System.out.println("Playing " + title + " by " + artist);
for (String track : tracks) {
System.out.println("-Track: " + track);
}
}
}
说明:play()
方法会循环所有的磁道并调用playTrack()
方法。但是,我们也可以通过playTrack()
方法直接播放某一个磁道中的歌曲。这里如果想记录每个磁道被播放的次数。一种方法就是修改playTrack()
方法,直接在每次调用的时候记录这个数量,但是,记录磁道的播放次数与播放本身是不同的关注点,因此不应该属于playTrack()
方法。这里为了记录每个磁道所播放的次数,我们创建TrackCounter
类,它是通知playTrack()
方法的一个切面:
package soundsystem;
import org.aspectj.lang.annotation.*;
import java.util.HashMap;
import java.util.Map;
@Aspect
public class TrackCounter {
private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();
@Pointcut("execution(* soundsystem.CompactDisc.playTrack(int)) && 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;
}
}
说明:这里使用args(trackNumber)
限定符。表明传递给playTrack()
方法的int类型参数也会传递到通知中去。参数的名称trackNumber
也与切点方法签名中的参数相匹配(其实是当被通知方法被调用时,会有一个参数向其传递过来,这个参数会被切面接收到)。到目前为止,在使用的切面中,所包装的都是被通知对象的已有方法。但是,方法包装仅仅是切面所能实现的功能之一。下面看一下如何通过切面,为被通知的对象引入全新的功能。
3.4 通过注解引入新功能
在Spring
中,切面只是实现了它们所包装bean
相同接口的代理。如果除了实现这些接口,代理也能暴露新接口的话,会怎样呢?那样的话,切面所通知的bean
看起来像是实现了新的接口,即便底层实现类并没有实现这些接口也无所谓。如图所示。
为了验证该注意能行得通,我们要为所有的
Performance
实现引入下面的Encoreable
接口:
package concert;
public interface Encoreable {
void performance();
}
如果想让所有Performance
实现都能引入Encoreable
接口中的方法,那么所有实现都必须修改,让它们都实现此接口才行。但是这并不是一个好的设计。这里借助AOP
的引入功能,首先创建一个新的切面:
package concert;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value="concert.Performance+", defaultImpl = DefaultEncoreable.class)
public static Encoreable encoreable;
}
说明:这和前面的切面有所不同,这里通过@DeclareParents
注解将Encoreable
接口引入到Performance bean
中。@DeclareParents
注解由三部分组成:
-
value
属性指定了哪种类型的bean
要引入该接口。在本例中,也就是所有实现Performance
的类型(标记符后买年的加号表示是Performance
的所有子类型,而不是Performance
本身)。 -
defaultImpl
属性指定了为引入功能提供实现的类。在这里是DefaultEncoreable
提供具体实现。 -
@DeclareParents
注解所标注的静态属性指明了要引入的接口。在这里,我们所引入的是Encoreable
接口。
同时,我们也要将EncoreableIntroducer
声明为一个bean
。最后,在Spring
中,注解和自动代理提供了一种很便利的方式来创建切面。但是,面向注解的切面声明有一个明显的劣势,就是必须能够为通知类添加注解,为了做到这一点,必须要有源码。如果没有的话,Spring
提供了另一种方式,即XML
配置方式。
四、在XML中声明切面
这里Spring
的AOP
配置元素能够以非侵入的方式声明切面:
AOP 配置元素 |
用途 |
---|---|
<aop:advise> |
定义AOP 通知器 |
<aop:after> |
定义AOP 后置通知(不管被通知方法是否执行成功) |
<aop:after-returning> |
定义AOP 返回通知 |
<aop:after-throwing> |
定义AOP 异常通知 |
<aop:around> |
定义AOP 环绕通知 |
<aop:aspect> |
定义一个切面 |
<aop:aspectj-autoproxy> |
启动@AspectJ 注解驱动的切面 |
<aop:before> |
定义一个AOP 前置通知 |
<aop:config> |
顶层的AOP 配置元素。大多数的<aop:*> 元素必须包含在<aop:config> 元素内 |
<aop:declare-parents> |
以透明的方式为被通知的对象引入额外的接口 |
<aop:printcut> |
定义一个切点 |
4.1 声明前置和后置通知
通过XML
将无注解的Audience
声明为切面:
<aop:config>
<aop:aspect ref="audience"><!--引用audience bean-->
<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="applause" />
<aop:after-throwing pointcut="execution(* concert.Performance.perform(..))" method="demandRefund" />
</aop:aspect>
</aop:config>
说明:和之前一样,这里也可以定义一个切点,然后对上述配置进行简化:
<aop:config>
<aop:aspect ref="audience"><!--引用audience bean-->
<aop:pointcut id="performance" expression="execution(* concert.Performance.perform(..))" />
<aop:before pointcut="performance" method="silenceCellPhones" />
<aop:before pointcut="performance" method="takeSeats" />
<aop:after-returning pointcut="performance" method="applause" />
<aop:after-throwing pointcut="performance" method="demandRefund" />
</aop:aspect>
</aop:config>
4.2 声明环绕通知
对于前置通知和后置通知,有时候如果想让两共享某些信息是较为困难的,有时候会出现线程安全问题,而如果使用环绕通知则不会出现这类问题了:
<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>
4.3 为通知传递参数
<aop:config>
<aop:aspect ref="trackCounter"><!--将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>
4.4 通过切面引入新的功能
<aop:aspect>
<aop:declare-parents types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
default-impl="concert.DefaultEncoreable" />
</aop:aspect>
说明:这里类型匹配Performance
接口(由type-matching
属性指定)的那些bean
在父类结构中会增加Encoreable
接口(由implement-interface
属性指定)。最后要解决的问题是Encoreable
接口中的方法实现要来自何处。
这里有两种方式标识所引入接口的实现。在本例中使用default-impl
属性用全限定类名来显示指定Encoreable
的实现。还有另一种方式,就是使用delegate-ref
属性来标识。
<aop:aspect>
<aop:declare-parents types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
delegate-ref="encoreableDelegate" />
</aop:aspect>
说明:delegate-ref
属性引用了一个Spring bean
作为引入的委托,这需要再声明一个ID
为encoreableDelegate
的bean
。两种方式的区别是后者是Spring bean
,本身可以被注入、通知或使用其他的Spring
配置。
五、注入AspectJ切面
虽然Spring AOP
能够满足许多应用的切面需求,但是与AspectJ
相比,却是一个功能较弱的AOP
解决方案。AspectJ
提供了Spring AOP
所不能支持的许多类型的切点。如当我们需要在创建对象时应用通知,构造器切点就非常方便。由于Java
构造器不同于其他的正常方法,这使得Spring
基于代理的AOP
无法把通知应用于对象的创建过程。
在应用AspectJ
切面时几乎不会涉及到Spring
,但是精心设计且有意义的切面很可能依赖其他类来完成它们的工作。如果在执行通知时,切面依赖于一个或多个类,我们可以在切面内部实例化这些协作的对象。但更好的方式是,我们可以借助Spring
的依赖注入把bean
装配进AspectJ
切面中。
下面通过例子说明,这里为之前的演出创建一个新切面,具体来讲,以切面的方式创建一个评论员的角色,它会观看演出并且会在演出之后提供一些批评意见。
package concert;
public aspect CriticAspect {
public CriticAspect(){}
private CriticismEngine criticismEngine;
pointcut performance() : execution(* perform(..));
after() returning : performance(){
System.out.println(criticismEngine.getCriticism());
}
public void setCriticismEngine(CriticismEngine criticismEngine){
this.criticismEngine = criticismEngine;
}
}
说明:
首先
IDE
必须支持AspectJ
,在IDEA
中可以直接新建aspect
文件。在编译此文件的时候需要使用ajc
编译器(即AspectJ
编译器,需要配置编译器)。此处书中使用的是afterReturning()
,但是从文档中没有找到此种写法,于是进行了修改。-
这里,
CriticAspect
的主要职责是在表演结束后为表演发表评论。CriticAspect
与一个CriticismEngine
对象相互协作,在表演结束时,调用该对象getCriticism()
方法来发表一个苛刻的评论。为了避免CriticAspect
和CriticismEngine
之间产生不必要的耦合,我们通过Setter
依赖注入为CriticAspect
设置CriticismEngine
。如图所示。
下面给出CriticismEngineImpl类的实现:
package concert;
public class CriticismEngineImpl implements CriticismEngine {
public CriticismEngineImpl(){}
private String[] criticismPool;
public String getCriticism(){
int i = (int)(Math.random() * criticismPool.length);
return criticismPool[i];
}
//injected
public void setCriticismPool(String[] criticismPool){
this.criticismPool = criticismPool;
}
}
说明:CriticismEngineImpl
实现类CriticismEngine
接口,通过从注入的评论中随机选择一个苛刻的评论。这个类可以使用如下的XML
声明为一个Spring bean
:
<bean id="criticismEngine" class="com.springaction.springidol.CriticismEngineImpl">
<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>
以上给出了一个CriticismEngine
的实现。剩下的就是为CriticAspect
装配CriticismEngineImpl
。首先必须清楚,AspectJ
切面根本不需要Spring
就可以织入到我们的应用中。如果想使用Spring
的依赖注入为AspectJ
切面注入协作者,那就需要在Spring
配置中把切面声明为一个Spring
配置中的bean
。如下:
<bean class="com.springinaction.springidol.CriticAspect" factory-method="aspectOf">
<property name="criticismEngine" ref="criticismEngine" />
</bean>
说明:
这里和之前我们配置的
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
来调用aspectOf()
方法,而不是调用CriticAspect
的构造方法。