电商项目之Java8函数式接口落地实践

在这里插入图片描述

1 问题背景

在电商场景中,会调用很多第三方的云服务,比如发送邮件、发起支付、发送验证码等等。由于网络存在抖动,有时候发起调用后会拿到500的状态码,io exception等报错,因此需要重新调用,简称重试机制。项目中很多地方用到重试机制,导致很多重复的代码,因此笔者考虑使用Java8函数式接口优化该重试机制,抽成一个工具类方法。

2 前言

  1. 本文的代码中,可能有些类型没有给出代码,不需要纠结,主要了解函数式接口怎么应用即可

3 多处重复的重试机制代码

项目中多次出现的代码如下:

        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这些是可变的,应该交由调用者写具体实现。那么我们可以定义一个函数式接口出来,有入参但无返回值,入参是提供给调用者使用的。

4 优化后的代码

定义一个打日志的函数式接口:

/**
 * 打日志的函数式接口
 * 
 * @param <T>
 */
@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));
    }
}

5 进一步优化

针对那些重试次数、休眠时间,可以在工具类中再定义一些默认的重试次数、默认的休眠时间,然后利用Java的多态特性(方法重载)定义多种工具方法即可。