前面的一系列文章介绍了AOP的方方面面:

  1. AOP的由来以及快速上手
  2. AOP的两种实现-Spring AOP以及AspectJ
  3. Spring AOP中提供的种种Aspects - Tracing相关
  4. Spring AOP中提供的种种Aspects - 异步执行
  5. Spring AOP中提供的种种Aspects - 并发控制

从本篇文章开始,会介绍一些基于AOP原理的自定义Aspect实现,用来解决在开发过程中可能遇到的各种常见问题。

方法的重试 - Retry

问题分析

在开发爬虫类应用的时候,经常需要处理的问题就是一次爬取过程失败了应该如何处理。其实爬取失败的比率在网络条件比较不稳定的情况下还是相当高的。解决办法一般都会考虑重新尝试这一最最基本和简单的方案。因此,在相关代码中就会出现很多这种结构:

  1. /**
  2. * 带有失败重试功能的业务代码。
  3. *
  4. * @param in
  5. * @return
  6. * @throws Exception
  7. */
  8. public OUT consume(IN in) throws Exception {
  9. while (shouldRetry()) {
  10. try {
  11. OUT output = request(in);
  12. if (isOutputOK(output)) {
  13. return output;
  14. } else {
  15. continue;
  16. }
  17. } catch (Exception e) {
  18. handleException(e);
  19. }
  20. }
  21. beforeExceptionalReturn();
  22. return null;
  23. }

上述代码表达的是一个网络请求相关的通用处理结构。可以发现其中主要包含一个控制结构以及若干扩展点:

控制结构:

  • while循环 - 用来控制失败重试,这里是一个控制结构

扩展点:

  • shouldRetry - 用来控制是否需要下次重试
  • request - 关键的业务方法,根据输入得到输出,比如给定一个URL,得到对应的HTML文档
  • handleException - 发生异常的时候进行处理
  • beforeExceptionalReturn - 无法获取结果并且不再进行重试时需要调用的方法

因此,从业务的角度来看,真正关心的也许只是request这一个方法。当然,为了应用的健壮性和灵活性,上面的扩展点都可以根据需要进行扩展,但是大多数情况下采用默认实现也绝对是够用的。

如何Aspect化

想要开发一个Aspect,从它本身的定义来看,首先需要考虑的就是如何定义Advice以及Pointcut。

我们可以将上述扩展点中的request方法作为目标方法,单独定义一个Component用于Advice的定义,然后采用一个基于注解的方式来定义Pointcut,在注解会提供各种属性来帮助开发人员方便地定义各种扩展点。因此,大概的思路就是这样的:

  • 注解的定义 - 用来限定Pointcut的范围,以及一些扩展点的定义
  • Advice的定义 - 具体而言就是一个@Aspect Bean,其中定义了控制结构和各种扩展点
  • Pointcut的定义 - 结合自定义的注解,在目标方法上使用该注解完成Pointcut的定义

注解的定义

根据需要完成的功能的语义,就把这个注解称为@Retry吧,它的实现如下所示:

  1. @Target({ElementType.METHOD})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface Retry {
  5. int maxRetryCount() default 5;
  6. String shouldRetry() default "";
  7. String isOutputOK() default "";
  8. String handleException() default "";
  9. String beforeExceptionalReturn() default "";
  10. }

注解中定义了几个关键的信息:

  • maxRetryCount:最多重试的次数(包括第一次调用),默认值为5。即会在初次调用失败后最多重试4次
  • shouldRetry:判断是否需要重试的扩展点,传入的是一个方法名
  • isOutputOK:判断返回结果是否合法的扩展点,传入的是一个方法名
  • handleException:在发生异常后进行处理的扩展点,传入的是一个方法名
  • beforeExceptionalReturn:在所有重试都失败后进行处理的扩展点,传入的是一个方法名

目标业务方法的定义

为了测试整个@Retry以及相关的Aspect是否满足需求,下面也定义了一个简单的方法作为目标业务方法(RetryService.doRetryBusiness):

  1. @Service
  2. public class RetryService {
  3. private AtomicInteger retryCount = new AtomicInteger(0);
  4. @Retry(maxRetryCount = 2, beforeExceptionalReturn = "extendedBeforeExceptionalReturn")
  5. public String doRetryBusiness() {
  6. if (retryCount.getAndIncrement() < 4) {
  7. throw new RuntimeException(Thread.currentThread().getName() + ": 结果获取失败");
  8. }
  9. return Thread.currentThread().getName() + ": 这是最终结果";
  10. }
  11. public void extendedBeforeExceptionalReturn() {
  12. System.out.println(Thread.currentThread().getName() + ": 自定义的处理失败后扩展点");
  13. }
  14. }

这个业务方法使用了上一步定义的@Retry注解进行修饰。它将最大重试的次数改成了2,也就是说最多只允许重试一次。另外还定义了beforeExceptionalReturn扩展点的实现方法的名称。这个方法对应的就是下方的:

  1. public void extendedBeforeExceptionalReturn() {
  2. System.out.println("自定义的处理失败后扩展点");
  3. }

因此我们期望的结果是当超过了调用业务方法的最大重试次数后,在返回空结果前会执行我们自定义的方法。由于注解中只包含了方法名称这一字符串类型的信息,毫无疑问在具体的Advice中会通过反射的方法来找到方法对象并调用之。

Aspect Bean的定义

很显然,并不是每次使用@Retry注解的时候都需要提供所有的扩展点实现。如果不提供的话则应该使用默认的实现。这些默认实现可以集中管理:

  1. public abstract class RetrySupport {
  2. protected boolean shouldRetry() {
  3. return true;
  4. }
  5. protected boolean isOutputOK(Object output) {
  6. return Objects.nonNull(output);
  7. }
  8. protected void handleException(Exception e) {
  9. System.out.println(e.getMessage());
  10. }
  11. protected void beforeExceptionalReturn() {
  12. System.out.println("默认的处理失败后扩展点");
  13. }
  14. }

这个类定义了所有的默认方法。当没有提供自定义的扩展方法的时候就会调用它们。

紧接着,就是Aspect本身了的定义了:

  1. @Component
  2. @Aspect
  3. public class RetryAspect extends RetrySupport {
  4. private static ThreadLocal<Integer> retryCounters;
  5. static {
  6. retryCounters = ThreadLocal.withInitial(() -> {
  7. return 0;
  8. });
  9. }
  10. @Around("com.rxjiang.aop.custom.Pointcuts.retryPointcuts()")
  11. public Object retryAdvice(ProceedingJoinPoint pjp) throws Throwable {
  12. System.out.println(Thread.currentThread().getName() + ": 进入Advice");
  13. // 获取被调用的对象以及Retry注解对象
  14. MethodSignature signature = (MethodSignature) pjp.getSignature();
  15. String methodName = signature.getMethod().getName();
  16. Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
  17. Object calledObject = pjp.getTarget();
  18. Retry retryAnno =
  19. pjp.getTarget().getClass().getMethod(methodName, parameterTypes).getAnnotation(Retry.class);
  20. try {
  21. while (aspectShouldRetry(calledObject, retryAnno)) {
  22. try {
  23. Object result = pjp.proceed(pjp.getArgs());
  24. if (isOutputOK(result)) {
  25. return result;
  26. } else {
  27. continue;
  28. }
  29. } catch (Exception e) {
  30. System.out.println(Thread.currentThread().getName() + ": 捕获到了异常: " + e.getMessage());
  31. handleException(e);
  32. }
  33. }
  34. } finally {
  35. retryCounters.set(0);
  36. }
  37. aspectBeforeExceptionalReturn(calledObject, retryAnno);
  38. return null;
  39. }
  40. // 拓展点:失败返回前的处理
  41. private void aspectBeforeExceptionalReturn(Object calledObject, Retry retryAnno)
  42. throws Throwable {
  43. String beforeExceptionalReturnMethodName = retryAnno.beforeExceptionalReturn();
  44. if (StringUtils.isEmpty(beforeExceptionalReturnMethodName)) {
  45. super.beforeExceptionalReturn();
  46. } else {
  47. Method beforeExceptionalReturnMethod =
  48. calledObject.getClass().getMethod(beforeExceptionalReturnMethodName);
  49. if (beforeExceptionalReturnMethod == null) {
  50. super.beforeExceptionalReturn();
  51. } else {
  52. beforeExceptionalReturnMethod.invoke(calledObject, new Object[] {});
  53. }
  54. }
  55. }
  56. // 拓展点: 是否进行重试
  57. private boolean aspectShouldRetry(Object calledObject, Retry retryAnno) throws Throwable {
  58. Integer currentCount = retryCounters.get();
  59. retryCounters.set(currentCount + 1);
  60. if (++currentCount > retryAnno.maxRetryCount()) {
  61. return false;
  62. }
  63. boolean shouldRetry = false;
  64. String shouldRetryMethodName = retryAnno.shouldRetry();
  65. if (StringUtils.isEmpty(shouldRetryMethodName)) {
  66. shouldRetry = super.shouldRetry();
  67. } else {
  68. Method shouldRetryMethod = calledObject.getClass().getMethod(shouldRetryMethodName);
  69. if (shouldRetryMethod == null) {
  70. System.out.println("Method does not exist, fallback to default one.");
  71. shouldRetry = super.shouldRetry();
  72. } else {
  73. shouldRetry = (boolean) shouldRetryMethod.invoke(calledObject, new Object[] {});
  74. }
  75. }
  76. return shouldRetry;
  77. }
  78. }

这个类比较长,但是逻辑还算清晰,主要分为以下几个部分:

  • ThreadLocal的定义,尽管这个Aspect被定义成一个单例对象,但是为了让它能够在多线程环境中正常工作,使用了一个ThreadLocal作为重试计数器
  • Around Advice的实现,该实现可以分为两个部分:
    • 通过反射获取被调用对象本身以及注解对象
    • 定义整体运行结构,即前文中提到的while循环部分
  • 默认方法和可能存在的扩展方法的选择

在主体结构中的while循环里面,会根据注解对象的信息来决定是调用自定义的扩展方法还是默认方法,以是否进行重试这个扩展点作为例子:

  1. // 拓展点: 是否进行重试
  2. private boolean aspectShouldRetry(Object calledObject, Retry retryAnno) throws Throwable {
  3. Integer currentCount = retryCounters.get();
  4. retryCounters.set(currentCount + 1);
  5. if (++currentCount > retryAnno.maxRetryCount()) {
  6. return false;
  7. }
  8. boolean shouldRetry = false;
  9. String shouldRetryMethodName = retryAnno.shouldRetry();
  10. if (StringUtils.isEmpty(shouldRetryMethodName)) {
  11. shouldRetry = super.shouldRetry();
  12. } else {
  13. Method shouldRetryMethod = calledObject.getClass().getMethod(shouldRetryMethodName);
  14. if (shouldRetryMethod == null) {
  15. System.out.println("Method does not exist, fallback to default one.");
  16. shouldRetry = super.shouldRetry();
  17. } else {
  18. shouldRetry = (boolean) shouldRetryMethod.invoke(calledObject, new Object[] {});
  19. }
  20. }
  21. return shouldRetry;
  22. }

如果当前重试的计数已经超过了最大重试次数,那么直接返回false用来终止执行。否则会继续执行查看是否自定义了重试方法名称。如果定义且方法对象却是存在,那么会调用自定义的扩展方法;否则调用默认方法,有两种情况会调用默认的方法:

  • 注解对象中没有定义相应的属性,这里是shouldRetry字符串
  • 注解对象中定义了自定义方法名称,但是通过反射没法获取到相应的方法对象(字符串拼写错误等原因)

配置以及测试方法

整体配置:

首先是Spring整体的配置,比如开启对于AOP的支持,启动包扫描功能:

  1. @Configuration
  2. @EnableAspectJAutoProxy
  3. @ComponentScan(basePackages = "com.rxjiang")
  4. public class CustomAopConfiguration {
  5. }

Pointcut的定义:

  1. public class Pointcuts {
  2. @Pointcut("execution(@com.rxjiang.aop.custom.Retry * *(..))")
  3. public void retryPointcuts() {}
  4. }

测试方法:

串行部分的测试:

  1. @RunWith(SpringJUnit4ClassRunner.class)
  2. @ContextConfiguration(classes = {CustomAopConfiguration.class})
  3. public class RetryTest {
  4. @Autowired
  5. private RetryService retryService;
  6. @Test
  7. public void testRetryMethod1() {
  8. System.out.println(retryService.doRetryBusiness());
  9. }
  10. @Test
  11. public void testRetryMethod2() {
  12. System.out.println(retryService.doRetryBusiness());
  13. }
  14. @Test
  15. public void testRetryMethod3() {
  16. System.out.println(retryService.doRetryBusiness());
  17. }
  18. }

以上是串行部分的测试。最终的输出大概是这样的:

  1. main: 进入Advice
  2. main: 捕获到了异常: main: 结果获取失败
  3. main: 结果获取失败
  4. main: 捕获到了异常: main: 结果获取失败
  5. main: 结果获取失败
  6. main: 自定义的处理失败后扩展点
  7. null
  8. main: 进入Advice
  9. main: 捕获到了异常: main: 结果获取失败
  10. main: 结果获取失败
  11. main: 捕获到了异常: main: 结果获取失败
  12. main: 结果获取失败
  13. main: 自定义的处理失败后扩展点
  14. null
  15. main: 进入Advice
  16. main: 这是最终结果

以上总共会打印出4条获取失败的信息,因为在业务方法中定义了前四次调用都会返回失败。每个测试方法最多会重试2次(加上初次调用),因此测试方法testRetryMethod1和测试方法testRetryMethod2最后的结果都是null。此时已经一共尝试了4次,因此当testRetryMethod3方法执行的时候会成功得到结果。

并行部分的测试:

  1. @Test
  2. public void testConcurrentRetry() throws InterruptedException {
  3. IntStream.range(0, 5).forEach(i -> {
  4. new Thread(() -> {
  5. try {
  6. System.out.println(Thread.currentThread().getName() + ": 启动了");
  7. System.out.println(retryService.doRetryBusiness());
  8. } catch (Exception e) {
  9. e.printStackTrace();
  10. }
  11. }).start();
  12. });
  13. Thread.sleep(10000);
  14. }

这里启动了5个线程同时去访问业务方法,同样地前4次会故意设置成失败,因此最多只会打印出来4条失败信息:

  1. Thread-2: 启动了
  2. Thread-4: 启动了
  3. Thread-3: 启动了
  4. Thread-5: 启动了
  5. Thread-6: 启动了
  6. Thread-3: 进入Advice
  7. Thread-4: 进入Advice
  8. Thread-5: 进入Advice
  9. Thread-6: 进入Advice
  10. Thread-2: 进入Advice
  11. Thread-4: 捕获到了异常: Thread-4: 结果获取失败
  12. Thread-5: 捕获到了异常: Thread-5: 结果获取失败
  13. Thread-5: 结果获取失败
  14. Thread-2: 捕获到了异常: Thread-2: 结果获取失败
  15. Thread-2: 结果获取失败
  16. Thread-6: 捕获到了异常: Thread-6: 结果获取失败
  17. Thread-6: 结果获取失败
  18. Thread-6: 这是最终结果
  19. Thread-3: 这是最终结果
  20. Thread-2: 这是最终结果
  21. Thread-5: 这是最终结果
  22. Thread-4: 结果获取失败
  23. Thread-4: 这是最终结果

而且值得注意的是每个线程都成功获取到了最终结果,这一行为和串行的方式有所差异。从打印的信息来看的话,线程2,4,5,6分别失败了一次,而每个线程最多是可以重试两次的,因此每个线程都获取了结果。

更多的扩展点

除了上述代码介绍的扩展点之外,其实还有很多地方可以扩展,比如:

  • 指定可重试的异常种类:这一点很好理解,并不是每种异常都可以通过重试的方案去解决的,对于网络相关的异常通常是可以恢复的,因此我们可以在注解中声明可重试的异常类型,只有抛出的异常种类相符的时候才会去重试
  • 指定不可重试的异常种类:和上面的情况正好相反,当发生了指定的不可重试的异常时,直接放弃重试
  • 调用fallback方法:当多次重试无效后,可以指定一个fallback(或者默认)方法,通过调用该fallback方法得到最终的结果

总结

本文介绍了一种基于AOP的重试机制的实现方法。在失败率比较高,但是可通过重试来解决的业务场景中可以考虑使用它来简化代码。这样做能够将和业务无关的代码剥离出去,尽可能地做到单一职责,让代码更加优雅。

这也是AOP的初衷,让各种模板代码从业务中独立出去,实现模板代码和业务代码的独立维护。

[AOP] 6. 一些自定义的Aspect - 方法的重试(Retry)的更多相关文章

  1. [AOP] 7. 一些自定义的Aspect - Circuit Breaker

    Circuit Breaker(断路器)模式 关于断路器模式是在微服务架构/远程调用环境下经常被使用到的一个模式.它的作用一言以蔽之就是提高系统的可用性,在出现的问题通过服务降级的手段来保证系统的整体 ...

  2. Springboot学习06-Spring AOP封装接口自定义校验

    Springboot学习06-Spring AOP封装接口自定义校验 关键字 BindingResult.Spring AOP.自定义注解.自定义异常处理.ConstraintValidator 前言 ...

  3. spring aop无法拦截类内部的方法调用

    1.概念 拦截器的实现原理就是动态代理,实现AOP机制.Spring 的代理实现有两种:一是基于 JDK Dynamic Proxy 技术而实现的:二是基于 CGLIB 技术而实现的.如果目标对象实现 ...

  4. Spring AOP开发时如何得到某个方法内调用的方法的代理对象?

    Spring AOP开发时如何得到某个方法内调用的方法的代理对象? 问题阅读起来拗口,看代码 在方法中调用其他方法很常见,也经常使用,如果在一个方法内部调用其他方法,比如 public class U ...

  5. jQuery Validate 表单验证插件----自定义一个验证方法

    一.下载依赖包 网盘下载:https://yunpan.cn/cryvgGGAQ3DSW  访问密码 f224 二.引入依赖包 <script src="../../scripts/j ...

  6. Asp.net MVC4.0自定义Html辅助方法

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.W ...

  7. SharePoint2010 自定义代码登录方法

    转:http://yysyb123.blog.163.com/blog/static/192050472011382421717/ SharePoint2010 自定义代码登录方法 (自定义Form验 ...

  8. Swift中自定义Log打印方法

    系统如何调用super方法 系统默认只会在构造函数中,自动调用super.init()方法,而且是在所写方法的尾部进行调用. 在其他函数中,如何需要调用父类的默认实现,都需要手动去实现. 如果在构造函 ...

  9. 让LINQ中的查询语法使用自定义的查询方法

    使用LINQ时有两种查询语法:查询语法和方法语法 查询语法:一种类似 SQL 语法的查询方式 方法语法:通过扩展方法和Lambda表达式来创建查询 例如: List<, , , }; //查询语 ...

随机推荐

  1. 11 Spring框架 SpringDAO的JdbcTemplate

    上几个章节我们探讨了Spring的IoC和AOP,这是Spring的重点,但是Spring对jdbc的支持同样我们也不能忘记,毕竟我们还要通过Spring来管理DAO框架(例如Hibernate或者M ...

  2. 优秀 H5 案例收集 Vol.2(不定期更新)

    上期浏览:Vol.1 再见了,影史最性感的硬汉http://news.163.com/special/fdh5_wolverine/ 活出真我http://balfhcy.pernod-ricard- ...

  3. 利用ST MCU内部的基准参考电压监测电源电压及其它

    源: 利用ST MCU内部的基准参考电压监测电源电压及其它

  4. Mysql:实现分组查询拼接未分组同一字段字符group_concat()

    Mysql:实现分组查询拼接未分组同一字段字符group_concat() MySQL中,如果想实现将分组之后的多个数据合并到一列,可以使用group_concat函数,如下图所示: 在oralce中 ...

  5. 20145235李涛《网络对抗》Exp7 网络欺诈技术防范

    基础问题回答 通常在什么场景下容易受到DNS spoof攻击? 使用未知的公共wifi或者在不安全的局域网下容易受到DNS spoof攻击. 在日常生活工作中如何防范以上两攻击方法? 首先要提高防范意 ...

  6. 前端人脸识别框架Tracking.js与JqueryFaceDetection

    这篇文章主要就介绍两种前端的人脸识别框架(Tracking.js和JqueryFaceDetection) 技术特点 Tracking.js是使用js封装的一个框架,使用起来需要自己配置许多的东西,略 ...

  7. PHP学习之路(一)

    先前对PHP的学习研究,今总结写下心得!!

  8. 《Computational Statistics with Matlab》硬译2

    T=; sigma=; thetamin=-;thetamax=; theta=zeros(,T); seed=;rand('state',seed);randn('state',seed); the ...

  9. 使用Mybatis时报错Invalid bound statement (not found):

    使用逆向工程时生成的.xml文件在conf目录下,而使用查询方法时,无法在dao包下找到xml文件,所以报错. 测试代码如下所示: @Test public void testSimple() thr ...

  10. mybatis报错 Error instantiating interface com.atguigu.mybatis.dao.DepartmentMapper with invalid types () or values ()

    mybatis报错 Error instantiating interface com.atguigu.mybatis.dao.DepartmentMapper with invalid types ...