发布于 2026-01-06 0 阅读
0

通过示例学习面向切面编程:什么是切面?@Cacheable:标准的 Spring 建议日志记录;使用自定义切面记录 REST 调用;使用 AOP 进行性能监控;使用 AOP 进行重试;结论

通过示例学习面向切面编程

什么是方面?

@Cacheable:一份标准的春季建议

使用自定义切面记录 REST 调用

使用 AOP 进行性能监控

基于AOP的重试机制

结论

本文通过具体示例,为你提供了一种学习面向切面编程的绝佳途径。我将重点展示SpringBoot AOP的四个切面实现

目录

如果你想跳过冗长的描述,直接查看具体的代码,那么我已经为你准备好了:

GitHub 标志 pmgysel / aop-examples

SpringBoot 中的面向切面编程 (AOP) 的一些示例

什么是方面?

网上有很多关于 Spring AOP 的优秀概述资源,包括Baeldung 的这篇文章Spring AOP 的官方文档。但由于我们不想专注于枯燥的理论,而是更注重实践,所以这里简单总结一下 AOP 的工作原理:

面向切面编程详解

本教程将用到以下术语:

  • 建议:实现某些常见任务(例如日志记录或缓存)的方法
  • 切入点:一种模式表达式,用于匹配应该调用 Advice 的位置。
  • 方面:建议加上切入点表达式
  • 附加题 - 连接点:代码中所有可能成为切入点的位置。

@Cacheable:一份标准的春季建议

让我们从简单的入手,考虑一下Spring已经实现的一种通知机制,即@Cacheable 注解。假设你的 Web 服务需要计算斐波那契数列中的数字。

如果你不知道斐波那契数列是什么:它是一个从 0 和 1 开始的数列,每个连续的数字都是前两个数字之和。

我们在@Service类中实现了斐波那契数列的计算



@Service
public class FibonacciService {
  public Long nthFibonacciTerm(Long n) {
    if (n == 1 || n == 0) {
      return n;
    }
    return nthFibonacciTerm(n-1) + nthFibonacciTerm(n-2);
  }
}


Enter fullscreen mode Exit fullscreen mode

接下来,我们在REST控制器中使用此服务类



@RestController
public class WebController {
  @Autowired private final FibonacciService fibonacciService;

  @GetMapping(path = "/api/fibonacci/{number}")
  public Long fibonacci(@PathVariable(value = "number") Long number) {
    return fibonacciService.nthFibonacciTerm(number);
  }
}


Enter fullscreen mode Exit fullscreen mode

我们的实现是递归的,因此速度比较慢。那么如何才能让 Web 服务更快呢?一种方法是使用更快的算法,但我们可以使用 Spring 的@Cacheable特性来解决这个问题。这个注解会在后台创建一个缓存,用于存储所有之前的计算结果。我们只需要将这个@Cacheable注解添加到我们的方法中即可:



@Cacheable("Fibonacci")
@GetMapping(path = "/api/fibonacci/{number}")
public Long fibonacci(@PathVariable(value = "number") Long number) { ... }


Enter fullscreen mode Exit fullscreen mode

现在我们准备通过向服务器发送 REST 请求来测试缓存机制http://localhost:8080/api/fibonacci/40。我尝试在自己的笔记本电脑上计算斐波那契数列第 40 个数,以下是结果:

  • 首次 REST 调用:1902 毫秒
  • 第二次 REST 调用:1 毫秒

结果还不错哦🤙😎

最后我想提一点:要激活 Spring 的缓存功能,您必须将其添加@EnableCaching@Configuration类中。

使用自定义切面记录 REST 调用

这很简单,对吧?那么让我们来看一个更高级的用例:现在我们创建一个自定义切面!

我们的目标是在每次调用 REST 方法时都生成一条日志消息。由于我们可能还想将此功能添加到未来的 REST 方法中,因此我们希望将此任务概括为一个切面



@Before("@annotation(com.example.aop.LogMethodName)")
public void logMethodName(JoinPoint joinPoint) {
  String method = joinPoint.getSignature().getName();
  String params = Arrays.toString(joinPoint.getArgs());
  System.out.println("Method [" + method + "] gets called with parameters " + params);
}


Enter fullscreen mode Exit fullscreen mode

第一行定义了切入点表达式,后面的方法表示通知。让我们逐一分析这两者:

切入点
切入点表达式定义了我们的通知插入的位置。在我们的例子中,切面会应用到每个带有注解的方法之前@LogMehtodName。请注意,这@LogMethodName是我们自定义的注解,它用作切入点标记。

通知
通知方法是一段逻辑,它将许多不同对象通用的任务进行概括。在本例中,通知会找到原始方法的名称及其调用参数,并将它们记录到控制台。

有了我们的 Aspect,还需要三行额外的代码才能使所有功能正常运行:

  • 首先,将标记添加@LogMethodName到我们的fibonacci()方法中
  • 其次,我们需要@Aspect向包含我们的切面的类中添加内容。
  • @EnableAspectJAutoProxy第三,在任何@Configuration类中启用 Spring 的切面扫描

好了,我们已经实现了自己的建议功能!🙌 让我们运行一个测试!我们向 Web 服务发送一个 REST 请求来计算第 40 个斐波那契数,然后查看控制台输出:



Method [fibonacci] gets called with parameters [40]


Enter fullscreen mode Exit fullscreen mode

毋庸置疑,如果您需要追踪应用程序中的错误,这些日志信息将非常有帮助。

使用 AOP 进行性能监控

在前面的例子中,我们使用了`@Before`类型的切入点表达式——在这种情况下,通知会在实际方法执行之前运行。现在让我们换个思路,实现一个`@Around`切入点。这种通知会在目标方法执行之前运行一部分,执行之后运行一部分。

我们现在的目标是监控所有 REST 调用的执行时间。接下来,我们将以一种通用的方式,即使用切面(Aspect)来实现这一监控需求:



@Around("@annotation(com.example.aop.MonitorTime)")
public Object monitorTime(ProceedingJoinPoint joinPoint) throws Throwable {
  long startTime = System.currentTimeMillis();
  Object proceed = joinPoint.proceed();
  long duration = System.currentTimeMillis() - startTime;
  System.out.println("Execution took [" + duration + "ms]");
  return proceed;
}


Enter fullscreen mode Exit fullscreen mode

切入点
和以前一样,我们创建一个新的自定义注释@MonitorTime来标记我们的切入点。

建议
切面应该有一个类型为ProceedingJoinPoint@Around的参数。该类型有一个方法,可以触发实际目标方法的执行。因此,在我们的建议中,我们首先查询当前时间(以毫秒为单位)。目标方法执行完毕后,我们再次测量当前时间,并由此计算时间差。proceed()

接下来,我们给目标方法添加@MonitorTime注解:



@MonitorTime
@LogMethodName
@Cacheable("Fibonacci")
@GetMapping(path = "/api/fibonacci/{number}")
public Long fibonacci(@PathVariable(value = "number") Long number) { ... }


Enter fullscreen mode Exit fullscreen mode

现在,我们的 REST 方法已经附加了不少切入点标记😉 好了,接下来我们来测试一下性能监控功能。和之前一样,我们计算斐波那契数列的第 40 个数:



Method [fibonacci] gets called with parameters [40]
Execution took [1902ms]


Enter fullscreen mode Exit fullscreen mode

如您所见,这次 REST 调用耗时 1902 毫秒。有了这方面的@Around经验,您绝对是一位高级 AOP 程序员!💪

基于AOP的重试机制

分布式系统可能会遇到并发问题。例如,当两个 Web 服务实例同时尝试访问数据库中的同一条记录时,就会出现这种情况。通常,这种锁问题可以通过重试操作来解决。这里唯一的要求是操作必须是幂等的。

接下来,我们创建一个切面,它会透明地重试操作直到成功为止



@Around("@annotation(com.example.aop.RetryOperation)")
public Object doIdempotentOperation(ProceedingJoinPoint joinPoint) throws Throwable {
  int numAttempts = 0;
  RuntimeException exception;
  do {
    try {
      return joinPoint.proceed();
    } catch(RuntimeException e) {
      numAttempts++;
      exception = e;
    }
  } while(numAttempts < 100);
  throw exception;
}


Enter fullscreen mode Exit fullscreen mode

切入点
我们的建议会绕过任何带有自定义注解的方法@RetryOperation

建议
在该try语句中,我们运行目标方法。该方法可能会抛出异常RuntimeException。如果发生这种情况,我们递增numAttempts计数器并重新运行目标方法。一旦目标方法成功执行,我们就退出建议。

为了演示,我们创建一个用于存储字符串的 REST 方法。此方法有 50% 的概率会失败:



@RetryOperation
@LogMethodName
@PostMapping(path = "/api/storeData")
public void storeData(@RequestParam(value = "data") String data) {
  if (new Random().nextBoolean()) {
    throw new RuntimeException();
  } else {
    System.out.println("Pretend everything went fine");
  }
}


Enter fullscreen mode Exit fullscreen mode

多亏了我们的@RetryOperation注解,上述方法会不断重试直到成功。此外,我们使用@LogMethodName注解还可以查看每一次方法调用。接下来,让我们测试一下新的 REST 端点;为此,我们向其发送一个 REST 请求localhost:8080/api/storeData?data=hello-world



Method [storeData] gets called with parameters [hello-world]
Method [storeData] gets called with parameters [hello-world]
Method [storeData] gets called with parameters [hello-world]
Pretend everything went fine


Enter fullscreen mode Exit fullscreen mode

在上述案例中,操作失败了 2 次,直到第三次才成功。

结论

恭喜,你现在是一名专业的AOP程序员了🥳🚀 你可以在我的Github仓库中找到一个包含所有切面的完整Web服务:

GitHub 标志 pmgysel / aop-examples

SpringBoot 中的面向切面编程 (AOP) 的一些示例

非常感谢您的阅读,如果您有任何问题或反馈,请留言!

文章来源:https://dev.to/pmgysel/learn-aspect-oriented-programming-by-example-m8o