在 Spring 中使用 AOP 记录执行日志

1、概览

Aspect-Oriented Programming(面向切面编程,简称 AOP)是一种范式,它能让我们在整个应用中隔离事务管理或日志记录等交叉问题,而不会干扰业务逻辑。

本文将带你了解如何在 Spring 中使用 AOP 记录执行日志。

2、不使用 AOP 记录日志

通常,我们会在方法的开始和结束处输出日志。这样,就能跟踪应用的执行流程。此外,还可以捕获传递给特定方法的值及其返回值。

示例如下:

public String greet(String name) {
    logger.debug(">> greet() - {}", name);
    String result = String.format("Hello %s", name);
    logger.debug("<< greet() - {}", result);
    return result;
}

尽管上面的实现看起来是一个标准的解决方案,但日志语句在代码中增加了复杂性。

如果没有日志记录,只需要一行代码就可以完成:

public String greet(String name) {
    return String.format("Hello %s", name);
}

3、面向切面(AOP)编程

顾名思义,面向切面的编程(Aspect-Oriented Programming)侧重于切面,而不是对象和类。我们可以使用 AOP 编程为特定应用实现额外功能,而无需修改其当前实现。

3.1、AOP 概念

在深入学习之前,让我们先从高层次来了解一下 AOP 的基本概念。

  • 切面(Aspect):横切关注点或我们希望在整个应用中应用的功能。
  • 连接点(Join Point):应用流程中我们希望应用切面的点。
  • Advice:在特定连接点应执行的操作。
  • Pointcut:应在其中应用某一切面的连接点集合。

另外,Spring AOP 仅支持方法执行的连接点。可以考虑使用 AspectJ 等编译时库为字段、构造函数、静态初始化器等创建切面。

3.2、Maven 依赖

要使用 Spring AOP,首先在 pom.xml 中添加 spring-boot-starter-aop 依赖:

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

4、使用 AOP 进行日志记录

在 Spring 中实现 AOP 的一种方法是使用带有 @Aspect 注解的 Spring Bean:

@Aspect
@Component
public class LoggingAspect {
}

@Aspect 注解是一个标记注解,Spring 不会自动将其视为组件。为了表明它应该是一个由 Spring 管理并通过组件扫描检测到的 Bean,还要使用 @Component 注解对该类进行注解。

接下来,定义一个切入点(Pointcut)。简单来说,Pointcut 允许我们指定我们希望通过切面拦截的连接点执行位置。

@Pointcut("execution(public * com.baeldung.logging.*.*(..))")
private void publicMethodsFromLoggingPackage() {
}

如上,定义了一个只包含 com.baeldung.logging 包中的 public 方法的 pointcut 表达式。

接下来看看如何定义 Advice,以记录方法执行的开始和结束时间。

4.1、使用 Around Advice

首先从更通用的 Advice 类型 - Around Advice 开始。它允许我们在方法调用前后实施自定义行为。此外,通过该 Advice,我们可以决定是否继续处理特定的 Join Point、返回自定义结果或抛出异常。

使用 @Around 注解来定义 Advice:

@Around(value = "publicMethodsFromLoggingPackage()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    Object[] args = joinPoint.getArgs();
    String methodName = joinPoint.getSignature().getName();
    logger.debug(">> {}() - {}", methodName, Arrays.toString(args));
    Object result = joinPoint.proceed();
    logger.debug("<< {}() - {}", methodName, result);
    return result;
}

value 属性将该 Around Advice 与先前定义的 Pointcut 关联起来。该 Advice 围绕与 publicMethodsFromLoggingPackage() Pointcut 签名相匹配的方法执行。

该方法接受一个 ProceedingJoinPoint 参数。它是 JoinPoint 类的子类,允许我们调用 proceed() 方法执行下一个 Advice(如果存在)或目标方法。

首先调用 joinPoint 上的 getArgs() 方法来获取方法参数数组。此外,还使用 getSignature().getName() 方法获取要拦截的方法的名称。

接下来,调用 proceed() 方法来执行目标方法并获取结果。

最后,调用到前面定义的 greet() 方法:

@Test
void givenName_whenGreet_thenReturnCorrectResult() {
    String result = greetingService.greet("Baeldung");
    assertNotNull(result);
    assertEquals("Hello Baeldung", result);
}

运行测试后,可以在控制台中看到以下结果:

>> greet() - [Baeldung]
<< greet() - Hello Baeldung

5、使用最小侵入性的 Advice

在决定使用哪种类型的 Advice 时,我们建议使用功能最弱的 Advice 来满足我们的需求。如果选择通用 Advice,如 Around Advice,就更容易出现潜在错误和性能问题。

也就是说,让我们来看看如何实现相同的功能,但这次我们将使用前置(Before)和后置(After)Advice。与环绕(Around)Advice 不同,它们不会包装方法的执行,因此无需显式调用 proceed() 方法来继续 Join Point 的执行。具体而言,我们使用这些类型的 Advice 在方法执行之前或之后拦截。

5.1、使用 Before Advice

为了在方法执行前拦截,使用 @Before 注解创建一个 Advice:

@Before(value = "publicMethodsFromLoggingPackage()")
public void logBefore(JoinPoint joinPoint) {
    Object[] args = joinPoint.getArgs();
    String methodName = joinPoint.getSignature().getName();
    logger.debug(">> {}() - {}", methodName, Arrays.toString(args));
}

与上一个例子类似,我们使用 getArgs() 方法获取方法参数,使用 getSignature().getName() 方法获取方法名称。

5.2、使用 AfterReturning Advice

再进一步,为了在方法执行后添加日志,再创建一个 @AfterReturning Advice,该 Advice 将在方法执行完成且未抛出任何异常时运行:

@AfterReturning(value = "publicMethodsFromLoggingPackage()", returning = "result")
public void logAfter(JoinPoint joinPoint, Object result) {
    String methodName = joinPoint.getSignature().getName();
    logger.debug("<< {}() - {}", methodName, result);
}

如上,我们定义了 returning 属性,以获取方法返回的值。此外,在属性中提供的值应与参数名称相匹配。返回值将作为参数传递给 Advice 方法。

5.3、使用 AfterThrowing Advice

如果要记录方法调用完成时出现异常的情况,可以使用 @AfterThrowing Advice:

@AfterThrowing(pointcut = "publicMethodsFromLoggingPackage()", throwing = "exception")
public void logException(JoinPoint joinPoint, Throwable exception) {
    String methodName = joinPoint.getSignature().getName();
    logger.error("<< {}() - {}", methodName, exception.getMessage());
}

这一次,我们将在 Advice 方法中获得抛出的异常,而不是返回值。

6、关于使用 Spring AOP 的建议

最后,来了解一下在使用 Spring AOP 时应注意的一些问题。

Spring AOP 是一个基于代理的框架。它创建代理对象来拦截方法调用并应用 Advice 中定义的逻辑。这会对我们应用的性能产生负面影响。

为了减少 AOP 对性能的影响,应该考虑只在必要时使用 AOP。应避免为孤立和不频繁的操作创建切面。

最后,如果仅为开发目的而使用 AOP,我们可以有条件地创建它,例如,只有在特定 Spring Profile 处于活动状态时才创建。

7、总结

本文介绍了如何在 Spring 应用中通过 Spring AOP 使用 Around Advice 以及 BeforeAfter Advice 来记录应用的执行日志。


Ref:https://www.baeldung.com/spring-aspect-oriented-programming-logging