【springboot】RestTemplate配置HttpClient连接池

在Java开发中,访问第三方HTTP协议的网络接口,通常使用的连接工具为JDK自带的HttpURLConnection、HttpClient(现在应该称之为HttpComponents)和OKHttp。

这些Http连接工具,使用起来都比较复杂,如果项目中使用的是Spring框架,可以使用Spring自带的RestTemplate来进行Http连接请求。

RestTemplate底层默认的连接方式是Java中的HttpURLConnection,可以使用ClientHttpRequestFactory来指定底层使用不同的HTTP连接方式。

RestTemplate中默认的连接方式

RestTemplate中默认使用的是SimpleClientHttpRequestFactory,我们这里手动创建SimpleClientHttpRequestFactory可以指定连接的超时时间,读数据的超时时间。

java 复制代码
package com.morris.user.demo;

import com.morris.user.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

/**
 * restTemplate+httpUrlConnection
 */
@Slf4j
public class RestTemplateDemo1 {

    public static void main(String[] args) {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(3000);
        factory.setReadTimeout(5000);
        RestTemplate restTemplate = new RestTemplate(factory);
        Order[] orders = restTemplate.getForObject("http://127.0.0.1:8020/order/findOrderByUserId?userId=", Order[].class, 1);
        log.info("orders :{}", orders);
    }

}

SimpleClientHttpRequestFactory底层在创建请求的时候使用的就是HttpURLConnection。

org.springframework.http.client.SimpleClientHttpRequestFactory#createRequest

java 复制代码
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
    HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
    prepareConnection(connection, httpMethod.name());

    if (this.bufferRequestBody) {
        return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
    }
    else {
        return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
    }
}

RestTemplate与HttpClient的结合

只需要在构造RestTemplate实例时传入HttpComponentsClientHttpRequestFactory对象即可。

java 复制代码
package com.morris.user.demo;

import com.morris.user.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

/**
 * RestTemplate+HttpClient
 */
@Slf4j
public class RestTemplateDemo2 {

    public static void main(String[] args) {
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        Order[] orders = restTemplate.getForObject("http://127.0.0.1:8020/order/findOrderByUserId?userId=", Order[].class, 1);
        log.info("orders :{}", orders);
    }

}

HttpComponentsClientHttpRequestFactory底层在创建请求时使用了HttpClient。

org.springframework.http.client.HttpComponentsClientHttpRequestFactory#createRequest

java 复制代码
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
    HttpClient client = getHttpClient();

    HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
    postProcessHttpRequest(httpRequest);
    HttpContext context = createHttpContext(httpMethod, uri);
    if (context == null) {
        context = HttpClientContext.create();
    }

    // Request configuration not set in the context
    if (context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) {
        // Use request configuration given by the user, when available
        RequestConfig config = null;
        if (httpRequest instanceof Configurable) {
            config = ((Configurable) httpRequest).getConfig();
        }
        if (config == null) {
            config = createRequestConfig(client);
        }
        if (config != null) {
            context.setAttribute(HttpClientContext.REQUEST_CONFIG, config);
        }
    }

    if (this.bufferRequestBody) {
        return new HttpComponentsClientHttpRequest(client, httpRequest, context);
    }
    else {
        return new HttpComponentsStreamingClientHttpRequest(client, httpRequest, context);
    }
}

RestTemplate与HttpClient的在生产环境使用的最佳实践

在构建HttpClient时,经常需要配置很多信息,例如RequestTimeout、ConnectTimeout、SocketTimeout、代理、是否允许重定向、连接池等信息。

在HttpClient,对这些参数进行配置需要使用到RequestConfig类的一个内部类Builder。

这里将这些常用的配置抽取出来放到配置文件中:

java 复制代码
package com.morris.user.config;

import lombok.extern.slf4j.Slf4j;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpHost;
import org.apache.http.client.HttpClient;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.ssl.SSLContextBuilder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;


@EnableConfigurationProperties(HttpClientConfig.class)
@ConditionalOnClass(RestTemplate.class)
@Configuration
@Slf4j
public class RestTemplateAutoConfiguration {

    @Resource
    private HttpClientConfig httpClientConfig;

    @Bean
    @ConditionalOnClass(CloseableHttpClient.class)
    public RestTemplate httpClientRestTemplate(ClientHttpRequestFactory clientHttpRequestFactory){
        return new RestTemplate(clientHttpRequestFactory);
    }

    @Bean
    @ConditionalOnClass(CloseableHttpClient.class)
    public ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) {
        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
        clientHttpRequestFactory.setHttpClient(httpClient);
        clientHttpRequestFactory.setConnectTimeout(httpClientConfig.getRequest().getConnectTimeout());
        clientHttpRequestFactory.setReadTimeout(httpClientConfig.getRequest().getReadTimeout());
        clientHttpRequestFactory.setConnectionRequestTimeout(httpClientConfig.getRequest().getConnectionRequestTimeout());
        clientHttpRequestFactory.setBufferRequestBody(httpClientConfig.getRequest().isBufferRequestBody());
        return clientHttpRequestFactory;
    }

    @Bean
    @Primary
    @ConditionalOnClass(CloseableHttpClient.class)
    public HttpClient httpClient() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        try {
            // 设置信任SSL访问
            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (arg0, arg1) -> true).build();
            httpClientBuilder.setSSLContext(sslContext);
            // 任何主机都不会抛出SSLException异常
            HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
            SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
            Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                    // 注册HTTP和HTTPS请求
                    .register("http", PlainConnectionSocketFactory.getSocketFactory())
                    .register("https", sslConnectionSocketFactory).build();

            // 使用Httpclient连接池的方式配置
            PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
            poolingHttpClientConnectionManager.setMaxTotal(httpClientConfig.getPool().getMaxTotalConnect());
            poolingHttpClientConnectionManager.setDefaultMaxPerRoute(httpClientConfig.getPool().getMaxConnectPerRoute());
            poolingHttpClientConnectionManager.setValidateAfterInactivity(httpClientConfig.getPool().getValidateAfterInactivity());

            httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
            httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(httpClientConfig.getPool().getRetryTimes(), true));
            httpClientBuilder.setKeepAliveStrategy(connectionKeepAliveStrategy());
            return httpClientBuilder.build();
        } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
            log.error("初始化HTTP连接池出错", e);
            throw e;
        }
    }

    /**
     * 配置长连接保持策略
     * @return ConnectionKeepAliveStrategy
     */
    public ConnectionKeepAliveStrategy connectionKeepAliveStrategy(){
        return (response, context) -> {
            // Honor 'keep-alive' header
            HeaderElementIterator it = new BasicHeaderElementIterator(
                    response.headerIterator(HTTP.CONN_KEEP_ALIVE));
            while (it.hasNext()) {
                HeaderElement he = it.nextElement();
                String param = he.getName();
                String value = he.getValue();
                if (value != null && "timeout".equalsIgnoreCase(param)) {
                    try {
                        return Long.parseLong(value) * 1000;
                    } catch(NumberFormatException error) {
                        log.error("解析长连接过期时间异常", error);
                    }
                }
            }
            HttpHost target = (HttpHost) context.getAttribute(HttpClientContext.HTTP_TARGET_HOST);
            //如果请求目标地址,单独配置了长连接保持时间,使用该配置
            Optional<Map.Entry<String, Integer>> any = Optional.ofNullable(httpClientConfig.getPool().getKeepAliveTargetHost()).orElseGet(HashMap::new)
                    .entrySet().stream().filter(
                            e -> e.getKey().equalsIgnoreCase(target.getHostName())).findAny();
            //否则使用默认长连接保持时间
            return any.map(en -> en.getValue() * 1000L).orElse(httpClientConfig.getPool().getKeepAliveTime() * 1000L);
        };
    }

}
相关推荐
向前看-2 小时前
验证码机制
前端·后端
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹2 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭3 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫3 小时前
泛型(2)
java
超爱吃士力架3 小时前
邀请逻辑
java·linux·后端
南宫生3 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石3 小时前
12/21java基础
java
李小白664 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp4 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea