通过示例学习面向切面编程
什么是方面?
@Cacheable:一份标准的春季建议
使用自定义切面记录 REST 调用
使用 AOP 进行性能监控
基于AOP的重试机制
结论
本文通过具体示例,为你提供了一种学习面向切面编程的绝佳途径。我将重点展示SpringBoot AOP的四个切面实现。
目录:
- 什么是方面?
@Cacheable:一份标准的春季建议- 记录 REST 调用(使用自定义切面)
- 性能监控(采用AOP)
- 重试机制(采用AOP)
如果你想跳过冗长的描述,直接查看具体的代码,那么我已经为你准备好了:
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);
}
}
接下来,我们在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);
}
}
我们的实现是递归的,因此速度比较慢。那么如何才能让 Web 服务更快呢?一种方法是使用更快的算法,但我们可以使用 Spring 的@Cacheable特性来解决这个问题。这个注解会在后台创建一个缓存,用于存储所有之前的计算结果。我们只需要将这个@Cacheable注解添加到我们的方法中即可:
@Cacheable("Fibonacci")
@GetMapping(path = "/api/fibonacci/{number}")
public Long fibonacci(@PathVariable(value = "number") Long number) { ... }
现在我们准备通过向服务器发送 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);
}
第一行定义了切入点表达式,后面的方法表示通知。让我们逐一分析这两者:
切入点:
切入点表达式定义了我们的通知插入的位置。在我们的例子中,切面会应用到每个带有注解的方法之前@LogMehtodName。请注意,这@LogMethodName是我们自定义的注解,它用作切入点标记。
通知:
通知方法是一段逻辑,它将许多不同对象通用的任务进行概括。在本例中,通知会找到原始方法的名称及其调用参数,并将它们记录到控制台。
有了我们的 Aspect,还需要三行额外的代码才能使所有功能正常运行:
- 首先,将标记添加
@LogMethodName到我们的fibonacci()方法中 - 其次,我们需要
@Aspect向包含我们的切面的类中添加内容。 @EnableAspectJAutoProxy第三,在任何@Configuration类中启用 Spring 的切面扫描
好了,我们已经实现了自己的建议功能!🙌 让我们运行一个测试!我们向 Web 服务发送一个 REST 请求来计算第 40 个斐波那契数,然后查看控制台输出:
Method [fibonacci] gets called with parameters [40]
毋庸置疑,如果您需要追踪应用程序中的错误,这些日志信息将非常有帮助。
使用 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;
}
切入点:
和以前一样,我们创建一个新的自定义注释@MonitorTime来标记我们的切入点。
建议:
切面应该有一个类型为ProceedingJoinPoint@Around的参数。该类型有一个方法,可以触发实际目标方法的执行。因此,在我们的建议中,我们首先查询当前时间(以毫秒为单位)。目标方法执行完毕后,我们再次测量当前时间,并由此计算时间差。proceed()
接下来,我们给目标方法添加@MonitorTime注解:
@MonitorTime
@LogMethodName
@Cacheable("Fibonacci")
@GetMapping(path = "/api/fibonacci/{number}")
public Long fibonacci(@PathVariable(value = "number") Long number) { ... }
现在,我们的 REST 方法已经附加了不少切入点标记😉 好了,接下来我们来测试一下性能监控功能。和之前一样,我们计算斐波那契数列的第 40 个数:
Method [fibonacci] gets called with parameters [40]
Execution took [1902ms]
如您所见,这次 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;
}
切入点:
我们的建议会绕过任何带有自定义注解的方法@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");
}
}
多亏了我们的@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
在上述案例中,操作失败了 2 次,直到第三次才成功。
结论
恭喜,你现在是一名专业的AOP程序员了🥳🚀 你可以在我的Github仓库中找到一个包含所有切面的完整Web服务:
pmgysel / aop-examples
SpringBoot 中的面向切面编程 (AOP) 的一些示例
非常感谢您的阅读,如果您有任何问题或反馈,请留言!
文章来源:https://dev.to/pmgysel/learn-aspect-oriented-programming-by-example-m8o