Spring实战第四章学习笔记————面向切面的Spring

Spring实战第四章学习笔记————面向切面的Spring

什么是面向切面的编程

我们把影响应用多处的功能描述为横切关注点。比如安全就是一个横切关注点,应用中许多方法都会涉及安全规则。而切面可以帮我们模块化横切关注点。而当我们要重用通用功能时,最常见的面向编程技术是继承或委托。但当整个应用都用相同的基类继承会导致整个对象体系脆弱,而委托会使调用变复杂。切面则提供了取代继承和委托的另一种方案。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用而无需修改的类。横切关注点被模块化为特殊的类,这些类被称为切面。这样做是的每个关注点都集中于一个地方。其次服务模块更简洁。

定义AOP术语

描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point)。
image

通知

在AOP术语中,切面的工作被称为通知。通知定义了切面是什么以及何时使用。通知除了描述切面要完成的工作还解决了何时执行这个工作的问题。Spring切面可以应用五种类型的通知:

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

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

切点(Poincut)

如果说通知定义了切面的“什么”和“何时”,那么切点定义了“何处”,切点的定义会匹配通知所要织入的一个或多个连接点。

切面(Aspect)

切面是通知和切点的结合。

引入(Introducation)

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

织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。织入一般发生在如下几个时机:

  • 编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才可以做的到,例如AspectJ的织入编译器;
  • 类加载时:使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码;
  • 运行时:切面在运行的某个时刻被织入,SpringAOP就是以这种方式织入切面的,原理应该是使用了JDK的动态代理技术。

Spring对AOP的支持

并不是所有的AOP框架都是相同的,它们在连接点模型上可能有强弱之分。织入切面的方式和时机也有所不同。但是创建切点来定义切面所织入的连接点是AOP框架的基本功能。
Spring提供了四种类型的AOP支持:

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

前三种是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。
  第二种:借助Spring的aop命名空间,我们可以将纯POJO转换为切面。实际上,这些POJO只是提供了满足切点条件时所要调用的方法。这种技术需要XML配置,但这的确是声明式地将对象转换为切面的简便方式。
  第三种Spring借鉴了AspectJ的切面,以提供注解驱动的AOP。本质上,它依然是Spring基于代理的AOP,但是编程模型几乎与编写成熟的AspectJ注解切面完全一致。
  第四种类型能够帮助你将值注入到AspectJ驱动的切面中。

Spring通知是Java编写的,Spring是在运行时通知对象,且只支持方法级别的连接点。

通过切点来选择连接点

在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。Spring是基于代理的,而某些切点表达式是与基于代理的AOP无关的。

AspectJ指示器 描述
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配AOP代理的Bean引用为指定类型的类
target() 限制连接点匹配目标对象为执行类型的类
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解
within() 限制连接点匹配指定的类型
@within() 限制连接点匹配指定注解所标注的类型
@annotation() 限制匹配带有指定注解连接点

编写切点

image

这里使用了execution()指示器来选择Performance的play()方法。表达式以*开头表示不关心返回值的类型,然后指定了全限定类名和方法名,使用..作为方法的参数列表,表示可以是任意的入参。

使用&&将execution()和within()进行连接,那么也就可以使用||(或)和!(非)。

image

在切点中选择bean

除了之前所列的指示器外,Spring还引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标识bean。

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

使用注解创建切面

定义切面

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("Demaning a refund");
    }
}

Audience类使用了@AspectJ注解进行了标注。该注解表明Audience不仅仅是一个POJO,还是一个切面。Audience类中的方法都使用注解来定义切面的具体行为。Audience有四个方法都使用了通知注解来表明它们应该在何时调用。

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

此时注意到这些注解都给定了一个切点表达式作为它的值,它会在Performance的perform()方法执行时触发。
但相同的切点表达式重复了四遍,有没有什么方法可以避免这种重复呢?如果我们只定义这个切点一次然后在需要的时候再引用它,这时就可以使用@Pointcut注解来定义一个切面内可重用的切点。

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 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 CLAP!!!");
    }
    
    @AfterThrowing("performance()")
    public void demandRefund(){
        System.out.println("Demaning a refund");
    }
}

performance()方法的实际内容并不重要,该方法只是一个标识,供@Ponitcut注解依附。
需要注意的是除了注解和performance()方法,Audience类仍是一个POJO。只不过它通过注解表明会作为切面使用。但此时Audience只是Spring容器一个bean使用的这些注解不会解析,也不会创建将其转换为切面的代理。因此可使用JavaConfig,在配置类的类级别上通过使用EnableAspectJ-AutoProxy注解启用自动代理功能:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy   //启用AspecJ代理
@ComponentScan
public class ConcertConfig {
    @Bean
    public Audience audience(){    //声明Audience bean
        return new Audience();
    }
}

当然也可以用XML来装配bean使用

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:context="http://www.springframework.org/schema/context"
 xmlns:aop="http://www.springframework.org/schema/aop"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
     http://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/aop
     http://www.springframework.org/schema/aop/spring-aop.xsd
     http://www.springframework.org/schema/context 
     http://www.springframework.org/schema/context/spring-context.xsd">
     
     <context:component-scan base-package="com.wang.four" />
     <aop:aspectj-autoproxy />
     <bean  class="com.wang.four.Audience"/>
     </beans>

Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是Spring基于代理的切面。这一点非常重要,因为这意味着尽管使用的是@AspectJ注解,但我们仍然限于代理方法的调用。

创建环绕通知

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

使用环绕通知重新实现Audience切面:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class Audience {
    
    @Pointcut("execution(* concert.Performance.perform(..))")
    public void performance(){}
    
    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint){
        try{
            System.out.println("Silencing cell phones");
            System.out.println("Taking seats");
            joinPoint.proceed();
            System.out.println("CLAP CLAP CLAP!!!");
        }catch(Throwable e){
            System.out.println("Demaning a refund");
        }
    }
}

关于这个新的通知方法,值得注意的是它接受ProceedingJoinPoint作为参数。这个对象的作用是通过它调用被通知的方法。当然你也可以不调用proceed()方法从而阻塞对被通知方法的访问。

处理通知的参数

当切面需要参数时,切点声明了提供给通知方法的参数。
image
切点表达式中的args(trackNumber)限定符。它表明传递给playTrack()方法的int类型参数也会传递到通知中去。参数的名称trackNumber也与切点方法签名中的参数相匹配。
这个参数会传递到通知方法中,这个通知方法是通过@Before注解和命名切点trackPlayed(trackNumber)定义的。切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。

@Aspect
public class TrackCounter {
    
    private Map<Integer,Integer> trackCounts = new HashMap<Integer,Integer>();
    
    @Pointcut("execution(* com.wang.second.CompactDisc.playTrack(int))" +
    "&& args(trackNumber)")
    public void trackPlayer(int trackNumber){}
    
    @Before("trackPlayer()")
    public void countTrack(int trackNumber){
        int currentCount = getPlayCount(trackNumber);
        trackCounts.put(trackNumber, currentCount);
    }
    
    public int getPlayCount(int trackNumber){
        return trackCounts.containsKey(trackNumber)?trackCounts.get(trackNumber):0;
    }
}

通过注解引入新功能

之前所介绍的切面都是包装被通知对象的已有方法。而切面还可以为被通知的对象引入全新的功能。
如果除了实现这些接口,代理也能暴露新接口的话,会怎么样呢?那样的话,切面所通知的bean看起来像是实现了新的接口,即便底层实现类并没有实现这些接口也无所谓。

image

需要注意的是当引入接口的方法被调用时,代理会把此调用委托给实现了接口的某个对象。实际上一个bean的实现。首先我们先引入一个Encoreable接口。

public interface Encoreable {
    void performEncore();
}

我们需要一种方式将这个接口应用到Performance实现中。借助于AOP的引入功能,我们创建一个新的切面:


@Aspect
public class EncoreableIntroducer {
    
    @DeclareParents(value="com.wbw.Performance+",defaultImpl=DefaultEmcoreable.class)
    public static Encoreable encoreable;

}

在这个切面中,我们使用了@DeclareParents注解,将Encoreable接口引入到Performance中。
@DeclareParents注解由三部分组成:

  • value属性指定了哪种类型的bean要引入该接口。在本例中,也就是所有实现Performance的类型。(标记符后面的加号表示是Performance的所有子类型,而不是Performance本身。)
  • defaultImpl属性指定了为引入功能提供实现的类。在这里,我们指定的是DefaultEncoreable提供实现。
  • @DeclareParents注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是Encoreable接口。和其他切面一样,我们需要将它声明为一个bean。Spring的自动代理机制会获取到它的声明,当Spring发现一个bean使用了@Aspect注解时,Spring就会创建一个代理,然后将调用委托给被代理的bean或被引入的实现,这取决于调用的方法属于被代理的bean还是属于被引入的接口。

    在XML中声明切面

AOP配置元素 用途
<aop:advisor> 定义AOP通知器
<aop:after> 定义AOP后置通知(不管被通知的方法是否执行成功)
<aop:after-returning> 定义AOP返回通知
<aop:after-throwing> 定义AOP异常通知
<aop:around> 定义AOP环绕通知
<aop:aspect> 定义一个切面
<aop:aspectj-autoproxy> 启用@AspectJ注解驱动的切面
<aop:before> 定义一个AOP前置通知
<aop:config> 顶层的AOP配置元素。大多数的<aop:*>;元素必须包含在<aop:config>元素内
<aop:declare-parents> 以透明的方式为被通知的对象引入额外的接口
<aop:pointcut> 定义一个切点

注入AspectJ切面

虽然Spring AOP能够满足许多应用的切面需求,但是与AspectJ相比,Spring AOP 是一个功能比较弱的AOP解决方案。AspectJ提供了Spring AOP所不能支持的许多类型的切点。

  例如,当我们需要在创建对象时应用通知,构造器切点就非常方便。不像某些其他面向对象语言中的构造器,Java构造器不同于其他的正常方法。这使得Spring基于代理的AOP无法把通知应用于对象的创建过程。
  对于大部分功能来讲,AspectJ切面与Spring是相互独立的。虽然它们可以织入到任意的Java应用中,这也包括了Spring应用,但是在应用AspectJ切面时几乎不会涉及到Spring。但是精心设计且有意义的切面很可能依赖其他类来完成它们的工作。如果在执行通知时,切面依赖于一个或多个类,我们可以在切面内部实例化这些协作的对象。但更好的方式是,我们可以借助Spring的依赖注入把bean装配进AspectJ切面中。

猜你喜欢

转载自www.cnblogs.com/wbw2621/p/9472007.html