Spring in Action——AOP

定义AOP术语

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

  • Before:前置通知,方法调用之前。
  • After:后置通知,方法调用之后,不关心方法的输出是什么。
  • After-returning:方法成功执行之后。
  • After-throwing:方法抛出异常之后。
  • Around:通知包裹了被通知的方法,在被通知的方法之前和调用之后执行自定义的行为。

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

切点
一个切面并不需要通知应用的所有连接点。切点有助于缩小切面所通知的连接点的范围。切点有助于缩小所通知的连接点的范围。

切面
切面是通知和切点的结合。如果说通知定义了切面的“什么”和“何时”的话,切点定义了“何处”。切面就是是什么,何时何地完成其功能。

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

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

  • 编译期:AspectJ的织入编译器就是以这种方式织入切面的。
  • 类加载期:切面在目标类加载到JVM的时候注入。这种方式需要特殊的类加载器ClassLoader。
  • 运行期:切面在应用运行的某个时刻被织入,一般情况下,在织入切面时,AOP容器为会目标对象动态的创建一个代理对象。SpringAOP就是以这种方式织入的。

Spring对AOP的支持

因为学习Spring,所以我们关注SpringAOP。话虽如此,Spring和AspectJ项目之间有大量的合作,Spring对AOP的支持在很多方面借鉴了AspectJ项目。
Spring提供了4种类型的AOP支持:

  • 基于代理的经典Spring AOP(复杂和笨重,不做介绍)
  • 纯POJO切面(需要XML支持)
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面(适用于各种Spring版本)

前三种都是Spring AOP实现的变体,它构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。
经典的Spring AOP过于复杂和笨重,会让人感到厌烦。
借助Spring的aop命名空间,我们可以将纯POJO转换为切面。实际上,这些POJO只是提供了满足切点条件所调用的方法。遗憾的是,这种技术需要XML配置,但这的确是声明式地将对象转换为切面的简便方式。

Spring通知是Java编写的
Spring在运行时通知对象
通过在代理类的包裹切面,Spring在运行期把切面织入到Spring管理的bean中。如图。
Spring in Action——AOP_第1张图片
代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。

Spring只支持方法级别的连接点
方法拦截可以满足绝大部分的需求,如果需要方法拦截之外的连接点拦截功能,那么我们可以利用Aspect来补充Spring AOP的功能。

通过切点来选择连接点

在Spring AOP中,要使用AsprctJ的切点表达式语言来定义切点。关于AspectJ切点,Spring仅支持AspectJ切点指示器的一个子集。
Spring in Action——AOP_第2张图片
Spring in Action——AOP_第3张图片
Spring在尝试使用AspectJ的其他指示器的时候,会抛出异常。

编写切点
为了阐述Spring中的切面,我们需要有个主题来定义切面的切点。为此,我们定义一个Performance接口:

package com.lee.concert;

public interface Performance {
    public void perform();
}

假设我们想编写Performance的perform()发方法触发的通知。下图展示了一个切点表达式,这个表达式能够设置当perform()方法执行时触发通知的调用。
Spring in Action——AOP_第4张图片
我们使用execution()指示器选择Perfomance的perform()方法。
方法表达式以*开始,表明了我们不关心方法返回值的类型
我们制定了全限定类名和方法名。对于方法参数列表,我们使用两个点号..表明切点要选择任意的perform()方法,无论方法的入参是什么

假设我们需要设置的切点仅匹配concert包。在此场景下,可以使用within()指示器来限制匹配:
execution(* concert.Performance.perform(..) && within(concert.*))

在切点中选择bean
除了以上的指示器外,还有一个新的bean()指示器,bean()使用bean ID来限制切点只匹配特定的bean。
execution(* concert.Performance.perform(..) && !bean('workstock'))

使用注解创建切面

使用注解来创建切面是AspectJ 5所引入的关键特性。

注:spring+mvn搭建方法:
https://blog.csdn.net/jasmine_lh/article/details/69159357

定义切面

package com.lee.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(* com.lee.concert.Performance.perform(..))")
    public void silenceCellPhones(){
        System.out.println("请电话静音");
    }
    @Before("execution(* com.lee.concert.Performance.perform(..))")
    public void takeSeats(){
        System.out.println("请坐");
    }
    @AfterReturning("execution(* com.lee.concert.Performance.perform(..))")
    public void applause(){
        System.out.println("热烈鼓掌");
    }
    @AfterThrowing("execution(* com.lee.concert.Performance.perform(..))")
    public void demandRefund(){
        System.out.println("演砸了");
    }
}

Audience类使用了@AspectJ注解进行了标注。该注解表明Audience不仅仅是一个POJO,还是一个切面。Audience类中的方法都使用注解来定义切面的具体行为。
你可能注意到了,相同的切点表达式execution...我们重复了四遍,这可能有点问题。所以我们使用@Pontcut注解定义一个可重用的切点:

package com.lee.concert;

import org.aspectj.lang.annotation.*;

@Aspect
public class Audience {
    @Pointcut("execution(* com.lee.concert.Performance.perform(..))")
    public void performance(){}
    
    @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("演砸了");
    }
}

performance()的实际内容并不重要,所以这里是空的。其实该方法本身只是一个标识而已。
当然,除了注解和没有实际操作的performance()方法。Audience类本身依然是一个POJO。我们能够像使用其他java类一样调用它的方法,它的方法也能够独立的进行单元测试。

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

而如果你使用了AspectJ注解,它仍然不被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理。如果你使用JavaConfig的话,可以使用@EnableAspectJAutoProxy注解启动自动代理功能:

package com.lee.concert;

import org.springframework.context.annotation.*;

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class ConcertConfig {
    @Bean
    public Audience audience(){
        return new Audience();
    }
}

而如果用XML的话,使用即可。

创建环绕通知

package com.lee.concert;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
public class Audience {
    @Pointcut("execution(* com.lee.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 throwable) {
            System.out.println("演砸了");
        }
    }
}

@Around注解表明watchPerformance()方法会作为performance()切点的环绕通知,这个通知的关键在于ProceedingJoinPoint作为参数的。它的proceed()方法不能忘记调用,如果不调用这个方法,你会阻塞被通知方的调用。当然你也可以多次调用已达到通知失败后的重复尝试。

处理通知中的参数
如果切面所通知的方法有参数,即我们例子中的perform()方法有参数。切面能访问和使用传递给被通知方法的参数么?

为了阐述这个问题,我们回到BlankDisc样例,假如我们有一个统计磁道并调用playTrack()方法。我们想记录每个磁道被播放的次数,一种方法是直接修改playTrack()方法,直接在每次调用的时候记录这个数量。但是,记录磁道的播放次数与播放本身是不同的关注点,所以不应该包含在playTrack()方法内。
为了记录每个磁道播放的次数,我们创建了TrackCounter类:

package com.lee.soundsystem.properties;

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

import java.util.HashMap;
import java.util.Map;

@Aspect
public class TrackCounter {
    private Map<Integer,Integer> trackCounts=
            new HashMap<>();
    @Pointcut("execution(* com.lee.soundsystem.CompactDisc.playTrack(int))" +
            "&& args(trackNumber)")
    public void trackPlayed(int trackNumber){}

    @Before("trackPlayed(trackNumber)")
    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;
    }
}

可以看出args(trackNumber)就是获取参数的关键,他的类型是int的。参数的名称和方法签名中的参数匹配。现在,我们可以将BlankDisc和TrackCounter定义为bean。

package com.lee.soundsystem.properties;

import com.lee.soundsystem.CompactDisc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import java.util.ArrayList;
import java.util.List;

@Configuration
@EnableAspectJAutoProxy
public class TrackCounterConfig {
    @Bean
    public CompactDisc sgtPeppers(){
        BlankDisc cd=new BlankDisc();
        cd.setTitle("this is title");
        cd.setArtist("this is artist");
        List<String> tracks=new ArrayList<>();
        tracks.add("1 d");
        tracks.add("2 d");
        tracks.add("3 d");
        tracks.add("4 d");
        //..更多磁道
        cd.setTracks(tracks);
        return cd;
    }
    @Bean
    public TrackCounter trackCounter(){
        return new TrackCounter();
    }
    
}

最后我们编写如下的测试,验证它能工作:

package com.lee.soundsystem.properties;

import com.lee.soundsystem.CompactDisc;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.StandardOutputStreamLog;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.Assert.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TrackCounterConfig.class)
public class TrackCounterConfigTest {
    @Rule
    public final StandardOutputStreamLog log=new StandardOutputStreamLog();

    @Autowired
    private CompactDisc cd;

    @Autowired
    private TrackCounter counter;

    @Test
    public void testTrackCounter(){
        cd.playTrack(1);
        cd.playTrack(2);
        cd.playTrack(2);
        cd.playTrack(3);
        cd.playTrack(3);
        cd.playTrack(3);

        assertEquals(1,counter.getPlayCount(1));
        assertEquals(2,counter.getPlayCount(2));
        assertEquals(3,counter.getPlayCount(3));
    }

}

很明显,获取了playTrack的arg。

通过注解引入新功能
回顾一下,在Spring中,切面只是实现了它们所包装bean相同接口的代理。如果除了实现这些接口,代理也能暴露新接口的话,会怎么样呢?
Spring in Action——AOP_第5张图片
我们需要注意的是,当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。实际上一个bean的实现被拆分到了多个类中。
为了证明该注意能够行得通,我们为Performance实现引入了Encoreable接口:

package com.lee.concert;
public interface Encoreable {
    void performEncore();
}

现在假设能够访问Performance的所有实现,并对其进行修改,让它们都实现Encoreable接口。但是从设计角度来看,这并不是很好的做法,并不是所有Performance都是具有Encoreable特性的。另一方面,有时候可能无法修改所有的Performance实现,当使用第三方实现并没有源码的时候更是如此。
值得庆幸的是,借助AOP的引入功能,我们可以不必在设计上妥协或者侵入性的改变现有实现:

package com.lee.concert;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;

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

可以看到EncoreableIntroducer是一个切面,但是它与我们之前所创建的切面不同,它并没有提供前置、后置或者环绕通知,而是通过@DeclareParents注解将Encoreable接口引入到Performance bean中。
@DeclareParents由三部分组成:

  • value属性指定了哪种类型的bean要引入该接口。(后面标记+表示Performance的所有子类型,而不是Performance本身)
  • defaultImpl属性制定了为引入功能提供实现的类。这里我们指定的是DefaultEncoreable。
  • 静态属性public static xxx xxx;表明了要引入的接口。这里指的是Encoreable接口。

在XML中声明切面

了解。暂略。P125

你可能感兴趣的:(Spring in Action——AOP)