前面的一系列文章介绍了AOP的方方面面:
- AOP的由来以及快速上手
- AOP的两种实现-Spring AOP以及AspectJ
- Spring AOP中提供的种种Aspects - Tracing相关
- Spring AOP中提供的种种Aspects - 异步执行
- Spring AOP中提供的种种Aspects - 并发控制
从本篇文章开始,会介绍一些基于AOP原理的自定义Aspect实现,用来解决在开发过程中可能遇到的各种常见问题。
方法的重试 - Retry
问题分析
在开发爬虫类应用的时候,经常需要处理的问题就是一次爬取过程失败了应该如何处理。其实爬取失败的比率在网络条件比较不稳定的情况下还是相当高的。解决办法一般都会考虑重新尝试这一最最基本和简单的方案。因此,在相关代码中就会出现很多这种结构:
/**
* 带有失败重试功能的业务代码。
*
* @param in
* @return
* @throws Exception
*/
public OUT consume(IN in) throws Exception {
while (shouldRetry()) {
try {
OUT output = request(in);
if (isOutputOK(output)) {
return output;
} else {
continue;
}
} catch (Exception e) {
handleException(e);
}
}
beforeExceptionalReturn();
return null;
}
上述代码表达的是一个网络请求相关的通用处理结构。可以发现其中主要包含一个控制结构以及若干扩展点:
控制结构:
- while循环 - 用来控制失败重试,这里是一个控制结构
扩展点:
- shouldRetry - 用来控制是否需要下次重试
- request - 关键的业务方法,根据输入得到输出,比如给定一个URL,得到对应的HTML文档
- handleException - 发生异常的时候进行处理
- beforeExceptionalReturn - 无法获取结果并且不再进行重试时需要调用的方法
因此,从业务的角度来看,真正关心的也许只是request这一个方法。当然,为了应用的健壮性和灵活性,上面的扩展点都可以根据需要进行扩展,但是大多数情况下采用默认实现也绝对是够用的。
如何Aspect化
想要开发一个Aspect,从它本身的定义来看,首先需要考虑的就是如何定义Advice以及Pointcut。
我们可以将上述扩展点中的request方法作为目标方法,单独定义一个Component用于Advice的定义,然后采用一个基于注解的方式来定义Pointcut,在注解会提供各种属性来帮助开发人员方便地定义各种扩展点。因此,大概的思路就是这样的:
- 注解的定义 - 用来限定Pointcut的范围,以及一些扩展点的定义
- Advice的定义 - 具体而言就是一个@Aspect Bean,其中定义了控制结构和各种扩展点
- Pointcut的定义 - 结合自定义的注解,在目标方法上使用该注解完成Pointcut的定义
注解的定义
根据需要完成的功能的语义,就把这个注解称为@Retry吧,它的实现如下所示:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retry {
int maxRetryCount() default 5;
String shouldRetry() default "";
String isOutputOK() default "";
String handleException() default "";
String beforeExceptionalReturn() default "";
}
注解中定义了几个关键的信息:
- maxRetryCount:最多重试的次数(包括第一次调用),默认值为5。即会在初次调用失败后最多重试4次
- shouldRetry:判断是否需要重试的扩展点,传入的是一个方法名
- isOutputOK:判断返回结果是否合法的扩展点,传入的是一个方法名
- handleException:在发生异常后进行处理的扩展点,传入的是一个方法名
- beforeExceptionalReturn:在所有重试都失败后进行处理的扩展点,传入的是一个方法名
目标业务方法的定义
为了测试整个@Retry以及相关的Aspect是否满足需求,下面也定义了一个简单的方法作为目标业务方法(RetryService.doRetryBusiness):
@Service
public class RetryService {
private AtomicInteger retryCount = new AtomicInteger(0);
@Retry(maxRetryCount =