Spring实战(十)Spring AOP应用——为方法引入新功能、为对象引入新方法

  切面最基本的元素是通知和切点,切点用于准确定位应该在什么地方应用切面的通知。

  1、Spring借助AspectJ的切点表达式语言来定义Spring切面

  在Spring中,要使用AspectJ的切点表达式语言来定义切点。

  重要的一点是,Spring仅支持AspectJ切点指示器的一个子集,当尝试使用AspectJ其他指示器时,会抛出异常

 

  arg()        限制连接点匹配参数为指定类型的执行方法

  @args()     限制连接点匹配参数为指定注解标注的执行方法

 execution()      用于匹配是连接点的执行方法

  this()       限制连接点匹配AOP代理的bean引用为指定类型的类

  target      限制连接点匹配目标对象为指定类型的类

  @target()     限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解

  within()      限制连接点匹配指定的类型

  @within     限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义

            在由指定的注解所注解的类中)

  @annotation   限定匹配带有指定注解的连接点

 

  2、编写切点——切点表达式

package concert
public interface Performance{
  public void perform();  
}

  下面编写Performance的perform()方法的触发的通知,这个切点表达式能够设置当perform()方法执行时触发通知的调用:

execution( *  concert.Performance.perform(..) )

 

  其中方法表达式从“ * ”号开始,表示我们不关心方法返回值的类型;

  然后,指定全限定类名和方法名;

  方法参数列表中,用两个点号(..)表明切点要选择任意的perform()方法,而不关心该方法的入参是什么。

  假设,我们要配置的切点仅匹配concert包,可以利用within()指示器:

 execution( *  concert.Performance.perform(..) ) && within(concert.*)

  这里我们使用了“&&”操作,在其他切点表达式中我们也可以使用“||”、“!”操作。(or、not)

 

  3、在切点中选择bean

  Spring引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID or bean名称作为参数来限制切点,让它只匹配特定的bean。

 execution( *  concert.Performance.perform(..) ) and bean('woodstock')

  这时,切面的通知会被织入到ID为woodstock的bean中。

  还可以如下使用,切面的通知会被织入到所有ID不为woodstock的bean中:

 execution( *  concert.Performance.perform(..) ) and !bean('woodstock')

 

 

  4、使用AspectJ注解创建切面

  使用注解创建切面是AspectJ 5引入的关键特性,之前需要学习一种Java语言的扩展。

  首先确定,上面定义的Performance接口,是切面中切点的目标对象,接下来我们定义一个Audience类,我们把观众作为演出的切面。

@Aspect
public class Audience {

    @Before("execution(* concert.Performance.perform(..))")
    public void silenceCellPhones(){
        System.out.println("Silencing cell phones.");
    }

    @Before("execution(* concert.Performance.perform(..))")
    public void takeSeats(){
        System.out.println("Taking seats.");
    }

    @AfterReturning("execution(* concert.Performance.perform(..))")
    public void applause(){
        System.out.println("CLAP!!~~CLAP!!~~");
    }

    @AfterThrowing("execution(* concert.Performance.perform(..))")
    public void demandRefund(){
        System.out.println("Demanding a refund.");
    }
}

 

  这四个方法定义了一个观众在观看演出时可能会做的事情:

  演出之前,观众就坐—将手机调至静音状态;

  演出精彩—观众鼓掌;

  演出很烂—观众要求退款。

  (通知方法中的注解都给定了一个切点表达式作为它的值)

 

  5、AspectJ提供的注解(声明通知方法)

  @After          通知方法在目标方法返回or抛出异常后执行;

  @AfterReturning       通知方法在目标方法返回后调用;

  @AfterThrowing      通知方法在目标方法抛出异常后调用;

  @Around         通知方法将目标方法封装起来;

  @Before          通知方法在目标方法调用之前执行;

 

  6、使用@Pointcut注解在一个@AspectJ切面内定义可以重用的切点。

  在上一个切面Performance类中,注解中的切点表达式我们重复使用了四次,下面使用@Pointcut进行简化:

@Aspect
public class Audience {
    @Pointcut("execution(* concert.Performance.perform(..))")
    public void performance() {}

    @Before("performance()")
    public void silenceCellPhones(){
        System.out.println("Silencing cell phones.");
    }

    @Before("performance()")
    public void takeSeats(){
        System.out.println("Taking seats.");
    }

    @AfterReturning("performance()")
    public void applause(){
        System.out.println("CLAP!!~~CLAP!!~~");
    }

    @AfterThrowing("performance()")
    public void demandRefund(){
        System.out.println("Demanding a refund.");
    }
}

  这个Audience类中的performance()方法的实际内容并不重要,在这里它实际上应该是空的,因为该方法本身只是一个标识,供@Pointcut注解依附。

  

  到此为止,我们创建了切面吗?并没有,目前,Audience只会是Spring容器中的一个bean。(@AspectJ会自动创建为bean?)

  这里即使我们使用了AspeJ注解,但它不会被是为切面,这些注解不会解析,也不会创建将其转换为切面的代理。

 

  7、使用JavaConfig,XML开启自动代理

  JavaConfig:

  可以在配置类(使用JavaConfig有一个配置类(@Configuration)来开启各项功能)上使用@EnableAspeJAutoProxy启用自动代理功能;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig{
 @Bean
 public Audience audience{
   return new Audience();  
 }            
}

 

  XML:

  使用Spring的aop命名空间中的<aop:aspectj-autoproxy>元素,开启自动代理。(先声明Spring的AOP命名空间)

  不管是哪种方式,AspeJ自动代理都会为使用@Aspect所注解的bean创建一个代理,这个代理会围绕所有该切面所匹配的bean。

 

  8、创建环绕通知@Around

  环绕通知是最为强大的通知类型,可以使自己编写的逻辑将被通知的目标方法完全包装起来。

  下面用一个环绕通知来代替之前的多个前置和后置通知:

@Aspect
public class AudienceAround {
    @Pointcut("execution(* concert.Performance.perform(..))")
    public void performance() {
    }

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint jp) {
        try {
            System.out.println("Silencing cell phone");
            System.out.println("Taking seats");
            jp.proceed();
            System.out.println("CLAP~~CLAP~");
        }catch(Throwable e){
            System.out.println("Demanding a refund!");
        }
    }
}

  这个位于一个方法中的通知所达到的效果,与之前的几个前置和后置通知是一样的。

  注意到,这个watchPerformance方法接受了一个ProceedingJoinPoint类型的参数,jp对象必须要有,因为通过它的proceed()方法来调用被通知(增强)的方法。也就是说,这个通知阻塞了被通知的方法,我们必须手动调用。

 

  9、若被通知的方法含有参数,切面能访问和使用被传递给被通知的方法的参数吗?

   也就是说,我们的增强逻辑中,需要利用被增强的方法中的参数完成一些功能。例如:

@Aspect
public class TrackCounter{
    @Pointcut("execution(* soundsystem.CompactDisc.playTrack(int))"&&"args(trackNumber)")
    public void trackPlayed(int trackNumber) {}

    @Before("trackPlayed(trackNumber)")
    public void countTrack(int trackNumber){...}  

    ...    
}

   这个切面中,使用@Pointcut注解定义命名的切点中声明了要提供给通知方法的参数。切点表达式中"args(trackNumber)"限定符表明了传递给playTrack()方法的int型参数也会传递到通知中去,参数的名称trackNumber也与切点方法签名中的参数相匹配。

  下面的前置通知方法的注解@Before中与countTrack方法中的入参均有trackNumber,这样就完成了从命名切点到通知方法的参数转移。

  形象点来说,我们靠AspectJ注解作为载体,把被通知方法中的参数转义到通知中。

  目前为止,我们所使用的切面中,所包装的都是被通知对象的已有方法,这仅仅是切面能实现的功能之一。

 

  10、如何通过编写切面,为被通知的对象(不是方法)引入全新的功能?

  Java不是动态语言,一旦类编译完成了,我们就很难再为该类添加新功能。

  我们之前做的都是为对象拥有的方法添加新功能,那我们为什么不能为对象增加新的方法呢?

  使用Spring AOP,我们可以为bean引入新的方法,代理拦截调用并委托给实现该方法的其他对象。

    11、通过@DeclareParens注解实现为对象引入新方法

  现在我们来为之前的Performance接口的所有实现引入新接口Encoreable(观众要求返场表演):

package concert;

public interface Encoreable {
    void performEncore();
}

 

  借助于AOP的引入功能,我们可以不必在设计上妥协或者侵入性地改变现有的实现。

  为了实现该功能,创建一个新的切面:

@Aspect
public class EncoreableIntroducer {
    @DeclareParents(value="concert.Performance+",defaultImpl = DefaultEncoreable.class)
    public static Encoreable encoreable;
}

  这个切面并没有提供前置、后置或环绕通知,而是通过@DeclareParens注解,将Encoreable接口引入到Performance bean中:

  • value属性指定了那种类型的bean要引入该接口。这里是指定为所有实现Performance的类型,“+”表示是Performance的所有子类型,而不是本身。
  • defaultImpl属性指定了为引入功能提供实现的类。在这里,指定DefaultEncoreable提供实现。
  • @DeclareParens注解所标注的静态属性表明了要引入的接口,这里我们引入Encoreable接口。

  和其他切面一样,我们需要在Spring应用中将EncoreableIntroducer声明为一个bean。然后Spring的自动代理机制会获取到它的声明,当Spring发现一个bean使用@AspectJ注解时,Spring就会创建一个代理,单后将调用委托给被代理的bean或被引入的实现,这取决于调用的方法属于被代理的bean还是属于被引入的接口。

 

  Spring的注解和自动代理提供了一种便利的方式创建切面,但是面向注解的切面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到这一点,必须要有源码!

  若没有源码or不想将AspectJ注解放入你的代码中,可以使用Spring XML配置文件声明切面。

 

 

posted @ 2018-01-29 03:16  爆炸的果核  阅读(1343)  评论(1编辑  收藏  举报