Spring使用注解式声明切面与使用

1 什么是面向切面

这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。正则表达式

AOP是Spring提供的关键特性之一。AOP即面向切面编程,是OOP编程的有效补充。使用AOP技术,能够将一些系统性相关的编程工做,独立提取出来,独立实现,而后经过切面切入进系统。从而避免了在业务逻辑的代码中混入不少的系统相关的逻辑——好比权限管理,事物管理,日志记录等等。这些系统性的编程工做均可以独立编码实现,而后经过AOP技术切入进系统便可。从而达到了 将不一样的关注点分离出来的效果。spring

2 AOP术语

2.1 通知(Advice)

切面必需要完成的工做即称为通知。通知定义了切面是什么以及何时实用。编程

spring切面能够实用的5种类型通知:bash

  • 前置通知(Before):在目标方法被调用以前调用通知功能;
  • 后置通知(After):在目标方法完成以后调用通知,此时不会关心方法的输出是什么;
  • 返回通知(After-returning):在目标方法成功执行以后调用通知;
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知;
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用以前和调用以后执行自定义的行为。

2.2 链接点(Join point)

咱们的应用可能有数以千计的时机应用通知。这些时机被称 为链接点。链接点是在应用执行过程当中可以插入切面的一个点。这个点能够是调用方法时、抛出异常时、甚至修改一个字段时。切面代码能够利用这些点插入到应用的正常流程之中,并添加新的行为。测试

2.3 切点(Poincut)

切点定义了从何处切入。切点的定义会匹配通知所要织入的一个或多个链接点。一般使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。ui

2.4 切面(Aspect)

切面是通知和切点的结合。通知和切点共同定义了切面的所有内容----它是什么,在什么时候和何处完成其功能。编码

2.5 引入(Introduction)

引入容许咱们向现有的类添加新方法或属性。spa

2.6 织入(Weaving)

织入是把切面应用到目标对象并建立新的代理对象的过程。切面在指定的链接点被织入到目标对象中。代理

  • 编译期:切面在目标类编译时被织入。这种方式须要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式须要特殊的类加载器(ClassLoader),它能够在目标类被引入应用以前加强该目标类的字节码。AspectJ 5的加载时织入(load-timeweaving,LTW)就支持以这种方式织入切面。
  • 运行期:切面在应用运行的某个时刻被织入。通常状况下,在织入切面时,AOP容器会为目标对象动态地建立一个代理对象。Spring AOP就是以这种方式织入切面的。

3 Spring对切面的支持

Spring提供了4种类型的AOP支持:日志

  • 基于代理的经典Spring AOP;
  • 纯POJO切面;
  • @AspectJ注解驱动的切面;
  • 注入式AspectJ切面(适用于Spring各版本)。

前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,所以,Spring对AOP的支持局限于方法拦截。

4 认识切点

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

首先定义一个接口来做为切点:

public interface Performance {
    void perform();
}
复制代码

假设咱们想编写Performance的perform()方法触发的通 知。下面的表达式可以设置当perform()方法执行时触发通知的调用。

execution(* com.wtj.springlearn.aop.Performance.perform(..))
复制代码

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

若是咱们须要设置切点匹配com.wtj.springlearn.aop包,可使用within()来限定匹配。

execution(* com.wtj.springlearn.aop.Performance.perform(..)) && within(com.wtj.springlearn.aop.*)
复制代码

表示com.wtj.springlearn.aop包下任意类的方法被调用时。

使用“&&”操做符把execution()和within()指示器链接在一块儿造成与(and)关系(切点必须匹配全部的指示器)。相似地,咱们可使用“||”操做符来标识或(or)关系,而使用“!”操做符来标识非(not)操做。

由于“&”在XML中有特殊含义,因此在Spring的XML配置里面描述切点时,咱们可使用and来代替“&&”。一样,or和not能够分别用来代替“||”和“!”。

还可使用bean的ID来标识bean。bean()使用bean ID或bean名称做为参数来限制切点只匹配特定的bean。

execution(* com.wtj.springlearn.aop.Performance.perform(..)) && bean('book')
复制代码

这里表示执行perform方法时通知,可是只限于bean的ID为book。

5 经过注解建立切面

本篇主要介绍注解方式的切面定义方式

经过@Aspect进行标注,表示该Audience不只是一个POJO仍是一个切面。类中的方法表示了切面的具体行为。

Spring提供了五种注解来定义通知时间:

首先建立一个切面:

@Aspect
public class Audience {

    //表演前 手机静音
    @Before("execution(* com.wtj.springlearn.aop.Performance.perform(..))")
    public void silenceCellPhone(){
        System.out.println("silence Cell Phone");
    }

    //表演成功-clap
    @AfterReturning("execution(** com.wtj.springlearn.aop.Performance.perform(..))")
    public void clap(){
    System.out.println("clap clap clap");
    }

    //表演失败-退款
    @AfterThrowing("execution(** com.wtj.springlearn.aop.Performance.perform(..))")
    public void refund(){
        System.out.println("refund refund refund");
    }

}
复制代码

Performance的实现类:

@Component
public class PerformanceImpl implements Performance {
    public void perform() {
    System.out.println("the perform is good");
    }
}
复制代码

最后还须要开启自动代理功能,经过JavaConfig进行配置,使用@EnableAspectJAutoProxy标签开启。

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class AudienceConfig {
    @Bean
    public Audience audience(){
        return new Audience();
    }
}
复制代码

最后经过一个简单的测试用例就能够来验证了。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AudienceConfig.class)
public class PerformanceTest {

    @Autowired
    private Performance performance;

    @Test
    public void perTest(){
        performance.perform();
    }
}
复制代码

打印结果:

silence Cell Phone
the perform is good
clap clap clap
复制代码

5.1 @PointCut声明切点

你会发现上面切面的方法中,切点的声明都是同样的,这种状况下可使用@Pointcut注解来定义切点。

@Pointcut("execution(* com.wtj.springlearn.aop.Performance.perform(..))")
    public void per(){};

    //表演前 手机静音
    @Before("per()")
    public void silenceCellPhone(){
        System.out.println("silence Cell Phone");
    }
复制代码

per()方法自己并不重要,该方法只是一个标识,供@PointCut注解依附。

5.2 环绕通知

环绕通知是最为强大的通知类型。它可以让你所编写的逻辑将被通知的目标方法彻底包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。

重写Audience切面,使用环绕通知替代以前多个不一样的前置通知和后置通知。

@Around("per()")
    public void watch(ProceedingJoinPoint point) throws Throwable {
        try{
            System.out.println("silence Cell Phone");
            point.proceed();
            System.out.println("clap clap clap");
        }catch (Exception e){
            System.out.println("refund refund refund");
        }
    }
复制代码

首先注意到的多是它接受ProceedingJoinPoint做为参数。这个对象是必需要有的,由于你要在通知中经过它来调用被通知的方法。通知方法中能够作任何的事情,当要将控制权交给被通知的方法时,它须要调用ProceedingJoinPoint的proceed()方法。

若是不调proceed()这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。一样的,你也能够调用屡次。

5.3 向通知中传入参数

上面咱们建立的切面都很简单,没有任何参数。那么切面能访问和使用传递给被通知方法的参数么?

Performance中新增方法:

void perform(String name);
复制代码

实现类:

public void perform(String name) {
        System.out.println("下面请 "+name+" 开始他的表演");
    }
复制代码

修改Audience中的切点和切面

@Pointcut("execution(* com.wtj.springlearn.aop.Performance.perform(String)) && args(name)")
    public void per(String name){};

    @Around("per(name)")
    public void toWatch(ProceedingJoinPoint point,String name) throws Throwable {
        try{
            point.proceed();
            System.out.println(name +" 上场啦");
            System.out.println(name +" 演出结束");
        }catch (Exception e){
            System.out.println("refund refund refund");
        }
    }
复制代码

表达式args(name)限定符,它表示传递给perform(String name)方法的String类型参数也会传到通知中去,参数名与切点中的参数名相同。perform(String)指明了传入参数的类型。

而后在@Around注解中指明切点与参数名,这样就完成了参数转移。

最后修改一下测试用例就完成了

@Test
    public void perTest(){
        performance.perform("渣渣辉");
    }
复制代码

打印输出:

下面请 渣渣辉 开始他的表演
渣渣辉 上场啦
渣渣辉 演出结束
复制代码

5.4 经过注解@DeclareParents引入新方法

若是咱们想在一个类上新增方法,一般状况下咱们会怎么作呢?最简单的办法就是在此目标类上增长此方法,可是若是原目标类很是复杂,动一发而牵全身。而且有些时候咱们是没有目标类的源码的,哪这个时候怎么办呢?

咱们能够为须要添加的方法创建一个类,而后建一个代理类,同时代理该类和目标类。用一个图来表示

当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其余对象。

仍是上面的例子,假设咱们须要让表演者跳起来。

新建Jump接口以及实现类:

public interface Jump {
    void duJump();
}
复制代码
public class JumpImpl implements Jump {
    public void duJump() {
        System.out.println("do Jump");
    }
}
复制代码

而后咱们代理两个类:

@Aspect
public class JumpIntroducer {
    @DeclareParents(value = "com.wtj.springlearn.aop.Performance+",defaultImpl = JumpImpl.class)
    public static Jump jump;
}
复制代码

@DeclareParents注解由三部分组成:

  • value属性指定了哪一种类型的bean要引入该接口。在本例中,也就是全部实现Performance的类型。(标记符后面的加号表示是Performance的全部子类型,而不是Performance本 身。)
  • defaultImpl属性指定了为引入功能提供实现的类。
  • @DeclareParents注解所标注的静态属性指明了要引入了接 口。

经过配置将JumpIntroducer声明

@ComponentScan
@Configuration
@EnableAspectJAutoProxy
public class JumpConfig {
    @Bean
    public JumpIntroducer jumpIntroducer(){
        return new JumpIntroducer();
    }
}
复制代码

或者你也能够在JumpIntroducer类上加入@Component注解,就能够不用声明bean了。

最后经过测试用例进行测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JumpConfig.class)
public class PerformanceTest {

    @Autowired
    private Performance performance;

    @Test
    public void perTest(){
        //类型转换
        Jump jump = (Jump) performance;
        jump.duJump();
    }
}
复制代码

打印结果:

do Jump
复制代码
相关文章
相关标签/搜索