2016年6月12日:從 Spring 4.3 開始加入了 OkHttp3ClientHttpRequestFactory
java
本文主要介紹 Spring Web 模塊中的 RestTemplate 組件的原理、優缺點、以及如何擴展以知足各類需求。程序員
在介紹 RestTemplate 以前,咱們先來談談 HTTP Client,談談選擇一個優秀的 HTTP Client 實現的的重要性,以及一個優秀的 HTTP Client 應該具有哪些特性。web
在 Java 社區中,HTTP Client 主要有 JDK 的 HttpURLConnection、Apache Commons HttpClient(或被稱爲 Apache HttpClient 3.x)、Apache HttpComponents Client(或被稱爲 Apache HttpClient 4.x)、Square 公司開源的 OkHttp。spring
除了這幾個純粹的 HTTP Client 類庫之外,還有 Spring 的 RestTemplate、Square 公司的 Retrofit、Netflix 公司的 Feign,以及像 Apache CXF 中的 client 組件。這些框架和類庫主要是針對 Web Service 場景,尤爲是 RESTful Web Service。它們每每是基於前面提到的 HTTP Client 實現,並在其基礎上提供了消息轉換、參數映射等對於 Web Service 來講十分必要的功能。編程
(固然,像 Netty、Mina 這樣的網絡 IO 框架,實現 HTTP 天然也再也不話下,但這些框架一般過於底層,不會被直接使用)服務器
雖然如今服務間的調用愈來愈多地使用了 RPC 和消息隊列,可是 HTTP 依然有適合它的場景。網絡
RPC 的優點在於高效的網絡傳輸模型(常使用 NIO 來實現),以及針對服務調用場景專門設計協議和高效的序列化技術。而 HTTP 的優點在於它的成熟穩定、使用實現簡單、被普遍支持、兼容性良好、防火牆友好、消息的可讀性高。因此在開放 API、跨平臺的服務間調用、對性能要求不苛刻的場景中有着普遍的使用。app
正式由於 HTTP 存在着很普遍的應用場景,因此選擇一個優秀的 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,可能會遇到哪些「坑」。
雖然 spring-web 模塊對其它 Spring 模塊並無顯式的依賴(Maven dependency 的 scope 爲 compile),可是對於一些功能,好比異步版本的 RestTemplate,要求必須有 4.1 以上版本的 spring-core 模塊。
因此,要想 RestTemplate 徹底發揮其功能,最好能有相近版本的其它的 Spring 模塊相配合(spring-core、spring-context、spring-beans、spring-aop)
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 是可變的,那就很不方便了。
注意,下面涉及到的代碼都是基於 spring-web 4.2.6.RELEASE 版本
上面提到,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 的功能。
實現這個需求有多種方法,好比經過攔截器。這裏使用另外一個方法,經過一個自定義的 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 底層的實現,而是利用了現有的技術,如 JDK 或 Apache HttpClient 等。
RestTemplate 須要使用一個實現了 ClientHttpRequestFactory
接口的類爲其提供 ClientHttpRequest
實現(另外還有 AsyncClientHttpRequestFactory
對應於異步 HTTP 實現,這裏暫且不表)。而 ClientHttpRequest
則實現封裝了組裝、發送 HTTP 消息,以及解析響應的的底層細節。
目前(4.2.6.RELEASE)的 RestTemplate 主要有四種 ClientHttpRequestFactory
的實現,它們分別是:
HttpURLConnection
的 SimpleClientHttpRequestFactory
HttpComponentsClientHttpRequestFactory
OkHttpClientHttpRequestFactory
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
來實現擴展。
根據我上面表達的思想,一個統1、規範和簡化 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 的思想、原理和優勢(原理其實不復雜,可是能想到這麼作的卻沒幾個,原創的創新思想永遠是最難得的)