Spring aop学习整理(spring in action):spring AOP

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring aop学习整理(spring in action):spring AOP相关的知识,希望对你有一定的参考价值。

  Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。

  通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。如图4.3所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。

                                                       技术分享 

    图4.3 Spring的切面由包裹了目标对象的代理类实现。代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法

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

AspectJ指示器

描述
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配AOP代理的bean引用为指定类型的类
target 限制连接点匹配目标对象为指定类型的类
@target()

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

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

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

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

  在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgument-Exception异常。

  当我们查看如上所展示的这些Spring支持的指示器时,注意只有execution指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的。这说明execution指示器是我们在编写切点定义时最主要使用的指示器。在此基础上,我们使用其他指示器来限制所匹配的切点。

  1.编写切点:

package concert;

public interface Performance {
    void perform();
}

  Performance可以代表任何类型的现场表演,如舞台剧、电影或音乐会。假设我们想编写Performance的perform()方法触发的通知。图4.4展现了一个切点表达式,这个表达式能够设置当perform()方法执行时触发通知的调用。
                                     技术分享

  我们使用execution()指示器选择Performance的perform()方法。方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。对于方法参数列表,我们使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么。

  现在假设我们需要配置的切点仅匹配concert包。在此场景下,可以使用within()指示器来限制匹配,如图4.5所示。

                                                        技术分享

  请注意我们使用了“&&”操作符把execution()和within()指示器连接在一起形成与(and)关系(切点必须匹配所有的指示器)。类似地,我们可以使用“||”操作符来标识或(or)关系,而使用“!”操作符来标识非(not)操作。

  因为“&”在XML中有特殊含义,所以在Spring的XML配置里面描述切点时,我们可以使用and来代替“&&”。同样,or和not可以分别用来代替“||”和“!”。

  1.1在切点中选择bean

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

  例如,考虑如下的切点:在这里,我们希望在执行Performance的perform()方法时应用通知,但限定bean的ID为woodstock。

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

  在某些场景下,限定切点为指定的bean或许很有意义,但我们还可以使用非操作为除了特定ID以外的其他bean应用通知:在此场景下,切面的通知会被编织到所有ID不为woodstock的bean中

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

  现在,我们已经讲解了编写切点的基础知识,让我们再了解一下如何编写通知和使用这些切点声明切面。
  2.使用注解创建切面:

  如果一场演出没有观众的话,那不能称之为演出。对不对?从演出的角度来看,观众是非常重要的,但是对演出本身的功能来讲,它并不是核心,这是一个单独的关注点。因此,将观众定义为一个切面,并将其应用到演出上就是较为明智的做法。

如果一场演出没有观众的话,那不能称之为演出。对不对?从演出的
角度来看,观众是非常重要的,但是对演出本身的功能来讲,它并不
是核心,这是一个单独的关注点。因此,将观众定义为一个切面,并
将其应用到演出上就是较为明智的做法。
  Audience类:观看演出的切面

package concert;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@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 CLAP!!!");
    }
    
    @AfterThrowing("execution(** concert.Performance.perform.())")
    public void demandRefund() {// 表演之后
        System.out.println("Demanding a refund");
    }
}

  Audience类使用@AspectJ注解进行了标注。该注解表明Audience不仅仅是一个POJO,还是一个切面。Audience类中的方法都使用注解来定义切面的具体行为。
Audience有四个方法,定义了一个观众在观看演出时可能会做的事情。在演出之前,观众要就坐(takeSeats())并将手机调至静音状态(silenceCellPhones())。如果演出很精彩的话,观众应该会鼓掌喝彩(applause())。不过,如果演出没有达到观众预期的话,观众会要求退款(demandRefund())。

  可以看到,这些方法都使用了通知注解来表明它们应该在什么时候调用。AspectJ提供了五个注解来定义通知,表所示。

注   解                          通   知
@After 通知方法会在目标方法返回或抛出异常后调用
@AfterReturning 通知方法会在目标方法返回后调用
@AfterThrowing 通知方法会在目标方法抛出异常后调用
@Around 通知方法会将目标方法封装起来
@Before 通知方法会在目标方法调用之前执行

  Audience使用到了前面五个注解中的三个。takeSeats()和silence CellPhones()方法都用到了@Before注解,表明它们应该在演出开始之前调用。applause()方法使用了@AfterReturning注解,它会在演出成功返回后调用。demandRefund()方法上添加了@AfterThrowing注解,这表明它会在抛出异常以后执行。
  你可能已经注意到了,所有的这些注解都给定了一个切点表达式作为它的值,同时,这四个方法的切点表达式都是相同的。其实,它们可以设置成不同的切点表达式,但是在这里,这个切点表达式就能满足所有通知方法的需求。让我们近距离看一下这个设置给通知注解的切点表达式,我们发现它会在Performance的perform()方法执行时触发。
  相同的切点表达式我们重复了四遍,这可真不是什么光彩的事情。这样的重复让人感觉有些不对劲。如果我们只定义这个切点一次,然后每次需要的时候引用它,那么这会是一个很好的方案。                                                                                                                                                                                                                      幸好,我们完全可以这样做:@Pointcut注解能够在一幸好,我们完全可以这样做:@Pointcut注解能够在一个@AspectJ切面内定义可重用的切点。                           新的Audience,现在它使用了@Pointcut。

package concert;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class Audience {
    @Pointcut("execution(** concert.Performance.perform.(..))")
    public void performace(){
        
    }
    
    @Before("performace()")
    public void silenceCellPhones() {// 表演之前
        System.out.println("silencing cell phones");
    }

    @Before("performace()")
    public void takeSeats() {// 表演之前
        System.out.println("Taking seats");
    }

    @AfterReturning("performace()")
    public void applause() {// 表演之后
        System.out.println("CLAP CLAP CLAP!!!");
    }
    
    @AfterThrowing("performace()")
    public void demandRefund() {// 表演之后
        System.out.println("Demanding a refund");
    }
}

  在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();
}

  如果你使用JavaConfig的话,可以在配置类的类级别上通过使用@EnableAspectJAutoProxy注解启用自动代理功能。                                                                            如果你使用XML的话,那么需要使用Springaop命名空间中的<aop:aspectj-autoproxy>元素。

  不管你是使用JavaConfig还是XML,AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。在这种情况下,将会为Concertbean创建一个代理,Audience类中的通知方法将会在perform()调用前后执行。

  我们需要记住的是,Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是Spring基于代理的切面。这一点非常重要,因为这意味着尽管使用的是@AspectJ注解,但我们仍然限于代理方法的调用。如果想利用AspectJ的所有能力,我们必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面。到现在为止,我们的切面在定义时,使用了不同的通知方法来实现前置通知和后置通知。另外的一种通知:环绕通知(around advice)。环绕通知与其他类型的通知有所不同,因此值得花点时间来介绍如何进行编写。

  创建环绕通知:

  环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。为了阐述环绕通知,我们重写Audience切面。这次,我们使用一个环绕通知来代替之前多个不同的前置通知和后置通知。

package concert;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

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

    }

    @Around("performace()")
    public void watchPerformace(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作为参数。这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用ProceedingJoinPoint的proceed()方法。

  需要注意的是,别忘记调用proceed()方法。如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。有可能这就是你想要的效果,但更多的情况是你希望在某个点上执行被通知的方法。

  有意思的是,你可以不调用proceed()方法,从而阻塞对被通知方法的访问,与之类似,你也可以在通知中对它进行多次调用。要这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。

 

------以上内容均摘录至《Spring实战》,主要用于后期翻阅。感谢查看,共勉

concert










以上是关于Spring aop学习整理(spring in action):spring AOP的主要内容,如果未能解决你的问题,请参考以下文章

Spring学习记录

Spring学习整理

Spring In Action 4 学习笔记Spring概览

Spring—aop使用整理

spring AOP 整理

Spring AOP整理