RestTemplate 一开始是在 Spring boot 的消费一个 rest 风格的 web 服务 遇到的,当时比较奇怪,正常来说与 http 请求相关的类名应该都带点 http 元素吧。

服务远程调用指南(RestTemplate、HttpClient) 中概述里有这样一句:

java开发中,使用 http 连接,访问第三方网络接口,通常使用的连接工具为 RestTemplate、HttpClient 和 OKHttp。

即已经存在了一个叫做 HttpClient 的包。RestTemplate 属于 Spring Web 中 client 下的一个实现类,官网的引导里也只需要引入 Spring Web 依赖就能使用。

因为项目不同的系统部分是独立的,数据也没有互通,所以需要在不同的系统间使用接口交互关键数据。一开始还好,但项目部署到正式站之后,数据量大和网络不稳定,导致时长丢失关键数据,进而导致一些业务数据状态异常。这时想起 PHP curl(类似的创建 http client 发送请求的工具)是可以设置重试次数,即当目标服务不稳定时可以自动重新发送请求。

搜索 RestTeplate 重试,找到了两种解决方案:

因为需要的依赖 HttpClient 已经安装好了,所以优先选择下面的方案,通过修改 RestTemplateConfig 配置文件,即修改 ClientHttpRequestFactory 相关配置来实现重试策略。

引入依赖 HttpClient

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.7</version>
</dependency>

4.x 版本可以在 jdk 1.8 下使用,最新的 httpclient5 不确定兼容性。

修改配置类

基础配置

@Configuration
public class RestTemplateConfig {
   @Bean
   public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
       return new RestTemplate(factory);
   }
    
   @Bean
   public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
       SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
       factory.setReadTimeout(150 * 1000); // ms
       factory.setConnectTimeout(150 * 1000); // ms
       return factory;
   }
}

基础配置通过 SimpleClientHttpRequestFactory 可以设置一些基本的读写超时、连接超时。这对于设置重试策略是不够的。

进阶配置

    /**
     * 定义 Http 请求拦截器
     * @return
     */
    @Bean
    public ClientHttpRequestInterceptor interceptor() {
        return new RestTemplateInterceptor();
    }

    /**
     * 定义 HttpClient
     * @return
     */
    @Bean
    public HttpClient httpClient() {
        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", SSLConnectionSocketFactory.getSocketFactory())
                .build();
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
        connectionManager.setMaxTotal(maxTotal); // 最大连接数
        connectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute);    //单个路由最大连接数
        connectionManager.setValidateAfterInactivity(validateAfterInactivity); // 最大空间时间
        RequestConfig requestConfig = RequestConfig.custom()
                //服务器返回数据(response)的时间,超过抛出read timeout
                .setSocketTimeout(socketTimeout)
                //连接上服务器(握手成功)的时间,超出抛出connect timeout
                .setConnectTimeout(connectTimeout)
                //从连接池中获取连接的超时时间,超时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
                .setConnectionRequestTimeout(connectionRequestTimeout)
                .build();
        return HttpClientBuilder.create()
                .setDefaultRequestConfig(requestConfig)
                .setConnectionManager(connectionManager)
                // 设置重试策略和重试次数
                .setServiceUnavailableRetryStrategy(new MyDefaultServiceUnavailableRetryStrategy())
                .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true))
                .build();
    }

    /**
     * 定义 请求工厂
     * @return
     */
    @Bean
    public ClientHttpRequestFactory httpRequestFactory() {
        return new HttpComponentsClientHttpRequestFactory(httpClient());
    }

    /**
     * 定义 rest 模板
     * @return
     */
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate(httpRequestFactory());
        restTemplate.setInterceptors(Collections.singletonList(interceptor()));
        return restTemplate;
    }

重新策略核心:

return HttpClientBuilder.create()
...
    // 设置重试策略和重试次数
    .setServiceUnavailableRetryStrategy(new MyDefaultServiceUnavailableRetryStrategy())
    .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true))
    .build();

MyDefaultServiceUnavailableRetryStrategy

/**
 * 根据返回的HTTP状态码决定是否要重试
 */
@Slf4j
public class MyDefaultServiceUnavailableRetryStrategy implements ServiceUnavailableRetryStrategy {

    private final int maxRetries;
    private final long retryInterval;

    public MyDefaultServiceUnavailableRetryStrategy(int maxRetries, int retryInterval) {
        Args.positive(maxRetries, "Max retries");
        Args.positive(retryInterval, "Retry interval");
        this.maxRetries = maxRetries;
        this.retryInterval = (long)retryInterval;
    }

    public MyDefaultServiceUnavailableRetryStrategy() {
        this(1, 1000);
    }

    /**
     * 修改此处的判断
     * @param response
     * @param executionCount
     * @param context
     * @return
     */
    public boolean retryRequest(HttpResponse response, int executionCount, HttpContext context) {
        log.info("retryRequest: " + response.getStatusLine().getStatusCode());
        return executionCount <= this.maxRetries && (response.getStatusLine().getStatusCode() == 503||
                response.getStatusLine().getStatusCode() == 502||
                response.getStatusLine().getStatusCode() == 403||
                response.getStatusLine().getStatusCode() == 405)
                ;
    }

    public long getRetryInterval() {
        return this.retryInterval;
    }
}

DefaultHttpRequestRetryHandler 两个参数 retryCount, requestSentRetryEnabled 分别表示重试次数和已发送请求重试开关。

到这里就可以了,会在请求返回状态码为 503、502、403、405 会发起请求重试,重试一次,间隔 1s。