RestTemplate實踐(及遇到的問題)


  在微服務都是以HTTP接口的形式暴露自身服務的,因此在調用遠程服務時就必須使用HTTP客戶端。我們可以使用JDK原生的URLConnection、Apache的Http Client、Netty的異步HTTP Client, Spring的RestTemplate。但是,用起來最方便、最優雅的還是要屬Feign了。這里介紹的是RestTemplate。

什么是RestTemplate?

RestTemplate是Spring提供的用於訪問Rest服務的客戶端,

RestTemplate提供了多種便捷訪問遠程Http服務的方法,能夠大大提高客戶端的編寫效率。

調用RestTemplate的默認構造函數,RestTemplate對象在底層通過使用java.net包下的實現創建HTTP 請求,

可以通過使用ClientHttpRequestFactory指定不同的HTTP請求方式。

ClientHttpRequestFactory接口主要提供了兩種實現方式

1、一種是SimpleClientHttpRequestFactory,使用J2SE提供的方式(既java.net包提供的方式)創建底層的Http請求連接。

2、一種方式是使用HttpComponentsClientHttpRequestFactory方式,底層使用HttpClient訪問遠程的Http服務,使用HttpClient可以配置連接池和證書等信息。

RestTemplate的核心之一 Http Client。

目前通過RestTemplate 的源碼可知,RestTemplate 可支持多種 Http Client的http的訪問,如下所示:

基於 JDK HttpURLConnection 的 SimpleClientHttpRequestFactory,默認。

基於 Apache HttpComponents Client 的 HttpComponentsClientHttpRequestFactory

基於 OkHttp3的OkHttpClientHttpRequestFactory。

基於 Netty4 的 Netty4ClientHttpRequestFactory。

其中HttpURLConnection 和 HttpClient 為原生的網絡訪問類,OkHttp3采用了 OkHttp3的框架,Netty4 采用了Netty框架。

xml配置的方式

請查看RestTemplate源碼了解細節,知其然知其所以然!

RestTemplate默認是使用SimpleClientHttpRequestFactory,內部是調用jdk的HttpConnection,默認超時為-1

基於jdk的spring的RestTemplate

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
       default-autowire="byName" default-lazy-init="true">
 
    <!--方式一、使用jdk的實現-->
    <bean id="ky.requestFactory" class="org.springframework.http.client.SimpleClientHttpRequestFactory">
        <property name="readTimeout" value="10000"/>
        <property name="connectTimeout" value="5000"/>
    </bean>
 
    <bean id="simpleRestTemplate" class="org.springframework.web.client.RestTemplate">
        <constructor-arg ref="ky.requestFactory"/>
        <property name="messageConverters">
            <list>
                <bean class="org.springframework.http.converter.FormHttpMessageConverter"/>
                <bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter"/>
                <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
                <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                    <property name="supportedMediaTypes">
                        <list>
                            <value>text/plain;charset=UTF-8</value>
                        </list>
                    </property>
                </bean>
            </list>
        </property>
    </bean>
</beans>

使用Httpclient連接池的方式

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
       default-autowire="byName" default-lazy-init="true">
 
    <!--方式二、使用httpclient的實現,帶連接池-->
    <bean id="ky.pollingConnectionManager" class="org.apache.http.impl.conn.PoolingHttpClientConnectionManager">
        <!--整個連接池的並發-->
        <property name="maxTotal" value="1000" />
        <!--每個主機的並發-->
        <property name="defaultMaxPerRoute" value="1000" />
    </bean>
 
    <bean id="ky.httpClientBuilder" class="org.apache.http.impl.client.HttpClientBuilder" factory-method="create">
        <property name="connectionManager" ref="ky.pollingConnectionManager" />
        <!--開啟重試-->
        <property name="retryHandler">
            <bean class="org.apache.http.impl.client.DefaultHttpRequestRetryHandler">
                <constructor-arg value="2"/>
                <constructor-arg value="true"/>
            </bean>
        </property>
        <property name="defaultHeaders">
            <list>
                <bean class="org.apache.http.message.BasicHeader">
                    <constructor-arg value="User-Agent"/>
                    <constructor-arg value="Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36"/>
                </bean>
                <bean class="org.apache.http.message.BasicHeader">
                    <constructor-arg value="Accept-Encoding"/>
                    <constructor-arg value="gzip,deflate"/>
                </bean>
                <bean class="org.apache.http.message.BasicHeader">
                    <constructor-arg value="Accept-Language"/>
                    <constructor-arg value="zh-CN"/>
                </bean>
            </list>
        </property>
    </bean>
 
    <bean id="ky.httpClient" factory-bean="ky.httpClientBuilder" factory-method="build" />
 
    <bean id="ky.clientHttpRequestFactory" class="org.springframework.http.client.HttpComponentsClientHttpRequestFactory">
        <constructor-arg ref="ky.httpClient"/>
        <!--連接超時時間,毫秒-->
        <property name="connectTimeout" value="5000"/>
        <!--讀寫超時時間,毫秒-->
        <property name="readTimeout" value="10000"/>
    </bean>
 
    <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
        <constructor-arg ref="ky.clientHttpRequestFactory"/>
        <property name="errorHandler">
            <bean class="org.springframework.web.client.DefaultResponseErrorHandler"/>
        </property>
        <property name="messageConverters">
            <list>
                <bean class="org.springframework.http.converter.FormHttpMessageConverter"/>
                <bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter"/>
                <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
                <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                    <property name="supportedMediaTypes">
                        <list>
                            <value>text/plain;charset=UTF-8</value>
                        </list>
                    </property>
                </bean>
            </list>
        </property>
    </bean>
 
</beans>

bean初始化+靜態工具

線程安全的單例(懶漢模式)

基於jdk的spring的RestTemplate

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
 
import javax.annotation.PostConstruct;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
 

@Component
@Lazy(false)
public class SimpleRestClient {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleRestClient.class);
 
    private static RestTemplate restTemplate;
 
    static {
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        requestFactory.setReadTimeout(5000);
        requestFactory.setConnectTimeout(5000);
 
        // 添加轉換器
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
        messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        messageConverters.add(new FormHttpMessageConverter());
        messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
        messageConverters.add(new MappingJackson2HttpMessageConverter());
 
        restTemplate = new RestTemplate(messageConverters);
        restTemplate.setRequestFactory(requestFactory);
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
 
        LOGGER.info("SimpleRestClient初始化完成");
    }
 
    private SimpleRestClient() {
 
    }
 
    @PostConstruct
    public static RestTemplate getClient() {
        return restTemplate;
    }
 
} 

使用Httpclient連接池的方式

import org.apache.http.Header;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
 
import javax.annotation.PostConstruct;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
 

@Component
@Lazy(false)
public class RestClient {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(RestClient.class);
 
    private static RestTemplate restTemplate;
 
    static {
        // 長連接保持30秒
        PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
        // 總連接數
        pollingConnectionManager.setMaxTotal(1000);
        // 同路由的並發數
        pollingConnectionManager.setDefaultMaxPerRoute(1000);
 
        HttpClientBuilder httpClientBuilder = HttpClients.custom();
        httpClientBuilder.setConnectionManager(pollingConnectionManager);
        // 重試次數,默認是3次,沒有開啟
        httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(2, true));
        // 保持長連接配置,需要在頭添加Keep-Alive
        httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
 
//        RequestConfig.Builder builder = RequestConfig.custom();
//        builder.setConnectionRequestTimeout(200);
//        builder.setConnectTimeout(5000);
//        builder.setSocketTimeout(5000);
//
//        RequestConfig requestConfig = builder.build();
//        httpClientBuilder.setDefaultRequestConfig(requestConfig);
 
        List<Header> headers = new ArrayList<>();
        headers.add(new BasicHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36"));
        headers.add(new BasicHeader("Accept-Encoding", "gzip,deflate"));
        headers.add(new BasicHeader("Accept-Language", "zh-CN"));
        headers.add(new BasicHeader("Connection", "Keep-Alive"));
 
        httpClientBuilder.setDefaultHeaders(headers);
 
        HttpClient httpClient = httpClientBuilder.build();
 
        // httpClient連接配置,底層是配置RequestConfig
        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
        // 連接超時
        clientHttpRequestFactory.setConnectTimeout(5000);
        // 數據讀取超時時間,即SocketTimeout
        clientHttpRequestFactory.setReadTimeout(5000);
        // 連接不夠用的等待時間,不宜過長,必須設置,比如連接不夠用時,時間過長將是災難性的
        clientHttpRequestFactory.setConnectionRequestTimeout(200);
        // 緩沖請求數據,默認值是true。通過POST或者PUT大量發送數據時,建議將此屬性更改為false,以免耗盡內存。
        // clientHttpRequestFactory.setBufferRequestBody(false);
 
        // 添加內容轉換器
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
        messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        messageConverters.add(new FormHttpMessageConverter());
        messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
        messageConverters.add(new MappingJackson2HttpMessageConverter());
 
        restTemplate = new RestTemplate(messageConverters);
        restTemplate.setRequestFactory(clientHttpRequestFactory);
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
 
        LOGGER.info("RestClient初始化完成");
    }
 
    private RestClient() {
 
    }
 
    @PostConstruct
    public static RestTemplate getClient() {
        return restTemplate;
    }
 
} 
 
@Configuration
public class RestConfig {

    @Bean
    public RestTemplate restTemplate(){
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate;
    }

    @Bean("urlConnection")
    public RestTemplate urlConnectionRestTemplate(){
        RestTemplate restTemplate = new RestTemplate(new SimpleClientHttpRequestFactory());
        return restTemplate;
    }

    @Bean("httpClient")
    public RestTemplate httpClientRestTemplate(){
        RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
        return restTemplate;
    }

    @Bean("oKHttp3")
    public RestTemplate OKHttp3RestTemplate(){
        RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory());
        return restTemplate;
    }
}

 

ErrorHolder

自定義的一個異常結果包裝類

使用樣例

api里面可以做自動的參數匹配:
如:http://you domainn name/test?empNo={empNo},則下面方法的最后一個參數為數據匹配參數,會自動根據key進行查找,然后替換

API沒有聲明異常,注意進行異常處理

更多使用語法請查看API文檔

RestTemplate處理請求狀態碼為非200的返回數據

默認的 RestTemplate 有個機制是請求狀態碼非200 就拋出異常,會中斷接下來的操作。如果不想中斷對結果數據得解析,可以通過覆蓋默認的 ResponseErrorHandler ,見下面的示例,示例中的方法中基本都是空方法,只要對hasError修改下,讓他一直返回true,即是不檢查狀態碼及拋異常了。

@Bean("sslRestTemplate")
    public RestTemplate getRestTemplate() throws Exception {
        RestTemplate sslRestTemplate = new RestTemplate(new HttpsClientRequestFactory());
        ResponseErrorHandler responseErrorHandler = new ResponseErrorHandler() {
            @Override
            public boolean hasError(ClientHttpResponse clientHttpResponse) throws IOException {
                return true;
            }
            @Override
            public void handleError(ClientHttpResponse clientHttpResponse) throws IOException {

            }
        };
        sslRestTemplate.setErrorHandler(responseErrorHandler);
        return sslRestTemplate;
    }

或者,修改resttemplate的源碼,把對應的源碼文件拷貝到自己的項目中,但不推薦。

 

例如,我用的是Httpclient的連接池,RestTemplate的超時設置依賴HttpClient的內部的三個超時時間設置。

 HttpClient內部有三個超時時間設置:連接池獲取可用連接超時,連接超時,讀取數據超時:
1.setConnectionRequestTimeout從連接池中獲取可用連接超時:設置從connect Manager獲取Connection 超時時間,單位毫秒。
HttpClient中的要用連接時嘗試從連接池中獲取,若是在等待了一定的時間后還沒有獲取到可用連接(比如連接池中沒有空閑連接了)則會拋出獲取連接超時異常。

2.連接目標超時connectionTimeout,單位毫秒。
 指的是連接目標url的連接超時時間,即客服端發送請求到與目標url建立起連接的最大時間。如果在該時間范圍內還沒有建立起連接,則就拋出connectionTimeOut異常。
 如測試的時候,將url改為一個不存在的url:“ http://test.com” ,超時時間3000ms過后,系統報出異常:    org.apache.commons.httpclient.ConnectTimeoutException:The host did not accept the connection within timeout of 3000 ms 

3.等待響應超時(讀取數據超時)socketTimeout ,單位毫秒。
連接上一個url后,獲取response的返回等待時間 ,即在與目標url建立連接后,等待放回response的最大時間,在規定時間內沒有返回響應的話就拋出SocketTimeout。
測試時,將socketTimeout 設置很短,會報等待響應超時。
 
我遇到的問題,restTemplate請求到一個高可用的服務是,返回的超時時間是設置值的2倍,是因為負載均衡器返回的重定向,導致httpClient底層認為沒有超時,又請求一次,如果負載均衡器下有兩個節點,就耗費 connectionTimeout的雙倍時間。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM