背景
在项目中需要调用第三方接口,但调用三方接口时可能出现网络错误等外部问题导致的接口调用失败,现需要在此场景下重新尝试调用,并指定重试(错误)的最大次数,超过最大次数后发送邮件通知
硬编码的做法(使用手写代码实现接口调用重试)
在代码块中手动循环处理,指定最大的循环次数(尝试次数),来实现接口重试调用
缺点: 硬编码的方式对主流程是强侵入的,侵入性很高,并且没有可定制化可配置维护不方便
基于spring aop(切面)的三方接口调用重试实现
通过spring aop切片的机制,在请求三方接口的地方增加一个切面,通过一个注释来配置重试相关的信息,然后通过切面内部的逻辑来处理重试和错误处理,分离了重试逻辑和主逻辑
优点: 重试逻辑和主逻辑分离开来,主流程中无需关心具体重试流程和逻辑,并且可以通过注解来配置定制重试次数和是否发送错误邮件等
具体代码实现
需要重试的接口请求工具类:
package com.wjj.application.facade.kangmei.util;
/**
* 接口请求工具类
* 这里所有公共方法都应该是对外的接口,公共方法会经过切面:
* 失败重试二次,重试后仍然失败发邮件,KangmeiBaseException异常直接邮件
*/
@Slf4j
@Component
public class KangMeiUtil {
/**
* 同步订单,只有同步成功才会返回,
* 其他情况,抛出异常
* @param orderInfo
* @return
* @throws Exception
*/
@RetryProcess(apiName = "订单同步")
public OrderInfo1 saveOrderInfo(OrderInfo orderInfo) throws Exception {
if (orderInfo == null || orderInfo.getData() == null){
//入参错误
throw new KangmeiParamErrorException(orderInfo);
}
// webService调用
Result result = (Result) xStream.fromXML(s);
if (result == null || !"0".equals(result.getResultCode()) || result.getOrderInfo() == null || !result.getOrderInfo().getSuccess()){
//康美返回失败
throw new KangmeiResultException("同步下单失败", orderInfo, result);
}
return result.getOrderInfo();
}
}
用于重试配置的注解:
package com.wjj.application.facade.kangmei.annotation;
import com.wjj.application.facade.kangmei.exception.KangmeiBaseException;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 康美接口重试注解
* 1.这里要注意入参会重复使用,set的值会保留
* 2.KangmeiBaseException,不会进行重试
* @author hank
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RetryProcess {
//重试的次数,默认2次,总共3次
int value() default 2;
/**
* 接口名
*/
String apiName();
/**
* 重试失败是否发邮件
* @return
*/
boolean isRetryFailSendEmail() default true;
/**
* 不重试的异常
* @return
*/
Class<? extends Throwable>[] notRetryThrowable() default {KangmeiBaseException.class};
}
切面处理重试和发送邮件逻辑:
package com.wjj.application.facade.kangmei.util;
import com.wjj.application.facade.kangmei.KangMeiConfig;
import com.wjj.application.facade.kangmei.annotation.RetryProcess;
import com.wjj.application.wjjutil.common.EmailUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* 康美请求工具类切面
* 这里所有公共方法都应该是对位的接口,公共方法会经过切面:
* 失败重试二次失败发邮件,KangmeiBaseException异常直接邮件
* @author hank
*/
@Slf4j
@Aspect
@Component
public class KangMeiUtilAspect {
@Autowired
private KangMeiConfig kangMeiConfig;
@Autowired
private EmailUtils emailUtils;
@Pointcut("execution(* com.wjj.application.facade.kangmei.util.KangMeiUtil.*(..))")
public void utilPointcut() {
}
@Around("utilPointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("康美接口,开始调用,入参:{}", Arrays.toString(joinPoint.getArgs()));
try {
Object ret = joinPoint.proceed();
log.info("康美接口,结束调用,出参:{}", String.valueOf(ret));
return ret;
} catch (Throwable ex) {
log.error("康美接口,调用异常", ex);
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
RetryProcess retryProcess = methodSignature.getMethod().getAnnotation(RetryProcess.class);
if(retryProcess != null){
return runThrowing(joinPoint, ex, 1, retryProcess);
}
throw ex;
}
}
public Object runThrowing(JoinPoint point, Throwable ex, Integer nowCount, RetryProcess retryProcess) throws Throwable {
if (isInstanceof(ex, retryProcess.notRetryThrowable()) || nowCount > retryProcess.value()) {
if (retryProcess.isRetryFailSendEmail()){
// 发邮件
emailUtils.sendText(
String.format("康美%s接口调用失败", retryProcess.apiName()),
kangMeiConfig.getEmails(),
String.format("请求参数:%s;\n\r异常信息:%s", Arrays.toString(point.getArgs()), ex.getMessage()));
}
throw ex;
}else{
log.info("康美请求 开始重试第"+nowCount);
try {
MethodInvocationProceedingJoinPoint methodPoint = ((MethodInvocationProceedingJoinPoint) point);
return methodPoint.proceed();
} catch (Throwable throwable) {
log.error("康美请求 重试失败", throwable);
return runThrowing(point, ex, ++nowCount, retryProcess);
}
}
}
/**
* 指定sourceObj是否在inClasses中
* @param sourceObj
* @param inClasses
* @return
*/
private boolean isInstanceof(Object sourceObj, Class<?>[] inClasses){
for (Class<?> inObj : inClasses) {
if(inObj.isInstance(sourceObj))
return true;
}
return false;
}
}