1. 概述

面向切面编程(AOP)通过将横切关注点从主业务逻辑中分离为独立单元(称为切面),从而改进程序设计。Spring AOP 是一个帮助我们轻松实现切面的框架。

AOP 切面与其他软件组件并无不同,它们同样需要通过不同测试来验证正确性。本教程将学习如何对 Spring AOP 切面进行单元测试和集成测试。

2. 什么是 AOP?

AOP 是一种编程范式,它补充了面向对象编程(OOP),用于模块化横切关注点——这些功能贯穿于应用程序的多个部分。OOP 的基础单元是类,而 AOP 的基础单元是切面。 日志记录和事务管理是典型的横切关注点示例。

切面由两个组件组成:

  • 通知(Advice):定义横切关注点的具体逻辑
  • 切入点(Pointcut):指定在应用程序执行期间何时应用该逻辑

下表概述了常见 AOP 术语:

术语 描述
关注点(Concern) 应用程序的特定功能
横切关注点(Cross-cutting concern) 贯穿应用程序多个部分的特定功能
切面(Aspect) 包含实现横切关注点的通知和切入点的 AOP 基础单元
通知(Advice) 我们希望在横切关注点中调用的具体逻辑
切入点(Pointcut) 选择应用通知的连接点的表达式
连接点(Join Point) 应用程序的执行点,如方法

3. 执行时间日志记录

本节将创建一个示例切面,用于记录连接点周围的执行时间。

3.1. Maven 依赖

存在多种 Java AOP 框架,如 Spring AOP 和 AspectJ 本教程使用 Spring AOP,需在 pom.xml 中添加以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>3.3.2</version>
</dependency>

对于日志部分,选择 SLF4J 作为 API,SLF4J Simple 作为日志实现。SLF4J 是一个门面,为不同日志实现提供统一 API。因此还需在 pom.xml 中添加:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.13</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>2.0.13</version>
</dependency>

3.2. 执行时间切面

ExecutionTimeAspect 类很简单,仅包含一个通知 logExecutionTime()。**我们使用 @Aspect@Component 注解将其声明为切面,并交由 Spring 管理:

@Aspect
@Component
public class ExecutionTimeAspect {
    private Logger log = LoggerFactory.getLogger(ExecutionTimeAspect.class);

    @Around("execution(* com.baeldung.unittest.ArraySorting.sort(..))")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long t = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        log.info("Execution time=" + (System.currentTimeMillis() - t) + "ms");
        return result;
    }
}

@Around 注解表示通知 logExecutionTime() 将在目标连接点(由切入点表达式 execution(...) 定义)周围运行。 在 Spring AOP 中,连接点始终是方法。

4. 切面的单元测试

从单元测试角度看,我们只测试切面内部的逻辑,不依赖任何外部组件(包括 Spring 应用上下文)。 本示例使用 Mockito 模拟 joinPoint 和日志记录器,并将模拟对象注入测试切面。

测试类使用 @ExtendsWith(MockitoExtension.class) 注解启用 JUnit 5 的 Mockito 功能,它会自动初始化模拟对象并注入到标注了 @InjectMocks 的测试单元中:

@ExtendWith(MockitoExtension.class)
class ExecutionTimeAspectUnitTest {
    @Mock
    private ProceedingJoinPoint joinPoint;

    @Mock
    private Logger logger;

    @InjectMocks
    private ExecutionTimeAspect aspect;

    @Test
    void whenExecuteJoinPoint_thenLoggerInfoIsCalled() throws Throwable {
        when(joinPoint.proceed()).thenReturn(null);
        aspect.logExecutionTime(joinPoint);
        verify(joinPoint, times(1)).proceed();
        verify(logger, times(1)).info(anyString());
    }
}

此测试用例验证: ✅ joinPoint.proceed() 方法在切面中被调用一次 ✅ 日志记录器的 info() 方法被调用一次以记录执行时间

要更精确地验证日志消息,可使用 ArgumentCaptor 捕获日志内容,从而断言消息以 "Execution time=" 开头:

ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
verify(logger, times(1)).info(argumentCaptor.capture());
assertThat(argumentCaptor.getValue()).startsWith("Execution time=");

5. 切面的集成测试

从集成测试角度看,我们需要 ArraySorting 类的实现,以便通过切入点表达式将通知应用到目标类。 sort() 方法简单调用静态方法 Collections.sort() 对列表排序:

@Component
public class ArraySorting {
    public <T extends Comparable<? super T>> void sort(List<T> list) {
        Collections.sort(list);
    }
}

⚠️ 踩坑提醒:为什么不直接将通知应用到 Collections.sort() 静态方法?这是 Spring AOP 的限制——它不支持静态方法拦截。 Spring AOP 通过创建动态代理拦截方法调用,该机制需要调用目标对象上的实际方法,而静态方法无需对象即可调用。如需拦截静态方法,必须改用支持编译时织入的 AOP 框架(如 AspectJ)。

在集成测试中,需要 Spring 应用上下文创建代理对象来拦截目标方法并应用通知。 我们使用 @SpringBootTest 注解加载启用 AOP 和依赖注入的应用上下文:

@SpringBootTest
class ExecutionTimeAspectIntegrationTest {
    @Autowired
    private ArraySorting arraySorting;

    private List<Integer> getRandomNumberList(int size) {
        List<Integer> numberList = new ArrayList<>();
        for (int n=0;n<size;n++) {
            numberList.add((int) Math.round(Math.random() * size));
        }
        return numberList;
    }

    @Test
    void whenSort_thenExecutionTimeIsPrinted() {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream originalSystemOut = System.out;
        System.setOut(new PrintStream(baos));

        arraySorting.sort(getRandomNumberList(10000));

        System.setOut(originalSystemOut);
        String logOutput = baos.toString();
        assertThat(logOutput).contains("Execution time=");
    }
}

测试方法分三步执行:

  1. 重定向输出流:将系统输出重定向到专用缓冲区,供后续断言使用
  2. 调用目标方法:通过 @Autowired 注入的 ArraySorting 实例调用 sort() 方法(❌ 切勿使用 new ArraySorting() 实例化,否则无法激活 Spring AOP)
  3. 验证日志输出:断言缓冲区中包含执行时间日志

6. 总结

本文讨论了 AOP 的核心概念,展示了如何在目标类上使用 Spring AOP 切面,并通过单元测试和集成测试验证切面的正确性。

完整代码示例可在 GitHub 获取。


原始标题:How to Test a Spring AOP Aspect | Baeldung