前面的一系列文章介绍了AOP的方方面面:
从本篇文章开始,会介绍一些基于AOP原理的自定义Aspect实现,用来解决在开发过程中可能遇到的各种常见问题。
在开发爬虫类应用的时候,经常需要处理的问题就是一次爬取过程失败了应该如何处理。其实爬取失败的比率在网络条件比较不稳定的情况下还是相当高的。解决办法一般都会考虑重新尝试这一最最基本和简单的方案。因此,在相关代码中就会出现很多这种结构:
/**
* 带有失败重试功能的业务代码。
*
* @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;
}
上述代码表达的是一个网络请求相关的通用处理结构。可以发现其中主要包含一个控制结构以及若干扩展点:
控制结构:
扩展点:
因此,从业务的角度来看,真正关心的也许只是request这一个方法。当然,为了应用的健壮性和灵活性,上面的扩展点都可以根据需要进行扩展,但是大多数情况下采用默认实现也绝对是够用的。
想要开发一个Aspect,从它本身的定义来看,首先需要考虑的就是如何定义Advice以及Pointcut。
我们可以将上述扩展点中的request方法作为目标方法,单独定义一个Component用于Advice的定义,然后采用一个基于注解的方式来定义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 "";
}
注解中定义了几个关键的信息:
为了测试整个@Retry以及相关的Aspect是否满足需求,下面也定义了一个简单的方法作为目标业务方法(RetryService.doRetryBusiness):
@Service
public class RetryService {
private AtomicInteger retryCount = new AtomicInteger(0);
@Retry(maxRetryCount = 2, beforeExceptionalReturn = "extendedBeforeExceptionalReturn")
public String doRetryBusiness() {
if (retryCount.getAndIncrement() < 4) {
throw new RuntimeException(Thread.currentThread().getName() + ": 结果获取失败");
}
return Thread.currentThread().getName() + ": 这是最终结果";
}
public void extendedBeforeExceptionalReturn() {
System.out.println(Thread.currentThread().getName() + ": 自定义的处理失败后扩展点");
}
}
这个业务方法使用了上一步定义的@Retry注解进行修饰。它将最大重试的次数改成了2,也就是说最多只允许重试一次。另外还定义了beforeExceptionalReturn扩展点的实现方法的名称。这个方法对应的就是下方的:
public void extendedBeforeExceptionalReturn() {
System.out.println("自定义的处理失败后扩展点");
}
因此我们期望的结果是当超过了调用业务方法的最大重试次数后,在返回空结果前会执行我们自定义的方法。由于注解中只包含了方法名称这一字符串类型的信息,毫无疑问在具体的Advice中会通过反射的方法来找到方法对象并调用之。
很显然,并不是每次使用@Retry注解的时候都需要提供所有的扩展点实现。如果不提供的话则应该使用默认的实现。这些默认实现可以集中管理:
public abstract class RetrySupport {
protected boolean shouldRetry() {
return true;
}
protected boolean isOutputOK(Object output) {
return Objects.nonNull(output);
}
protected void handleException(Exception e) {
System.out.println(e.getMessage());
}
protected void beforeExceptionalReturn() {
System.out.println("默认的处理失败后扩展点");
}
}
这个类定义了所有的默认方法。当没有提供自定义的扩展方法的时候就会调用它们。
紧接着,就是Aspect本身了的定义了:
@Component
@Aspect
public class RetryAspect extends RetrySupport {
private static ThreadLocal retryCounters;
static {
retryCounters = ThreadLocal.withInitial(() -> {
return 0;
});
}
@Around("com.rxjiang.aop.custom.Pointcuts.retryPointcuts()")
public Object retryAdvice(ProceedingJoinPoint pjp) throws Throwable {
System.out.println(Thread.currentThread().getName() + ": 进入Advice");
// 获取被调用的对象以及Retry注解对象
MethodSignature signature = (MethodSignature) pjp.getSignature();
String methodName = signature.getMethod().getName();
Class>[] parameterTypes = signature.getMethod().getParameterTypes();
Object calledObject = pjp.getTarget();
Retry retryAnno =
pjp.getTarget().getClass().getMethod(methodName, parameterTypes).getAnnotation(Retry.class);
try {
while (aspectShouldRetry(calledObject, retryAnno)) {
try {
Object result = pjp.proceed(pjp.getArgs());
if (isOutputOK(result)) {
return result;
} else {
continue;
}
} catch (Exception e) {
System.out.println(Thread.currentThread().getName() + ": 捕获到了异常: " + e.getMessage());
handleException(e);
}
}
} finally {
retryCounters.set(0);
}
aspectBeforeExceptionalReturn(calledObject, retryAnno);
return null;
}
// 拓展点:失败返回前的处理
private void aspectBeforeExceptionalReturn(Object calledObject, Retry retryAnno)
throws Throwable {
String beforeExceptionalReturnMethodName = retryAnno.beforeExceptionalReturn();
if (StringUtils.isEmpty(beforeExceptionalReturnMethodName)) {
super.beforeExceptionalReturn();
} else {
Method beforeExceptionalReturnMethod =
calledObject.getClass().getMethod(beforeExceptionalReturnMethodName);
if (beforeExceptionalReturnMethod == null) {
super.beforeExceptionalReturn();
} else {
beforeExceptionalReturnMethod.invoke(calledObject, new Object[] {});
}
}
}
// 拓展点: 是否进行重试
private boolean aspectShouldRetry(Object calledObject, Retry retryAnno) throws Throwable {
Integer currentCount = retryCounters.get();
retryCounters.set(currentCount + 1);
if (++currentCount > retryAnno.maxRetryCount()) {
return false;
}
boolean shouldRetry = false;
String shouldRetryMethodName = retryAnno.shouldRetry();
if (StringUtils.isEmpty(shouldRetryMethodName)) {
shouldRetry = super.shouldRetry();
} else {
Method shouldRetryMethod = calledObject.getClass().getMethod(shouldRetryMethodName);
if (shouldRetryMethod == null) {
System.out.println("Method does not exist, fallback to default one.");
shouldRetry = super.shouldRetry();
} else {
shouldRetry = (boolean) shouldRetryMethod.invoke(calledObject, new Object[] {});
}
}
return shouldRetry;
}
}
这个类比较长,但是逻辑还算清晰,主要分为以下几个部分:
在主体结构中的while循环里面,会根据注解对象的信息来决定是调用自定义的扩展方法还是默认方法,以是否进行重试这个扩展点作为例子:
// 拓展点: 是否进行重试
private boolean aspectShouldRetry(Object calledObject, Retry retryAnno) throws Throwable {
Integer currentCount = retryCounters.get();
retryCounters.set(currentCount + 1);
if (++currentCount > retryAnno.maxRetryCount()) {
return false;
}
boolean shouldRetry = false;
String shouldRetryMethodName = retryAnno.shouldRetry();
if (StringUtils.isEmpty(shouldRetryMethodName)) {
shouldRetry = super.shouldRetry();
} else {
Method shouldRetryMethod = calledObject.getClass().getMethod(shouldRetryMethodName);
if (shouldRetryMethod == null) {
System.out.println("Method does not exist, fallback to default one.");
shouldRetry = super.shouldRetry();
} else {
shouldRetry = (boolean) shouldRetryMethod.invoke(calledObject, new Object[] {});
}
}
return shouldRetry;
}
如果当前重试的计数已经超过了最大重试次数,那么直接返回false用来终止执行。否则会继续执行查看是否自定义了重试方法名称。如果定义且方法对象却是存在,那么会调用自定义的扩展方法;否则调用默认方法,有两种情况会调用默认的方法:
整体配置:
首先是Spring整体的配置,比如开启对于AOP的支持,启动包扫描功能:
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.rxjiang")
public class CustomAopConfiguration {
}
Pointcut的定义:
public class Pointcuts {
@Pointcut("execution(@com.rxjiang.aop.custom.Retry * *(..))")
public void retryPointcuts() {}
}
测试方法:
串行部分的测试:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {CustomAopConfiguration.class})
public class RetryTest {
@Autowired
private RetryService retryService;
@Test
public void testRetryMethod1() {
System.out.println(retryService.doRetryBusiness());
}
@Test
public void testRetryMethod2() {
System.out.println(retryService.doRetryBusiness());
}
@Test
public void testRetryMethod3() {
System.out.println(retryService.doRetryBusiness());
}
}
以上是串行部分的测试。最终的输出大概是这样的:
main: 进入Advice
main: 捕获到了异常: main: 结果获取失败
main: 结果获取失败
main: 捕获到了异常: main: 结果获取失败
main: 结果获取失败
main: 自定义的处理失败后扩展点
null
main: 进入Advice
main: 捕获到了异常: main: 结果获取失败
main: 结果获取失败
main: 捕获到了异常: main: 结果获取失败
main: 结果获取失败
main: 自定义的处理失败后扩展点
null
main: 进入Advice
main: 这是最终结果
以上总共会打印出4条获取失败的信息,因为在业务方法中定义了前四次调用都会返回失败。每个测试方法最多会重试2次(加上初次调用),因此测试方法testRetryMethod1和测试方法testRetryMethod2最后的结果都是null。此时已经一共尝试了4次,因此当testRetryMethod3方法执行的时候会成功得到结果。
并行部分的测试:
@Test
public void testConcurrentRetry() throws InterruptedException {
IntStream.range(0, 5).forEach(i -> {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + ": 启动了");
System.out.println(retryService.doRetryBusiness());
} catch (Exception e) {
e.printStackTrace();
}
}).start();
});
Thread.sleep(10000);
}
这里启动了5个线程同时去访问业务方法,同样地前4次会故意设置成失败,因此最多只会打印出来4条失败信息:
Thread-2: 启动了
Thread-4: 启动了
Thread-3: 启动了
Thread-5: 启动了
Thread-6: 启动了
Thread-3: 进入Advice
Thread-4: 进入Advice
Thread-5: 进入Advice
Thread-6: 进入Advice
Thread-2: 进入Advice
Thread-4: 捕获到了异常: Thread-4: 结果获取失败
Thread-5: 捕获到了异常: Thread-5: 结果获取失败
Thread-5: 结果获取失败
Thread-2: 捕获到了异常: Thread-2: 结果获取失败
Thread-2: 结果获取失败
Thread-6: 捕获到了异常: Thread-6: 结果获取失败
Thread-6: 结果获取失败
Thread-6: 这是最终结果
Thread-3: 这是最终结果
Thread-2: 这是最终结果
Thread-5: 这是最终结果
Thread-4: 结果获取失败
Thread-4: 这是最终结果
而且值得注意的是每个线程都成功获取到了最终结果,这一行为和串行的方式有所差异。从打印的信息来看的话,线程2,4,5,6分别失败了一次,而每个线程最多是可以重试两次的,因此每个线程都获取了结果。
除了上述代码介绍的扩展点之外,其实还有很多地方可以扩展,比如:
本文介绍了一种基于AOP的重试机制的实现方法。在失败率比较高,但是可通过重试来解决的业务场景中可以考虑使用它来简化代码。这样做能够将和业务无关的代码剥离出去,尽可能地做到单一职责,让代码更加优雅。
这也是AOP的初衷,让各种模板代码从业务中独立出去,实现模板代码和业务代码的独立维护。