Spring提供了4种类型的AOP支持:
- 基于代理的经典Spring AOP
- 纯POJO切面
- @AspectJ注解驱动的切面
- 注入式AspectJ切面(适用于Spring各版本)
Spring仅支持AspectJ切点指示器(pointcut designator)的一个子集:
AspectJ指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型(当时用Spring AOP时,方法定义在由指定的注解所标注的类里) |
@annotation | 限定匹配带有指定注解的连接点 |
在Spring中使用AspectJ其他指示器时,将会抛出IllegalArgumentException异常。
切点表达式(以剧院为例子):
execution(* concert.Performance.perform(..))
*:返回任意类型
concert.Performance
:方法所属的类
.perform(..):方法
仅匹配concert包:
execution(* concert.Performance.perform(..)) && within(concert.*)
可使用关系符(如&&, ||等)连接, "!"标识not操作。
除上表所列的指示器,Spring还引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的id来标识bean:
execution(* concert.Performance.perform(..)) && bean('xiyangyang')
指定对id为'xiyangyang'的bean进行操作;
execution(* concert.Performance.perform(..)) && !bean('xiyangyang')
对所有id不为'xiyangyang'的bean操作。
使用注解创建切面
Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。
创建环绕通知
环绕通知(@Around)是最为强大的通知类型,它能使你编写的逻辑将被通知的目标方法完全包装起来,就像在一个通知方法中同时编写前置通知和后置通知。
例:
@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(Thorwable e){
System.out.println("Demanding a refund");
}
}
}
As we can see,@Around 接受ProceedingJoinPoint作为参数。这个对象是必须要有的,我们需要在通知中通过它调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,他需要调用ProceedingJointPoint的proceed()方法。这个方法必须被调用,否则通知会阻塞对被通知方法的调用。
处理通知中的参数
假如我们想记录某方法执行的次数,有两种方法,一是直接在每次调用时记录使用次数,然而记录使用次数和方法本身是不同的关注点,因此不应该属于方法。二就是使用切面。
例:
@Aspect
public class TrackCounter{
private Map<Integer, Integer> trackCounts =
new HashMap<Integer, Integer>();
@Pointcut(
"execution(* soundsystem.CompactDisc.playTrack(int)) " + //通知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.contaisKey(trackNumber)
? trackCounts.get(trackNumber) : 0;
}
}
其中,
"execution(* soundsystem.CompactDisc.playTrack(int)) " + "&& args(trackNumber)"
*
: 返回任意类型
soundsystem.CompactDisc
:方法所属的类型
.playTrack
:方法
int
:接受int类型的参数
args(trackNumber)
:指定参数
这里需要关注的是args(trackNumber)
限定符。它表明传给playTrack()
方法的int类型参数也会传递到通知中去。参数的名称trackNumber
也与窃电方法签名中的参数相匹配。
现在把TrackCounter和要记录的类定义为bean,并启动AspectJ代理,就可以记录播放次数了。
知道如何使用切面包装方法后,可以看看如何通过编写切面,为被通知的对象引入全新的功能。
通过注解引入新功能
如果使用代理暴露新接口,切面所通知的bean看起来像是实现了新的接口,即便底层实现类并没有实现这些接口也无所谓。
当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。实际上就是一个bean的实现被拆分到了多个类中。
举个栗子,为Performance实现引入Encoreable接口:
public interface Encoreable{
void performEncore();
}
首先我们创建一个新切面:
@Aspect
public class EncoreableIntroducer{
@DeclareParents(value="concert.Performance+",
defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}
与之前的切面不同,EncoreableIntroducer通过的是@DeclareParants注解将Encoreable接口引入到Performance bean中。和其他切面一样,我们需要在Spring中将EncoreableIntroducer声明为一个bean。
Spring的自动代理机制会获取到此声明。注解和自动代理提供了一种很便利的方式来创建切面。简单且涉及极少Spring配置。但面向注解的切面声明有一个明显劣势:必须能够为通知类添加注解。这说明必须有源码。如果不想将AspectJ放到代码中,可在Spring XML中配置。因为工作中不用xml,我这里就不说啦。
注入AspectJ切面
如果在执行通知时,切面依赖于一个或多个类,我们可以在切面内部实例化这些协作对象。更好的方法是借助Spring的依赖注入把bean装配进AspectJ切面中。
总结一下
AOP是OOP的一个强大补充。通过AspectJ,我们可以把分散在应用各处的行为放入可重用的模块中。通过显示地声明在何处如何应用该行为,可以有效的减少代码冗余,让我们的类关注自身的主要功能。