《SpringBoot 3:入门与应用实战》第 7 章 AOP 思想与实现 阅读笔记 15

《SpringBoot 3:入门与应用实战》第 7 章 AOP 思想与实现 阅读笔记 15

7.4 Spring Boot 使用 AOP---基于 AspectJ

下面分别讲解使用 Spring Boot 和原生 Spring Framework 的方式整合使用 AOP。首先讲解 Spring Boot 的方式,由于 Spring Boot 更推荐使用注解驱动 + JavaConfig 的方式编写代码,而这又是当下的主流方式,因此读者可以把更多精力放到这个环节中。

7.4.1 搭建工程环境

xml 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aspectj</artifactId>
        </dependency>

7.4.2 前置测试代码编写

为了使接下来的演示代码更具有通用性,下面制作一个 Service 层的接口、一个接口的实现类、一个普通的 Service 类,以及一个切面类Logger。

java 复制代码
package com.yangjunbo.springboot.aop.examplea;

import java.util.List;

public interface OrderService {

    void createOrder();

    void deleteOrderById(String id);

    String getOrderById(String id);

    List<String> findAll();

}
java 复制代码
package com.yangjunbo.springboot.aop.examplea;

import java.util.Arrays;
import java.util.List;

@Service
public class OrderServiceImpl implements OrderService {
    
    @Override
    public void createOrder() {
        System.out.println("OrderServiceImpl 创建订单。。。");
    }
    
    @Override
    public void deleteOrderById(String id) {
        System.out.println("OrderServiceImpl 删除订单,id为" + id);
    }
    
    @Override
    public String getOrderById(String id) {
        System.out.println("OrderServiceImpl 查询订单,id为" + id);
        return id;
    }
    
    @Override
    public List<String> findAll() {
        System.out.println("OrderServiceImpl 查询所有订单。。。");
        return Arrays.asList("111", "222", "333");
    }

}
java 复制代码
package com.yangjunbo.springboot.aop.examplea;

@Component
public class FinanceService {
    
    public void addMoney(double money) {
        System.out.println("FinanceService 收钱 === " + money);
    }

    public double subtractMoney(double money) {
        System.out.println("FinanceService 付钱 === " + money);
        return money;
    }

    public double getMoneyById(String id) {
        System.out.println("FinanceService 查询账户,id为" + id);
        return Math.random();
    }

}
java 复制代码
package com.yangjunbo.springboot.aop.examplea;

public class Logger {

    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }

    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

}

7.4.3 基于注解的 AOP 编写

要使用注解式 AOP 需要经过以下几个步骤,每个步骤都相对简单,下面逐一讲解。

1.开启注解式 AOP 支持

为了使用注解式 AOP,我们需要在配置类(或 Spring Boot 主启动类)上标注一个注解:@EnableAspectJAutoProxy,从注解名上读者是否很强烈地感受到它采用了模块装配的思想,其作用就是让 Spring Boot 项目支持 AOP 特性,后续若想与 AOP 相关的所有代码生效,必须有该注解的支持。

java 复制代码
package com.yangjunbo.springboot.aop;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableAspectJAutoProxy
public class SpringbootAopDApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootAopDApplication.class, args);
    }

}

即便不标注 @EnableAspectJAutoProxy 注解,Spring Boot 的自动装配机制也会在底层帮我们标注该注解,开启 AOP 支持。

@EnableAspectJAutoProxy 注解有以下两个属性。

  • proxyTargetClass:是否强制使用 Cglib 动态代理的方式。
  • exposeProxy:是否将代理对象暴露出来,供全局获取(将在第 8 章中讲解)。

2.声明切面类

很明显 Logger 是具备功能增强逻辑的切面类,而 OrderService 和 FinanceService 则是具体的业务逻辑类。首先声明切面类,要想让Logger 变成一个被 Spring Framework 管理的切面类,需要在类上标注两个注解:@Component、@Aspect。

java 复制代码
package com.yangjunbo.springboot.aop.examplea;

import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class Logger {

    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }

    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

}

切面类必须同时声明 @Component 和 @Aspect 注解,如果仅声明 @Aspect 注解,没有组件扫描或手动注册的动作,那么 IOC 容器不会将其注册为 IOC 容器中的 Bean。

3.声明通知

在 Logger 类中定义了 4 个方法,分别对应 AspectJ 中规定的除环绕通知外的 4 种通知类型,相应地,在 Spring Framework 整合 AspectJ 的编码方式时,也提供了一一对应的注解,分别是 @Before、@After、@AfterReturning、@AfterThrowing。本节先介绍前置通知对应的 @Before。

使用前置通知时,对应 Logger 类中的方法是 beforePrint,当把 @Before 注解标注到方法上时,发现它需要我们提供一个 value 属性,这个属性值的编写方式不是任意的,而是有严格的格式,这套格式被称为 "切入点表达式"。

java 复制代码
package com.yangjunbo.springboot.aop.examplea;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class Logger {

    @Before("execution(public void com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney(double))")
    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }

    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

}

下面解释这个切入点表达式的含义。

  • execution:使用 execution 编写的表达式,会直接作用于类中相应的方法。
  • public:限定只切入 public 类型的方法。
  • void:限定只切入返回值类型为 void 的方法。
  • com.yangjunbo.springboot.aop.examplea.FinanceService:限定只切入 FinanceService 这个类的方法。
  • addMoney:限定只切入方法名为 addMoney 的方法。
  • (double):限定只切入方法的参数列表中只有一个参数且其类型为 double 的方法。

因此,使用上述的切入点表达式,就可以直接锁定到 FinanceService 的 addMoney 方法。

在编写完切入点表达式后,使用 IDEA 的读者可以发现在 @Before 注解的左边多了一个图标,单击该图标,就会跳转到 FinanceService的 addMoney 方法,同时 addMoney 方法的左边有一个方向相反的图标。

4.测试效果

下面通过简单的运行代码检验 AOP 是否生效。因为 SpringApplication.run 方法的返回值就是 IOC 容器,所以可以在获得 IOC 容器后取出 FinanceService 并调用其方法,观察控制台的输出,检验 AOP 的效果。

java 复制代码
package com.yangjunbo.springboot.aop;

import com.yangjunbo.springboot.aop.examplea.FinanceService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@EnableAspectJAutoProxy
@SpringBootApplication
public class SpringbootAopDApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = SpringApplication.run(SpringbootAopDApplication. class, args);
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        financeService.subtractMoney(543.21);
        financeService.getMoneyById("abc");
    }
}

运行主启动类,控制台中打印了如下 4 行内容,证明 Logger 的前置通知方法 beforePrint 被触发,而其余两个方法没有增强逻辑,运行效果符合预期。

7.4.4 切入点表达式的编写方式

通过 7.4.3 节的内容,想必读者对 AOP 的使用有了基本的了解,AOP 的开发原则正如上述的步骤,即确定切面类和通知方法,随后给通知方法标注注解,声明切入点表达式。切入点表达式的写法比较多,下面基于 7.4.3 节的切入点表达式逐渐演变,讲解更多的切入点表达式编写方式。

1.基本通配符

把代码清单 7-31 中的切入点表达式稍做修改,即可得到一个可以匹配更多方法的切入点表达式:

execution(public * com.linkedbear.springboot.aop.a_aspectj.service.FinanceService.*(double)),

这个表达式中有两个位置替换成了通配符 *,它们的含义分别如下。

  • void 的位置替换为 *,代表不限制返回值类型,任意返回值类型都会被匹配。
  • FinanceService.*(double) 这里的方法名替换为 *,代表不限制方法名,任意方法都可以切入。

由此可见,上述的切入点表达式可以切入的方法扩展到 2 个,除了 addMoney 方法,subtractMoney 也可以被切入。

匹配效果是否真的如此,可以再编写一个后置通知加以检验。找到 Logger 类的 afterPrint 方法,在该方法上标注 @After,并声明切入点表达式。

java 复制代码
package com.yangjunbo.springboot.aop.examplea;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class Logger {

    @Before("execution(public void com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney(double))")
    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }

    @After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (double)))")
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

}

其他代码不需要做改动,重新运行主启动类,观察控制台的输出中包含两行 Logger afterPrint run,证明 afterPrint 方法被调用两次,后置通知也生效了。

在切入点表达式的方法参数中,对于基本数据类型直接声明即可;对于引用数据类型则要写类的全限定名。

2.方法参数通配符

如果继续修改上面的切入点表达式,将最后括号内的内容由 double 改为 *,则意味着被切入的方法只需要一个入参,对于参数的类型则不作限制。修改完成后,再次执行主启动类,可以发现控制台输出中有 3 行 Logger afterPrint run ...,说明 getMoneyById 方法也被增强。

3.类名通配符

下面继续变换切入点表达式的内容,如果将 FinanceService 替换为 *,则意味着 OrderService 接口的方法也会被切入。但是请注意一点,由于FinanceService 与 OrderService 接口位于同一个包下,而 OrderServiceImpl 与 FinanceService 不在同一个包下,这是否意味着OrderServiceImpl 的方法不会被增强呢?下面来回答该问题。

找到 Logger 的 afterReturningPrint 方法,并在方法上标注 @AfterReturning 注解。这下无须运行程序来验证,仅凭单击 IDEA 提示的通知标识按钮就能得知,OrderServiceImpl 的两个方法 deleteOrderById 和 getOrderById 也被切入,这就意味着当切入点表达式覆盖到了接口,如果这个接口有实现类并且注册到 IOC 容器中成为 bean 对象,那么实现类中对应的方法会被增强。

4.方法参数任意通配符

回到 FinanceService中,给 subtractMoney 重载一个两参数方法。

java 复制代码
package com.yangjunbo.springboot.aop.examplea;

import org.springframework.stereotype.Component;

@Component
public class FinanceService {
    
    public void addMoney(double money) {
        System.out.println("FinanceService 收钱 === " + money);
    }

    public double subtractMoney(double money) {
        System.out.println("FinanceService 付钱 === " + money);
        return money;
    }

    public double subtractMoney(double money, String id) {
        System.out.println("FinanceService 付钱 === " + money);
        return money;
    }

    public double getMoneyById(String id) {
        System.out.println("FinanceService 查询账户,id为" + id);
        return Math.random();
    }

}

当代码编写完毕后,借助 IDEA 会发现左侧并没有切入点的图标,这就说明切入点表达式的 (*) 并不能切入两参数的方法,而如果想要切入任意个参数的方法,或者没有参数的方法,就要用到一个特殊符号:...,即双点号。如此编写完毕后,包括 FinanceService 和 OrderService 的所有方法都会被切入。

5.包名通配符

与类名、方法名的通配符一样,一个 * 代表一个目录层级,

比如下面的切入点表达式代表切入 com.yangjunbo.springboot.aop.examplea 包下的所有一级包内任意类的任意方法,

诸如

com.yangjunbo.springboot.aop.examplea.controller、

com.yangjunbo.springboot.aop.examplea.service、

com.yangjunbo.springboot.aop.examplea.dao

等包下的所有类都会被切入。

java 复制代码
execution(public * com.yangjunbo.springboot.aop.examplea.*.*.*(..)))

如果需要切入多层级包,则同样可以使用双点号 ... 匹配任意层级的包,如下面的切入点表达式就可以切入 com.yangjunbo.springboot 下的所有类的所有方法。

java 复制代码
execution(public * com.yangjunbo.springboot..*.*(..)))

另外,如果去掉 public 修饰符,则所有访问修饰符修饰的方法都会被切入,通常在编写切入点表达式时不会带有 public。

6.抛出异常的切入

四大主要通知的最后一种是异常通知,如果需要切入有异常抛出的方法,需要在切入点表达式上声明抛出异常的类型。借助 IDEA 可以看到,如此声明后 afterThrowingPrint 方法只会增强两参数的 subtractMoney 方法。

7.使用 @annotation

除 execution 之外,还有一种切入点表达式也比较常用:@annotation(),这种切入点表达式的使用方式相对简单,只需要声明注解的全限定名。简单测试一下效果,切面类依然选择使用 Logger,之后声明一个 @Log,用于标注要打印日志的方法。

java 复制代码
package com.yangjunbo.springboot.aop.examplea;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
    
}

相应的切入点表达式只需要声明 @annotation(com.yangjunbo.springboot.aop.examplea.Log),采用该方法声明的切入点表达式会搜索整个 IOC容器中所有标注了 @Log 的 bean 对象,并对其进行增强。

java 复制代码
package com.yangjunbo.springboot.aop.examplea;

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

@Component
@Aspect
public class Logger {

    //@Before("execution(public void com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney(double))")
    @Before("@annotation(com.yangjunbo.springboot.aop.examplea.Log)")
    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }

    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (double)))")
    @After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (*)))")
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(*)))")
    @AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    @AfterThrowing("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..) throws java.lang.Exception)")
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

}

替换 @Before 的表达式,并在 FinanceService 的 addMoney 方法上标注 @Log。运行主启动类,观察控制台可以发现,beforePrint 方法对应的前置通知只打印了一次,且的确位于 addMoney 方法调用之前,证明基于注解的切入点表达式生效。

8.抽取通用切入点表达式

如果一个切面类中出现了两个通知方法,且其切入点表达式都是一样的,那么可以使用 @Pointcut 注解抽取通用的切入点表达式。抽取的方式很简单,只需要将 @Pointcut 注解声明在一个没有返回值且方法体为空的方法中。其他通知要引用该通用切入点表达式时,只需要标注方法名,无须重复编写。

7.4.5 使用环绕通知

下面讲解基于 AspectJ 的环绕通知编写。

1.添加新的环绕通知方法

回到 Logger 类中定义一个新的方法 aroundPrint,并标注环绕通知的 @Around,配置切入点表达式。

java 复制代码
package com.yangjunbo.springboot.aop.examplea;

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

@Component
@Aspect
public class Logger {

    @Pointcut("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    public void defaultPointcut() {

    }

    //@Before("execution(public void com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney(double))")
    @Before("@annotation(com.yangjunbo.springboot.aop.examplea.Log)")
    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }

    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (double)))")
    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (*)))")
    @After("defaultPointcut()")
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(*)))")
    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    @AfterReturning("defaultPointcut()")
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    @AfterThrowing("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..) throws java.lang.Exception)")
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

    @Around("execution(* com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney (*))")
    public void aroundPrint() {}

}

需要了解环绕通知中的一个特殊参数:ProceedingJoinPoint。在 aroundPrint 方法的参数中添加 ProceedingJoinPoint,并把方法的返回值类型改为 Object。如果不需要对方法进行增强,直接调用 ProceedingJoinPoint 的 proceed 方法就可以执行目标对象的目标方法。

java 复制代码
package com.yangjunbo.springboot.aop.examplea;

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

@Component
@Aspect
public class Logger {

    @Pointcut("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    public void defaultPointcut() {

    }

    //@Before("execution(public void com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney(double))")
    @Before("@annotation(com.yangjunbo.springboot.aop.examplea.Log)")
    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }

    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (double)))")
    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (*)))")
    @After("defaultPointcut()")
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(*)))")
    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    @AfterReturning("defaultPointcut()")
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    @AfterThrowing("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..) throws java.lang.Exception)")
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

    @Around("execution(* com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney (*))")
    public Object aroundPrint(ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed(); 
    }

}

剩下的内容就是根据实际业务逻辑编写相应的代理增强逻辑,诸如记录日志、事务控制等,很明显四行控制台输出刚好对应了四种通知类型。

java 复制代码
package com.yangjunbo.springboot.aop.examplea;

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

@Component
@Aspect
public class Logger {

    @Pointcut("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    public void defaultPointcut() {

    }

    //@Before("execution(public void com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney(double))")
    @Before("@annotation(com.yangjunbo.springboot.aop.examplea.Log)")
    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }

    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (double)))")
    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (*)))")
    @After("defaultPointcut()")
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(*)))")
    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    @AfterReturning("defaultPointcut()")
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    @AfterThrowing("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..) throws java.lang.Exception)")
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

    @Around("execution(* com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney (*))")
    public Object aroundPrint(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Logger aroundPrint before run ......");
        try {
            Object retVal = joinPoint.proceed();
            System.out.println("Logger aroundPrint afterReturning run ......");
            return retVal;
        } catch (Throwable e) {
            System.out.println("Logger aroundPrint afterThrowing run ......");
            throw e;
        } finally {
            System.out.println("Logger aroundPrint after run ......");
        }
    }

}

2.测试效果

将 Logger 中除 @Before 之外的其他通知注解都暂时注释掉,之后重新运行主启动类,观察控制台的输出,可发现同时包含环绕通知和前置通知。

java 复制代码
package com.yangjunbo.springboot.aop.examplea;

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

@Component
@Aspect
public class Logger {

    @Pointcut("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    public void defaultPointcut() {

    }

    //@Before("execution(public void com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney(double))")
    @Before("@annotation(com.yangjunbo.springboot.aop.examplea.Log)")
    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }

    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (double)))")
    //@After("execution(public * com.yangjunbo.springboot.aop.examplea.FinanceService.* (*)))")
    //@After("defaultPointcut()")
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(*)))")
    //@AfterReturning("execution(public * com.yangjunbo.springboot.aop.examplea.*.*(..)))")
    //@AfterReturning("defaultPointcut()")
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    //@AfterThrowing("execution(* com.yangjunbo.springboot.aop.examplea.*.*(..) throws java.lang.Exception)")
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

    @Around("execution(* com.yangjunbo.springboot.aop.examplea.FinanceService.addMoney (*))")
    public Object aroundPrint(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Logger aroundPrint before run ......");
        try {
            Object retVal = joinPoint.proceed();
            System.out.println("Logger aroundPrint afterReturning run ......");
            return retVal;
        } catch (Throwable e) {
            System.out.println("Logger aroundPrint afterThrowing run ......");
            throw e;
        } finally {
            System.out.println("Logger aroundPrint after run ......");
        }
    }

}

另外,根据打印的先后顺序可以得出一个结论:同一个切面类中,环绕通知的执行时机比单个通知要早。

以上就是基于 Spring Boot 的注解式 AOP 编写方式,这是当下的主流开发中最常使用的方式,读者一定要多加练习。

7.5 Spring 使用 AOP---基于 XML

使用 AOP 并不是 Spring Boot 的专利,Spring Framework 从最开始就设计了基于 XML 的 AOP。本节将快速讲解基于 XML 的 AOP 方式,由于当下主流开发 Spring Boot 的应用中已经很难见到 XML 配置文件,因此这部分内容不会作为重点讲解。

7.5.1 搭建工程环境

原生 Spring Framework 使用 AOP 时不需要额外导入其他的 Spring Framework 依赖坐标,仅需导入 spring-context 依赖,借助 IDEA 的 Maven插件可以看到,spring-context 坐标中已经传递依赖了 spring-aop 的包。

此外,还要手动导入两个依赖,分别是 Cglib 和 AspectJ 的坐标。如果没有这两个依赖,后面的演示将无法正确进行。

接下来准备基础代码,只需像 7.4.2 节的内容那样准备一份同样的基础代码,此处不再贴出。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.yangjunbo</groupId>
        <artifactId>springboot-example-jetli</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>spring-aop-a</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.9</version>
        </dependency>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.19</version>
        </dependency>

    </dependencies>

</project>
java 复制代码
package com.yangjunbo.aop.examplea;

import java.util.List;

public interface OrderService {

    void createOrder();

    void deleteOrderById(String id);

    String getOrderById(String id);

    List<String> findAll();

}
java 复制代码
package com.yangjunbo.aop.examplea.impl;

import com.yangjunbo.aop.examplea.OrderService;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

public class OrderServiceImpl implements OrderService {
    
    @Override
    public void createOrder() {
        System.out.println("OrderServiceImpl 创建订单。。。");
    }
    
    @Override
    public void deleteOrderById(String id) {
        System.out.println("OrderServiceImpl 删除订单,id为" + id);
    }
    
    @Override
    public String getOrderById(String id) {
        System.out.println("OrderServiceImpl 查询订单,id为" + id);
        return id;
    }
    
    @Override
    public List<String> findAll() {
        System.out.println("OrderServiceImpl 查询所有订单。。。");
        return Arrays.asList("111", "222", "333");
    }

}
java 复制代码
package com.yangjunbo.aop.examplea;

import org.springframework.stereotype.Component;

public class FinanceService {

    public void addMoney(double money) {
        System.out.println("FinanceService 收钱 === " + money);
    }

    public double subtractMoney(double money) {
        System.out.println("FinanceService 付钱 === " + money);
        return money;
    }

    public double subtractMoney(double money, String id)  throws Exception {
        System.out.println("FinanceService 付钱 === " + money);
        return money;
    }

    public double getMoneyById(String id) {
        System.out.println("FinanceService 查询账户,id为" + id);
        return Math.random();
    }

}
java 复制代码
package com.yangjunbo.aop.examplea;

import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

public class Logger {

    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }

    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }

    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }

    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }

}

7.5.2 编写配置文件

使用 XML 配置文件的方式使用 AOP 时,先要将原始类和切面类都注册到 IO C容器,使其成为对应的 bean 对象。请注意代码中导入的命名空间, 标签中除了声明 beans 系列的标签作为默认命名空间,还导入了 aop 的命名空间,这样就可以在这个 XML 文件中使用 aop 的标签。

当导入命名空间后,在配置文件的空白位置输入 aop 前缀,会发现 IDEA 给予了 aop 命名空间中的 3 个根标签。本书重点讲解前两个标签,最后一个标签使用频率极低,感兴趣的读者可以自行查阅资料了解。

xml 复制代码
<?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: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 https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="financeService" class="com.yangjunbo.aop.examplea.FinanceService"/>
    <bean id="orderService" class="com.yangjunbo.aop.examplea.impl.OrderServiceImpl"/>
    <bean id="logger" class="com.yangjunbo.aop.examplea.Logger"/>
    
</beans>

配置 AOP 的方式大致可以拆分为两步,

使用 XML 的方式声明切面时需要先声明一对 aop:config 标签,并在其中使用 aop:aspect 标签声明一个切面,由于切面需要基于 IOC 容器中的一个特定 Bean,而切面本身又有自己的名字,因此要分别声明 id 和 ref 属性;

之后在 aop:aspect 标签中即可使用包括 aop:before、aop:after 等在内的几种通知标签,引用切面类中的方法并声明切入点表达式,还可以使用 aop:pointcut 标签声明通用的切入点表达式,整体上与使用注解式并无太大差别。

xml 复制代码
<?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: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 https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="financeService" class="com.yangjunbo.aop.examplea.FinanceService"/>
    <bean id="orderService" class="com.yangjunbo.aop.examplea.impl.OrderServiceImpl"/>
    <bean id="logger" class="com.yangjunbo.aop.examplea.Logger"/>

    <aop:config>
        <aop:aspect id="loggerAspect" ref="logger">
            <aop:pointcut id="defaultPointcut" expression="execution(public * com.yangjunbo.aop.examplea.*.*(..))"/>
            <aop:before method="beforePrint" pointcut="execution(public void com.yangjunbo.aop.examplea.FinanceService.addMoney(double))"/>
            <aop:after method="afterPrint" pointcut="execution(public * com.yangjunbo.aop.examplea.FinanceService.*(..))"/>
            <aop:after-returning method="afterReturningPrint" pointcut-ref="defaultPointcut"/>
            <aop:after-throwing method="afterThrowingPrint" pointcut="execution(public * com.yangjunbo.aop.examplea.*.*(..) throws Exception)"/>
        </aop:aspect>
    </aop:config>

</beans>

与注解式切面类相似,当在 XML 配置文件中声明 AOP 切面后,IDEA 中配置文件的左侧也会出现可跳转的图标,单击图标也能跳转到相应可以切入的方法中。

7.5.3 测试效果

为检验 AOP 是否生效,下面编写一个具备 main 方法的测试启动类 XmlAspectApplication 检验效果。使用 XML 配置文件驱动 IOC 容器,并将IOC 容器的 FinanceService 和 OrderService 都取出并调用其方法,观察控制台的输出。

运行main方法,控制台中打印的内容颇多,但经过简单分辨我们就能识别出来每个通知方法打印的依据。

java 复制代码
package com.yangjunbo.aop.examplea;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class XmlAspectApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-aop.xml");
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        financeService.subtractMoney(543.21);
        financeService.getMoneyById("abc");
    
        OrderService orderService = ctx.getBean(OrderService.class);
        orderService.createOrder();
        orderService.getOrderById("abcde");
    }
}

7.6 小结

本章接续第 2 章的 IOC 思想演变过程继续向下推演,从 GoF 23 设计模式出发逐步分析出 AOP 的核心机制---动态代理。作为 OOP 的补充,AOP 引入了切面的思想,将共用代码抽取为通用的切面,并通过增强逻辑与原始目标对象的织入动作构造出代理对象,实现动态增强原有逻辑的效果。

AOP 的底层支撑机制就是动态代理,Spring Framework 通过借助 JDK 原生的动态代理和 Cglib 字节码增强技术,并整合 AspectJ 的编码方式形成现有的 AOP 模式。

此外,本章通过 Spring Boot 和原生 Spring Framework 的工程环境使用 AOP 技术,并通过应用多种通知类型和切入点表达式帮助读者了解和上手 AOP 的核心编程内容,读者要熟练掌握本章中讲到的通知类型应用场景和切入点表达式的编写方式。

相关推荐
深圳老胡2 分钟前
STM32 MCU 国产替代型号简介
笔记·stm32·单片机·代码规范
ShineWinsu13 分钟前
对于Redis:缓存的解析
java·redis·缓存·缓存穿透·缓存击穿·缓存雪崩·缓存预热
rannn_11122 分钟前
【Java面试题】高频面试题1|Java 后端、MySQL、Redis 与 RAG 面试整理
java·jvm·数据库·后端·面试
写后端的胖头鱼25 分钟前
一文详解Spring 事务失效场景
java·spring·事务·transactional·事务传播机制
Q264336502329 分钟前
【有源码】基于Spring Boot和Vue的在线考试与交流系统-教育信息化背景下在线考试与交流一体化平台
java·vue.js·spring boot·mysql·spring·毕业设计·课程设计
Mikko740 分钟前
JVM 线上排查实战(七):jps 看不到进程、jstack 报不允许的操作怎么办?attach 失败的六种情况实测
java·运维·jvm·后端
jianjinwssy40 分钟前
MyBatis-Plus 从入门到实战:常用功能与核心原理
java·mybatis
会编程的吕洞宾1 小时前
Spring AI 2.0 会话记忆实战,让 AI 助手记住每一次对话
java·人工智能·spring
小宋10211 小时前
Agent 工具升级如何不破坏线上:Tool Schema 版本兼容与契约测试
java·人工智能·后端·spring
汤米粥1 小时前
后端开发主流技术方案
java·python·golang·php·nodejs·后端开发