【读书笔记】《Spring实战》第4章 面向切面的Spring

198 阅读9分钟

AOP术语

通知:通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?

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

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

连接点(Join Point):连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。

切点(Pointcut):如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。连接点说明目标对象哪些位置可以切入通知,切点则是实际进行切入的一个或多个连接点。

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

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

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

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

Spring AOP的四种类型

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

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

Spring 通知是 Java 编写的。

Spring在运行时通知对象。通过在代理类中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。

Spring AOP是基于动态代理的,只支持方法级别的连接点,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象;对于没有实现接口的对象,无法使用 JDK Proxy 去进行代理,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理。

AspectJ 切点指示器

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

Spring 仅支持 AspectJ 切点指示器(pointcut designator)的一个子集。

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

使用注解创建切面

示例代码结构说明:

  1. Performance接口定义了表演方法,该方法有两个实现类Movie和Drama。
// Performance.java
package org.tang.it.aop;
public interface Performance {
    public void perform();
}
// Movie.java
package org.tang.it.aop;
@Component
@Qualifier("movie")  //限定符, 用于消除自动装配bean时的歧义性
public class Movie implements Performance{
    public void perform() {
        System.out.println("---The movie is showing---");
    }
}
// Drama.java
package org.tang.it.aop;
@Component
@Qualifier("drama") //限定符, 用于消除自动装配bean时的歧义性
public class Drama implements Performance{
    public void perform() {
        System.out.println("***The drama is on***");
    }
}
  1. 定义切面,切点为Performance接口的perform方法,对Movie和Drama两个实现类使用不同的通知。

定义切面

使用到的主要注解:

  • @Pointcut 注解:在一个 @AspectJ 切面内定义可重用的切点。
  • @After注解:通知方法会在目标方法返回或抛出异常后调用。
  • @AfterReturning注解:通知方法会在目标方法返回后调用。
  • @AfterThrowing注解:通知方法会在目标方法抛出异常后调用。
  • @Around注解:通知方法会将目标方法封装起来。
  • @Before注解:通知方法会在目标方法调用之前执行。

切面定义示例如下:

// Audience.java
package org.tang.it.aop;
@Aspect
@Component // 声明为Spring Bean,保证该切面类能被装载注入
public class Audience {
    // 使用pointcut定义切连接点, 便于复用.  表示切点左右的连接点是Performance的perform()方法
    // 通过bean指示符限制该连接点只对名为movie的bean生效
    @Pointcut("execution(** org.tang.it.aop.Performance.perform(..)) && bean(movie)")
    public void performce() { }

    @Before("performce()")
    public void silenceCellPhones() {
        System.out.println("---AOP:Silencing cell phones");
    }

    @Before("performce()")
    public void takeSeats() {
        System.out.println("---AOP:Taking seats");
    }

    @AfterReturning("performce()")
    public void applause() {
        System.out.println("---AOP:CLAP CLAP CLAP!!!");
    }

    @AfterThrowing("performce()")
    public void demandRefund() {
        System.out.println("---AOP:Demanding a refund");
    }
}

目前Audience 还只是 Spring 容器中的一个 bean。即便使用了 AspectJ 注解,但它并不会被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理。如果你使用 JavaConfig 的话,可以在配置类的类级别上通过使用 EnableAspectJAutoProxy 注解启用自动代理功能。示例如下:

@Configuration
@ComponentScan(basePackages={"org.tang.it.aop"})
@EnableAspectJAutoProxy
public class ConcertConfig {
}

至此,AspectJ 自动代理都会为使用 @Aspect 注解的 bean 创建一个代理,这个代理会围绕着所有该切面的切点所匹配的 bean。在这种情况下,将会为movie bean 创建一个代理,Audience 类中的通知方法将会在 perform() 调用前后执行。

Spring 的 AspectJ 自动代理仅仅使用 @AspectJ 作为创建切面的指导,切面依然是基于代理的。

创建环绕通知

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

// Audience.java
package org.tang.it.aop;
@Aspect
@Component // 声明为Spring Bean, 保证该切面类能被装载注入
public class Audience {
    //定义切连接点, 便于复用. 通过target指示符限制该连接点只对Dram类生效
    @Pointcut("execution(** org.tang.it.aop.Performance.perform(..)) && target(org.tang.it.aop.Drama)")
    public void performceDrama() { }

    @Around("performceDrama()")
    public void watchPerformance(ProceedingJoinPoint jp) {
        try {
            System.out.println("***AOP:Silencing cell phones");
            System.out.println("***AOP:Taking seats");
            jp.proceed();
            System.out.println("***AOP:CLAP CLAP CLAP!!!");
        } catch (Throwable e) {
            System.out.println("***AOP:Demanding a refund");
        }
    }
}

Around通知方法接受 ProceedingJoinPoint 作为参数。这个对象是必须要有的,因为要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用 ProceedingJoinPoint 的 proceed() 方法。

测试验证示例代码如下:

package org.tang.it.aop;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=ConcertConfig.class)
public class ConcertTest {
    @Autowired  //自动注入Bean
    @Qualifier("movie")  //Performance接口的两个实现类都声明为组件Bean,通过限定符明确注入哪个实现类
    Performance movie;

    @Autowired
    @Qualifier("drama")
    Performance drama;

    @Test
    public void testMoviePerform() {
        System.out.println("---Perform process start---");
        movie.perform();
        System.out.println("---Perform process end---");

        System.out.println();

        System.out.println("***Perform process start***");
        drama.perform();
        System.out.println("***Perform process end***");
    }
}

输出结果:

---Perform process start---
---AOP:Silencing cell phones
---AOP:Taking seats
---The movie is showing---
---AOP:CLAP CLAP CLAP!!!
---Perform process end---

***Perform process start***
***AOP:Silencing cell phones
***AOP:Taking seats
***The drama is on***
***AOP:CLAP CLAP CLAP!!!
***Perform process end***

通过注解引入新功能

在AOP术语中引入(Introduction)表示允许我们向现有的类添加新方法或属性。这里介绍如果通过Spring AOP给bean添加其他。

在 Spring 中,切面只是实现了它们所包装 bean 相同接口的代理。如果除了实现这些接口,代理也能暴露新接口的话,会怎么样呢?那样的话,切面所通知的 bean 看起来像是实现了新的接口,即便底层实现类并没有实现这些接口也无所谓,原理如下:

image.png

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

示例代码:验证之前的代码,Movie类依然是Performance接口的实现类,现在我们希望在不改动接口Performance和实现类Movie的情况下,给Performance实现类的bean添加新方法。

  1. 新方法放在Encoreable接口和DefaultEncoreable实现类中。
// Encoreable.java
package org.tang.it.aop;
public interface Encoreable {
    void performEncore();
}
// DefaultEncoreable.java
package org.tang.it.aop;
@Component
public class DefaultEncoreable implements Encoreable {
    public void performEncore() {
        System.out.println("---AOP: Introduced function proceed!!!");
    }
}
  1. 定义切面。
@Aspect
@Component
public class EncodeableIntroducer {
    @DeclareParents(value="org.tang.it.aop.Performance+", defaultImpl=DefaultEncoreable.class)
    public static Encoreable encoreable;
}

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

@DeclareParents 注解由三部分组成:

  • value 属性指定了哪种类型的 bean 要引入该接口。在本例中,也就是所有实现 Performance 的类型。(标记符后面的加号表示是 Performance 的所有子类型,而不是 Performance 本身。)
  • defaultImpl 属性指定了为引入功能提供实现的类。在这里,我们指定的是 DefaultEncoreable 提供实现。
  • @DeclareParents 注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是 Encoreable 接口。
  1. 测试验证
// 配置类
package org.tang.it.aop;
@Configuration
@ComponentScan(basePackages={"org.tang.it.aop"})
@EnableAspectJAutoProxy
public class ConcertConfig {
}
// 测试代码
package org.tang.it.aop;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=ConcertConfig.class)
public class ConcertTest {
    @Autowired 
    @Qualifier("movie")  
    Performance movie;

    @Test
    public void testMoviePerform() {
        System.out.println("---Spring AOP introduction demo---");
        ((Encoreable)movie).performEncore();  // 强制类型转换以调用新方法

        String className = movie.getClass().getName();
        System.out.println(className);
        Class<?>[] interfaceNames = movie.getClass().getInterfaces();
        for (Class inter : interfaceNames) {
            System.out.println(inter);
        }
    }
}

输出结果:

---Spring AOP introduction demo---
---AOP: Introduced function proceed!!!
com.sun.proxy.$Proxy23
interface org.tang.it.aop.Performance
interface org.tang.it.aop.Encoreable
interface org.springframework.aop.SpringProxy
interface org.springframework.aop.framework.Advised

可以看到movie是代理类,这个代理类同时实现了Performance接口和Encoreable接口。