AOP笔记

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 trackCounts = 
      new HashMap();

@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,我们可以把分散在应用各处的行为放入可重用的模块中。通过显示地声明在何处如何应用该行为,可以有效的减少代码冗余,让我们的类关注自身的主要功能。

你可能感兴趣的:(AOP笔记)