4.Spring学习笔记之面向切面的Spring

在软件开发中,散布于应用中多处的功能被称为横切关注点。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。
依赖注入(DI)有助于应用对象之间的解耦,而AOP可以实现横切关注点与它们所影响的对象之间的解耦。

1.什么是面向切面编程

在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。
横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。
描述切面的常用术语有通知(advice)、切点(pointcut)、连接点(join point)
在AOP术语中,切面的工作被称为通知。通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。
Spring切面可以应用5种类型的通知:

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

连接点
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

切点(Poincut)
如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。切点的定义会匹配所要织入的一个或多个连接点。我们通常使用明明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。

切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么、在何时何处完成其功能。

引入(Introduction)
引入允许我们向现有的类添加新方法或属性。

织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点呗织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:

  • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是一这种方法织入切面的。
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标了IDE字节码。AspectJ 5的加载时织入就支持以这种方式织入切面。
  • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象。Spring AOP就是以这种方式织入切面。

Spring对AOP的支持
创建切点来定义切面所织入的连接点是AOP框架的基本功能。
Spring提供了4中类型的AOP支持:

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

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

Spring所创建的通知都是用标准的Java类编写的。

通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。Spring的切面包裹了目标对象的代理类实现。代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法。直到应用需要被代理的bean时,Spring才创建代理对象。因为Spring运行时才创建代理对象,因此我们不需要特殊的编译器来织入Spring AOP的切面。

Spring只支持方法级别的连接点。

2.通过切点来选择连接点

在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。关于Spring AOP的AspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器的一个子集。因为Spring是基于代理的,而某些切点表达式与基于代理的AOP无关。
下图为Spring AOP所支持的AspectJ切点指示器:
4.Spring学习笔记之面向切面的Spring_第1张图片
execution指示器执行匹配,其他指示器来限制匹配的切点。
除此之外,Spring还引入一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标示bean。
例如,我们要使用AspectJ切点表达式来选择一个名为Performance类中的perform()方法:

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

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

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

3.使用注解创建切面

如果我们把Performance类中perform()方法看成是一个表演的话,那么下面这个切面定义的是在表演之前、表演之后、以及表演失败之后观众的反映(即程序的输出)。

@Aspect //表明Audience是一个切面
public class Audience {

    //表演之前
    @Before("execution(** concert.Performance.perform(..))")
    public void silenceCellPhone() {
        System.out.println("将手机调至静音状态");
    }

    //表演之前
    @Before("execution(** concert.Performance.perform(..))")
    public void takeSeats() {
        System.out.println("就坐");
    }

    //表演之后
    @AfterReturning("execution(** concert.Performance.perform(..))")
    public void applause() {
        System.out.println("鼓掌喝彩");
    }

    //表演之后
    @AfterThrowing("execution(** concert.Performance.perform(..))")
    public void demandrefund() {
        System.out.println("要求退款");
    }
}

此外,@Pointcut注解能够在一个@Aspect切面内定义可重用的切点

@Aspect //表明Audience是一个切面
public class Audience {

    //定义命名的切点
    @Pointcut("execution(** concert.Performance.perform(..))")
    public void performance() {}

    //表演之前
    @Before("performance()")
    public void silenceCellPhone() {
        System.out.println("将手机调至静音状态");
    }

    //表演之前
    @Before("performance()")
    public void takeSeats() {
        System.out.println("就坐");
    }

    //表演之后
    @AfterReturning("performance()")
    public void applause() {
        System.out.println("鼓掌喝彩");
    }

    //表演之后
    @AfterThrowing("performance()")
    public void demandrefund() {
        System.out.println("要求退款");
    }
}

在这里,Audience仍然是一个Java类,只不过它通过注解表明会作为切面使用而已。同其他类一样,可以装配为Spring中bean。

除此之外,你还需要对AspectJ注解进行配置,不然这些代码不会生效。如果使用JavaConfig的话,可以在配置类的类级别上通过使用@EnableAspectJAutoProxy注解启用自动代理:

@Configuration
@EnableAspectJAutoProxy //启用AspectJ自动代理
@Component
public class ConcertConfg{
    //声明Audience bean
    @Bean
    public Audience audience() {
        return new Audience();
    }
}

如果使用xml装配bean的话,那么需要使用Spring aop命名空间的元素:

...
    记得加命名空间哦>
    "concert" />
    
    "concert.Audience" />

需要记住的是,Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是Spring基于代理的切面。这一点非常重要,因为这意味着尽管使用的是@AspectJ注解,但我们仍然限于代理方法的调用。如果想利用AspectJ的所有能力,我们必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面。

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


@Aspect //表明Audience是一个切面
public class Audience {

    //定义命名的切点
    @Pointcut("execution(** concert.Performance.perform(..))")
    public void performance() {}

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint jp) {
        try {
            System.out.println("将手机调至静音状态");
            System.out.println("就坐");
            jp.proceed();//调用被通知的方法
            System.out.println("鼓掌喝彩");
        }catch (Throwable e) {
            System.out.println("要求退款");
        }
    }

}

ProceedingJoinPoint 对象参数是必须的。通知中通过他来调用被通知的方法。

处理通知中的参数

@Aspect //表明Audience是一个切面
public class Audience {

    //定义命名的切点
    @Pointcut("execution(** concert.Performance.perform(String))"
            +"&& args(songName)")
    public void performance(String songName) {}

    @Before("performance(songName)")
    public void watchPerformance(String songName) {

            System.out.println("演唱的歌曲是"+songName);
    }

}

这样就可以将方法中的参数传达到通知中。

通过注解引入新功能

当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。也就是说,一个bean的实现被拆分到多个类中。
为了实现该功能,我们需要创建一个新的切面:

@Aspect 
public class EncoreableIntroducer {

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

通过@DeclareParents注解,将Encoreable 接口引入到Performance bean中。
@DeclareParents注解由三部分组成:

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

4.在XML中声明切面

在Spring的aop命名空间中,提供了多个元素用来在XML中声明切面
4.Spring学习笔记之面向切面的Spring_第2张图片
4.Spring学习笔记之面向切面的Spring_第3张图片

去掉Audience所有的Aspect注解

public class Audience {

    public void silenceCellPhone() {
        System.out.println("将手机调至静音状态");
    }

    public void takeSeats() {
        System.out.println("就坐");
    }

    public void applause() {
        System.out.println("鼓掌喝彩");
    }

    public void demandrefund() {
        System.out.println("要求退款");
    }
}

声明前置通知和后置通知

<aop:config>
    
    <aop:aspect ref="audience">
        <aop:before 
            pointcut="execution(** concert.Performance.perform(..))"
            method="silenceCellPhone" />

        <aop:before 
            pointcut="execution(** concert.Performance.perform(..))"
            method="takeSeats" />

        <aop:after-returning
            pointcut="execution(** concert.Performance.perform(..))"
            method="applause" />

        <aop:after-throwing
            pointcut="execution(** concert.Performance.perform(..))"
            method="demandrefund" />
    aop:aspect>
aop:config>

使用定义切点

<aop:config>
    
    <aop:aspect ref="audience">
        
        <aop:pointcut
        id="performance"
        expression="execution(** concert.Performance.perform(..))" />

        <aop:before 
            pointcut-ref="performance"
            method="silenceCellPhone" />

        <aop:before 
            pointcut-ref="performance"
            method="takeSeats" />

        <aop:after-returning
            pointcut-ref="performance"
            method="applause" />

        <aop:after-throwing
            pointcut-ref="performance"
            method="demandrefund" />
    aop:aspect>
aop:config>

声明环绕通知

public class Audience {
    public void watchPerformance(ProceedingJoinPoint jp) {
        try {
            System.out.println("将手机调至静音状态");
            System.out.println("就坐");
            jp.proceed();//调用被通知的方法
            System.out.println("鼓掌喝彩");
        }catch (Throwable e) {
            System.out.println("要求退款");
        }
    }
}
<aop:config>
    
    <aop:aspect ref="audience">
        
        <aop:pointcut
        id="performance"
        expression="execution(** concert.Performance.perform(..))" />
        
        <aop:around 
            pointcut-ref="performance"
            method="watchPerformance" />

    aop:aspect>
aop:config>

为通知传递参数

public class Audience {
    //要声明为前置通知的方法
    public void watchPerformance(String songName) {
            System.out.println("演唱的歌曲是"+songName);
    }
}
<aop:config>
    
    <aop:aspect ref="audience">
        
        <aop:pointcut
        id="performance"
        expression="execution(** concert.Performance.perform(String))
        and args(songName)" />

        <aop:before 
            pointcut-ref="performance"
            method="watchPerformance" />

    aop:aspect>
aop:config>

通过切面引入新的功能

<aop:aspect>
<aop:declare-parents
    types-matching="concert.Performance+"
    implement-interface="concert.Encoreable"
    default-impl="concert.DefaultEncoteable"
/>
</aop:aspect>

你可能感兴趣的:(Spring学习笔记)