Spring学习——AOP

1、AOP是什么

DI能够让相互协作的软件组件保持松散耦合,面向切面编程(aspect-oriented programming,AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件。

DI有助于应用对象之间解耦,而AOP可以实现横切关注点与他们所影响的对象之间的解耦

在软件开发中,散布于应用多处的功能被称为横切关注点(cross-cutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会嵌入到应用的业务逻辑中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。

2、为何要面向切面编程

横切关注点可以描述为影响应用多处的功能。为了重用通用功能,最常见的面向对象技术是继承或委托。但是,如果在整个应用程序中都使用相同的基类,继承往往会导致一个脆弱的对象体系;而使用委托可能需要对委托对象进行复杂的调用。

切面提供了取代继承和委托的另一种可选方案,而且在很多场景下更清晰简洁。在使用面向切面编程时,我们仍然可以在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)

面向切面的好处:

  1. 每个关注点都集中于一个地方,而不是分散到多处代码中
  2. 服务模块更简洁,只包含核心代码

3、AOP术语

3.1、通知(Advice)

切面的工作被称为通知。

通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题

Spring切面可以应用5种类型的通知:

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

3.2、连接点(Join point)

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

3.3、切点(Pointcut)

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

注意:连接点是应用中可以切入的时机,切点是某一个切面要织入的具体时机,也就相当于从连接点中选了一个或多个,作为某一个切面的切点。

3.4、切面(Aspect)

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

3.5引入(Introduction)

引入允许我们向现有的类添加新的方法或属性。我们可以声明新的类和实例变量并将他们引入到现有的类中,从而可以在无需修改现有类的情况下,让它们具有新的行为和状态。

3.6织入(Weaving)

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

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

4、Spring对AOP的支持

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

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

前三种都是SpringAOP实现的变体,SpringAOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截,也就是说,Spring只支持方法级别的连接点。

注意:Spring不支持构造器连接点

5、实战

以下程序为《Spring实战》第四版第四章部分代码

这里模拟了观众观看表演的场景,表演前观众需要把手机静音,就坐,表演成功会鼓掌,失败会要求退款

5.1、定义切面和要被通知的类

5.1.1、切面
@Aspect
public class Audience {

    /*
    * @Pointcut注解能够在一个@Aspect切面内定义可重用的切点
    * */
    @Pointcut("execution(* *cn.shangxiaoying.SpringAOP.concert.Performance.perform(..))")
    public void performance(){}  //这个方法是空的,只是一个标识,供@Pointcut注解依附

    /*
    * 表演之前调用该方法
    * */
    @Before("performance()")
    public void silenceCellPhones(){
        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("退款");
    }
}
5.1.2、被通知的类
@Component
public class Performance {

    public  void perform(){
        System.out.println("表演中");
    }


}

5.2、将切面装配为Spring中的bean

@Configuration
@ComponentScan(basePackages = "cn.shangxiaoying.SpringAOP")
@EnableAspectJAutoProxy  //开启AspectJ自动代理
public class ConcertConfig {

    @Bean
    public Audience audience(){
        return new Audience();
    }
}

5.3、测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ConcertConfig.class)
public class testAOPWithConfigClass {

    @Autowired
    private Performance performance;
    
    @Test
    public void testPerformance(){
        performance.perform();
    }
}

5.4、运行结果

手机静音
就坐
表演中
鼓掌

5.5、环绕通知

对于上面的代码,改造成环绕通知如下:

@Aspect
public class Audience {

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


    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint){
        try{
            System.out.println("手机静音");
            System.out.println("就坐");
            /*
            * joinPoint.proceed();这是调用被通知的方法,如果不调用该方法,通知会阻塞被通知方法的调用
            * 当然也可以多次调用该方法,比如实现重试逻辑,也就是在通知方法失败后,进行重复尝试。
            * */
            joinPoint.proceed();
            System.out.println("鼓掌");
        }catch (Throwable e){
            System.out.println("退款");
        }
    }
}

5.6、通知中的参数

带参数的切面如下:

@Aspect
public class AudienceWithArgs {

    /*
    * 定义命名的切点
    * */
    @Pointcut("execution(* *cn.shangxiaoying.SpringAOP.concert.PerformanceWithArgs.perform(int))"+"&&args(performNumber)")
    public void performance(int performNumber){}

    /*
    * 表演之前调用该方法
    * */
    @Before("performance(performNumber)")
    public void silenceCellPhones(int performNumber){
        System.out.println("手机静音");
    }

    /*
    * 表演之前调用该方法
    * */
    @Before("performance(num)")//注解里的参数名称和方法的参数名称要相同,和上面切点定义的参数名可以不同
    public void takeSeats(int num){
        System.out.println("就坐");
    }

    /*
    * 表演失败之后调用该方法
    * */
    @AfterReturning("performance(performNumber)")
    public void applause(int performNumber){
        System.out.println("鼓掌");
    }

    /*
    * 表演之前调用该方法
    * */
    @AfterThrowing("performance(performNumber)")
    public void demandRefund(int performNumber){
        System.out.println("退款");
    }
}

其他步骤与上面相同,这里直接写测试类以及运行结果

测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ConcertConfig.class)
public class testAOPWithConfigClass {

    @Autowired
    private PerformanceWithArgs performanceWithArgs;

    @Test
    public void testPerformanceWithArgs(){

        for (int i=0;i<10;i++)
            performanceWithArgs.perform(i);
    }
}

运行结果:

手机静音
就坐
第0场表演进行中
鼓掌
手机静音
就坐
第1场表演进行中
鼓掌
手机静音
就坐
第2场表演进行中
鼓掌
手机静音
就坐
第3场表演进行中
鼓掌
手机静音
就坐
第4场表演进行中
鼓掌
手机静音
就坐
第5场表演进行中
鼓掌
手机静音
就坐
第6场表演进行中
鼓掌
手机静音
就坐
第7场表演进行中
鼓掌
手机静音
就坐
第8场表演进行中
鼓掌
手机静音
就坐
第9场表演


代码书写世界,吉他演奏生活