關於RestTemplate的使用深度解析

從 Spring 4.3 開始加入瞭 OkHttp3ClientHttpRequestFactory

一、概述

本文主要介紹 Spring Web 模塊中的 RestTemplate 組件的原理、優缺點、以及如何擴展以滿足各種需求。

在介紹 RestTemplate 之前,我們先來談談 HTTP Client,談談選擇一個優秀的 HTTP Client 實現的的重要性,以及一個優秀的 HTTP Client 應該具備哪些特性。

在 Java 社區中,HTTP Client 主要有 JDK 的 HttpURLConnection、Apache Commons HttpClient(或被稱為 Apache HttpClient 3.x)、Apache HttpComponents Client(或被稱為 Apache HttpClient 4.x)、Square 公司開源的 OkHttp。

除瞭這幾個純粹的 HTTP Client 類庫以外,還有 Spring 的 RestTemplate、Square 公司的 Retrofit、Netflix 公司的 Feign,以及像 Apache CXF 中的 client 組件。這些框架和類庫主要是針對 Web Service 場景,尤其是 RESTful Web Service。它們往往是基於前面提到的 HTTP Client 實現,並在其基礎上提供瞭消息轉換、參數映射等對於 Web Service 來說十分必要的功能。

(當然,像 Netty、Mina 這樣的網絡 IO 框架,實現 HTTP 自然也不再話下,但這些框架通常過於底層,不會被直接使用)

選擇一個優秀的 HTTP Client 的重要性

雖然現在服務間的調用越來越多地使用瞭 RPC 和消息隊列,但是 HTTP 依然有適合它的場景。

RPC 的優勢在於高效的網絡傳輸模型(常使用 NIO 來實現),以及針對服務調用場景專門設計協議和高效的序列化技術。而 HTTP 的優勢在於它的成熟穩定、使用實現簡單、被廣泛支持、兼容性良好、防火墻友好、消息的可讀性高。所以在開放 API、跨平臺的服務間調用、對性能要求不苛刻的場景中有著廣泛的使用。

正式因為 HTTP 存在著很廣泛的應用場景,所以選擇一個優秀的 HTTP Client 便是十分重要的。

優秀的 HTTP Client 需要具備的特性

  • 連接池
  • 超時時間設置(連接超時、讀取超時等)
  • 是否支持異步
  • 請求和響應的編解碼
  • 可擴展性

連接池

因為目前 HTTP 1.1 不支持多路復用,隻有 HTTP Pipeline 這用半復用的模型支持。所以,在需要頻繁發送消息的場景中,連接池使必須支持的,以減少頻繁建立連接所帶來的不必要的性能損耗。

超時時間設置(連接超時、讀取超時等)

當對端出現問題的時候,長時間的,甚至是無限的超時等待是絕對不能接受的。所以必須必須能夠設置超時時間。

是否支持異步

HTTP 相關技術(服務器端和客戶端)通常被人認為是性能低下的一個重要原因在於,在很長一段時間裡,HTTP 的相關實現缺乏對異步的支持。這不僅指非阻塞 IO,也包括異步的編程模型。缺乏異步編程模型的後果就是,即便 HTTP 協議棧是基於非阻塞 IO 實現的,調用客戶端的或者在服務端處理消息的線程有大量時間被浪費在瞭等待 IO 上面。所以,異步是非常重要的特性。

請求和響應的編解碼

通常,開發人員希望面向對象使用各種服務(這裡面自然也包括基於 HTTP 協議的服務),而不是直接面對原始的消息和響應開發。所以,透明地將 HTTP 請求和響應進行編解碼是十分有必要,因為這可以很大程度地降低開發人員的工作量。

可擴展性

不論一個框架設計的多好,總有一些特殊場景是它們無法原生支持的。這時可擴展性的好壞便體現出來瞭。

答案

基於上述幾點的考慮,RestTemplate 是相對好的選擇。原因在於 RestTemplate 本身基於成熟的 HTTP Client 實現(Apache HttpClient、OkHttp 等),並可以靈活地在這些實現中切換,而且具有良好的擴展性。最重要的是提供瞭前面幾個 HTTP Client 不具備的消息編解碼能力。

這裡要提一句為什麼沒有自己封裝 HTTP Client 的原因。這個原因在於想要基於一種 HTTP Client 去提供消息編解碼能力和一定的擴展能力並不難,但是如果要設計出一個通用的,對底層實現透明的,具有優秀如 Spring 的擴展性設計的框架並不是一件容易事。這裡的不易並不在於技術有多高深,而是在於優秀的擴展性設計往往源自從眾多優秀程序員、社區和軟件公司得到的豐富的一線實踐經驗,再由像 Spring 轉換為最終設計。這樣的產品不是一朝一夕就能得到的。在我們覺得自己打造自己的工具之前,我們可以先深入瞭解現有的優秀功能都能做到什麼。

二、使用 RestTemplate 的缺點

欲揚先抑,我們先來看加入使用 RestTemplate,可能會遇到哪些“坑”。

依賴 Spring 其它模塊

雖然 spring-web 模塊對其它 Spring 模塊並沒有顯式的依賴(Maven dependency 的 scope 為 compile),但是對於一些功能,比如異步版本的 RestTemplate,要求必須有 4.1 以上版本的 spring-core 模塊。

所以,要想 RestTemplate 完全發揮其功能,最好能有相近版本的其它的 Spring 模塊相配合(spring-core、spring-context、spring-beans、spring-aop)

默認情況下 RestTemplate 存在的不足

Spring Web 模塊中的 RestTemplate 是一個很不錯的面向 RESTful Web 服務的客戶端。它提供瞭很多簡化對 RESTful Web 服務調用的功能,例如 Path Parameter 的格式化功能(/hotels/{hotel_id}/books/{book_id},這裡的 hotel_id 和 book_id 就是 Path Paramter)、JSON 或 XML 等格式的數據與實體類之間的透明轉換等。

所謂默認情況指的是不去擴展 RestTemplate 所提供的類或接口,而是完全依賴其本身提供的代碼。在這種情況下,RestTemplate 還是有一些不便的地方。例如,它的 Path Parameter 格式化功能,對於普通 HTTP 服務的調用來說,反而成為瞭一個缺點,因為普通的 HTTP 服務的 GET 方法常使用 Query Parameter,而不是 Path Parameter。Query Paramter 的形式是 an_http_url?name1=value1&name2=value2。例如 getOrder.action?order_code=xxx。如果使用 RestTemplate,作為參數傳遞給 RestTemplate 的 URL 就必須是 getOrder.action?order_code={order_code}。如果是固定的參數還好,如果一個 HTTP 服務的 Query Parameter 是可變的,那就很不方便瞭。

三、擴展 RestTemplate

註意,下面涉及到的代碼都是基於 spring-web 4.2.6.RELEASE 版本

設置 Query Params

上面提到,RestTemplate 的 getForEntity、getForObject、postForEntity 等方法中的 Map 參數是 uriVariables,即我們常說的 Path Param,而非 Query Param(這兩個參數的定義可以參照 JAX-RS 中 @PathParam 和 @QueryParam 的定義)。

Path Param 是 URL 的一部分,RESTful 的 Web Service 會按照其定義的 URL Template 從 URL 中解析出其對應的值

RestTemplate 的這種機制面對 RESTful 的 Web Service 無疑是方便的,但很多情況下我們還是希望 RestTemplate 能夠在開發人員不用編寫額外代碼的情況下將 Map 類型的參數當做 Query Param 發送給對端的服務。

幸好來自 Spring 大傢庭的 RestTemplate 也具有良好的可擴展性,其具有一個名為 UriTemplateHandler 擴展點。因為不論是 Path Param 還是 Query Param,它們都是 URI 的一部分,所以隻需實現自定義的 URI 生成機制即可解決這個問題。

通過擴展 DefaultUriTemplateHandler,我們可以將 Map<String, ?> uriVariables 也作為 Query Param。具體實現如下:

public class QueryParamsUrlTemplateHandler extends DefaultUriTemplateHandler {
    @Override
    public URI expand(String uriTemplate, Map<String, ?> uriVariables) {
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(uriTemplate);
        for (Map.Entry<String, ?> varEntry : uriVariables.entrySet()) {
            uriComponentsBuilder.queryParam(varEntry.getKey(), varEntry.getValue());
        }
        uriTemplate = uriComponentsBuilder.build().toUriString();
        return super.expand(uriTemplate, uriVariables);
    }
}

上面的實現基於 DefaultUriTemplateHandler,所以保有瞭原來設置 Path Param 的功能。

設置自定義的 HTTP Header

實現這個需求有多種方法,比如通過攔截器。這裡使用另一個方法,通過一個自定義的 ClientHttpRequestFactory

public class CustomHeadersClientHttpRequestFactoryWrapper extends AbstractClientHttpRequestFactoryWrapper {
    private HttpHeaders customHeaders = new HttpHeaders();
    /**
     * Create a {@code AbstractClientHttpRequestFactoryWrapper} wrapping the given request factory.
     *
     * @param requestFactory the request factory to be wrapped
     */
    protected CustomHeadersClientHttpRequestFactoryWrapper(ClientHttpRequestFactory requestFactory) {
        super(requestFactory);
    }
    @Override
    protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod,
            ClientHttpRequestFactory requestFactory) throws IOException {
        ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod);
        for (Map.Entry<String, List<String>> headerEntry : customHeaders.entrySet()) {
            request.getHeaders().put(headerEntry.getKey(), headerEntry.getValue());
        }
        return request;
    }
    public void addHeader(String header, String... values) {
        customHeaders.put(header, Arrays.asList(values));
    }
}

簡化配置

RestTemplate 提供瞭良好的擴展性,但是有些設置是使用 “

四、RestTemplate 原理解析

HTTP Client 實現

RestTemplate 本身並沒有做 HTTP 底層的實現,而是利用瞭現有的技術,如 JDK 或 Apache HttpClient 等。

RestTemplate 需要使用一個實現瞭 ClientHttpRequestFactory 接口的類為其提供 ClientHttpRequest 實現(另外還有 AsyncClientHttpRequestFactory 對應於異步 HTTP 實現,這裡暫且不表)。而 ClientHttpRequest 則實現封裝瞭組裝、發送 HTTP 消息,以及解析響應的的底層細節。

目前(4.2.6.RELEASE)的 RestTemplate 主要有四種 ClientHttpRequestFactory 的實現,它們分別是:

  • 基於 JDK HttpURLConnection 的 SimpleClientHttpRequestFactory
  • 基於 Apache HttpComponents Client 的 HttpComponentsClientHttpRequestFactory
  • 基於 OkHttp 2(OkHttp 最新版本為 3,有較大改動,包名有變動,不和老版本兼容)的 OkHttpClientHttpRequestFactory
  • 基於 Netty4 的 Netty4ClientHttpRequestFactory

另外,還有用於提供攔截器功能的 InterceptingClientHttpRequestFactory。

寫消息

寫消息指的是 requestBody 轉換為某一種格式,如 JSON、XML 的數據的過程。

spring-web 模塊提供瞭一個 HttpMessageConverter 接口,用來讀寫 HTTP 消息。這個接口不僅被 RestTemplate 使用,也被 Spring MVC 所使用。

spring-web 模塊提供瞭基於 Jackson、GSON 等類庫的 HttpMessageConverter,用於進行 JSON 或 XML 格式數據的轉換。

RestTemplate 在發送消息時,會根據消息的 ContentType 或者 RequestBody 對象本身的一些屬性判斷究竟是使用哪個 HttpMessageConverter 寫消息。

具體來說,如果 RequestBody 是一個 HttpEntity 的話,會從中讀取 ContentType 屬性。同時,RequestBody 對象本身也會覺得一個 HttpMessageConverter 是否會處理這個對象。例如,ProtobufHttpMessageConverter 會要求 RequestBody 對象必須實現 com.google.protobuf.Message 接口。

讀消息

讀消息指的是讀取 HTTP Response 中的數據,轉換為用戶指定的格式(通過 Class<T> responseType 參數指定)。類似於寫消息的處理,讀消息的處理也是通過 ContentType 和 responseType 來選擇的相應 HttpMessageConverter 來進行的。

錯誤處理

RestTemplate 提供瞭一個 ResponseErrorHandler 的接口,用來處理錯誤的 Response。可以通過設置自定義的 ResponseErrorHandler 來實現擴展。

後記

根據我上面表達的思想,一個統一、規范和簡化 RestTemplate 使用的工具已經產生,不過暫時由於其代碼是公司項目的一部分,所以暫時不便公開。而且我希望是在這個工具經過瞭更多的實踐考驗之後再貢獻出來會更好。

目前的一個完整使用案例如下:

@Configuration
public class SpringConfigurationDemo {
    @Bean
    public RestTemplate myRestTemplate() {
        return RestTemplateBuilder.create()
                .withClientKey("myRestTemplate")
                .implementation(HttpClientImplementation.OK_HTTP)
                .clearMessageConverters()
                .setMessageConverter(new MappingJackson2HttpMessageConverter(), MediaType.TEXT_PLAIN)
                .enableAutoQueryParams()
                .connectTimeout(100)
                .readTimeout(200)
                .header(HttpHeaders.USER_AGENT, "MyAgent")
                .build();
    }
}

雖然 RestTemplate 是一個很不錯的 HTTP Client,但 Netflix 已經開源瞭一個更好地 HTTP Client 工具 – Feign。它是一個聲明式的 HTTP Client,在易用性、可讀性等方面大幅領先於現有的工具。我打算稍後寫一篇文章分析 Feign 的思想、原理和優點(原理其實不復雜,但是能想到這麼做的卻沒幾個,原創的創新思想永遠是最可貴的)

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: