在电商场景中,会调用很多第三方的云服务,比如发送邮件、发起支付、发送验证码等等。由于网络存在抖动,有时候发起调用后会拿到500的状态码,io exception等报错,因此需要重新调用,简称重试机制。项目中很多地方用到重试机制,导致很多重复的代码,因此笔者考虑使用Java8函数式接口优化该重试机制,抽成一个工具类方法。
- 本文的代码中,可能有些类型没有给出代码,不需要纠结,主要了解函数式接口怎么应用即可
项目中多次出现的代码如下:
BasicResponse<String> response = null;
int retryTimes = 0;
do {
try {
String startTimeStr = DATE_TIME_FORMATTER.format(LocalDateTime.now());
response = restTemplate.postForString(basicRequest); // 此行代码是可变的,可能是get方式请求,可能是post方式
String endTimeStr = DATE_TIME_FORMATTER.format(LocalDateTime.now());
PayReq logObject = PayReq.getLogObject(payReq);
log.info("XXXPay payOrder, request:{}, response:{}, startTimeStr:{}, endTimeStr:{}, retryTimes:{}", JSON.toJSONString(logObject), JSON.toJSONString(response), startTimeStr, endTimeStr, retryTimes);
} finally {
if (response != null && !response.getCode().equals(HttpStatus.SC_OK)) {
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
retryTimes++;
}
} while (!response.getCode().equals(HttpStatus.SC_OK) && retryTimes < 3);
分析:
如上所示,在这行代码response = restTemplate.postForString(basicRequest);
是可变的,有可能是get方式提交http请求,有可能是post方式。因此要把此处抽象出来,交给调用者写具体实现。调用者需要拿到http响应报文,那么抽象出来的接口,需要有返回值。那么此处可以使用Supplier
函数式接口,或者自己定义一个有返回值的函数式接口也可以。
在log.info
打日志这行,需要打出响应报文、开始时间、结束时间、重试次数等,这些都可以抽到工具类里面,但是日志的内容XXXPay payOrder
这些是可变的,应该交由调用者写具体实现。那么我们可以定义一个函数式接口出来,有入参但无返回值,入参是提供给调用者使用的。
定义一个打日志的函数式接口:
/**
* 打日志的函数式接口
*
* @param
*/
@FunctionalInterface
public interface LogFunc<T> {
/**
* 打日志
*
* @param response 响应报文
* @param startTimeStr http调用开始时间
* @param endTimeStr http调用结束时间
* @param curTime 当前重试次数
*/
void log(T response, String startTimeStr, String endTimeStr, int curTime);
}
Http重试工具类如下,主要关注有代码注释的那两处地方即可:
@Slf4j
public class HttpRetryUtil {
private final static DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SSS");
public static <T> T retryOnException(Supplier<T> supplier, LogFunc logFunc,int maxRetryTimes, long sleepMillis) {
T result = null;
int retryTimes = 0;
do {
try {
String startTimeStr = LocalDateTime.now().format(DATE_TIME_FORMATTER);
// 交给调用者写具体实现,并把值返回出去
result = supplier.get();
String endTimeStr = LocalDateTime.now().format(DATE_TIME_FORMATTER);
// 交给调用者写具体实现,入参供调用者使用
logFunc.log(result, startTimeStr, endTimeStr, retryTimes);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (result != null && !((BasicResponse<String>) result).getCode().equals(HttpStatus.SC_OK)) {
try {
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
retryTimes++;
}
} while (((result == null) || !((BasicResponse<String>) result).getCode().equals(HttpStatus.SC_OK))
&& retryTimes < maxRetryTimes);
return result;
}
}
测试用例,如下所示,优化前有21行/代码(见第3小节的代码),其实如果不写注释不换行,只需用1行就可以将这个重试机制调用起来了(见下面的代码),简洁多了:
@Slf4j
public class HttpRetryUtilTest extends AppTest {
@Resource
private HttpRestTemplate restTemplate;
@Test
public void testRetry(){
BasicRequest basicRequest = new BasicRequest();
basicRequest.setMethodUrl("https://www.google.com");
BasicResponse<String> resp = HttpRetryUtil.retryOnException(
// 实现supplier函数式接口
() -> restTemplate.getForString(basicRequest),
// 实现LogFunc函数式接口
(response, startTimeStr, endTimeStr, curTime)
-> log.info("HttpRetryUtil retryOnException, request:{}, response:{}, startTimeStr:{}, endTimeStr:{}, times:{}", JSON.toJSONString(basicRequest), JSON.toJSONString(response), startTimeStr, endTimeStr, curTime),
3, 500L);
log.info("repsonse:{}", JSON.toJSONString(resp));
}
}
针对那些重试次数、休眠时间,可以在工具类中再定义一些默认的重试次数、默认的休眠时间,然后利用Java的多态特性(方法重载)定义多种工具方法即可。