每篇一句
人圓月圓心圓,人和家和國和---中秋節快樂
前言
在閱讀本篇之前,建議先閱讀開山篇效果更佳。RestTemplate
是Spring提供的用於訪問Rest
服務的客戶端工具,它提供了多種便捷訪問遠程Http服務的方法,能夠大大提高客戶端的編寫效率。
弱弱呼吁一句:對於那些在Spring
環境下還在使用HttpClient
(或其它Client)的同學,今兒看完本文后,建議切換到RestTemplate
(有特殊需求的當然除外嘍~)。
RestTemplate
簡化了與http服務的通信,程序代碼可以給它提供URL,並提取結果。它默認使用的JDK 的HttpURLConnection
進行通信,然而我們是可以通過RestTemplate.setRequestFactory
切換到不同的HTTP源:如Apache HttpComponents
、Netty
、OkHttp
等等。
RestOperations
指定一組基本restful操作的接口,定義了基本的Rest操作集合,它的唯一實現是RestTemplate
;不直接使用,但這是增強可測試性的一個有用選項,因為它很容易被模擬或存根(后面這句話請好好理解)。
可以對比參照
RedisOperations
,它的實現類也只有RedisTemplate
一個。他倆都采用了設計模式中的模板模式
方法們:
由於此接口里的方法實在太多了(40+個),因此我按照Http標准進行分類如下表格:
// @since 3.0
public enum HttpMethod {
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
...
}
HttpMethod | 方法 |
---|---|
GET | ![]() |
HEAD | ![]() |
POST | ![]() |
PUT | ![]() |
PATCH | ![]() |
DELETE | ![]() |
OPTIONS | ![]() |
TRACE | 無 |
any(執行任何Http方法) | ![]() |
觀察發現,雖然方法眾多但有很強的規律可循。每個方法都有三種重載實現:2種的url參數為字符串,一種URI參數,所以掌握規律后再使用,就不用害怕它的多而不知咋使用了。
xxxForObject:返回響應體(也就直接是body體力的內容) (T)
xxxForEntity:返回的相應行、響應頭、響應碼、響應體等等 (ResponseEntity)
xxxForLocation:提交成功之后,返回新資源的URI。這個只需要服務提供者返回一個 URI 即可,該 URI 表示新資源的位置,可謂非常輕量。 (URI)
注意:使用字符串類型的url默認會對url進行轉義,如http://example.com/hotel list
在執行時會轉義為http://example.com/hotel%20list
,隱式的轉義這樣是沒有問題的。但如果你自己已經轉義過了,那就不ok了。
若不想要這種隱式的轉義,建議使用URI(URI uri = uriComponents.toUri()
)來構造。
RestTemplate
中POST請求的三種方式
post
請求代表新建/創建一個資源,所以它是有返回值的。因為它的使用最為復雜,因此本文以它為例進行講解。
你如果熟練使用過瀏覽器的開發者工具
調試過,你肯定知道POST
請求它傳參是有兩種方式的:
- Form Data方式:我們用from表單提交的方式就是它;使用ajax(注意:這里指的是jQuery的ajax,而不是源生js的)默認的提交方式也是它~
- request payload方式:多部分方式/json方式
這兩種方式是通過Content-Type
來區別的:若是application/x-www-form-urlencoded
那就是formdata
方式;若是application/json
或者multipart/form-data
等方式那就是request payload
方式
jQuery
在執行post請求時,默認會給你設置Content-Type
為application/x-www-form-urlencoded
,所以服務器能夠正確解析。
若使用js原生的ajax,如果不顯示的
設置Content-Type,那么默認是text/plain,這時服務器就不知道怎么解析數據了,所以才只能通過獲取原始數據流的方式來進行解析請求數據。(相信沒人這么干吧~)
exchange和execute方法:
exchange方法:更通用的請求方法。它入參必須接受一個RequestEntity
,從而可以設置請求的路徑、頭等等信息,最終全都是返回一個ResponseEntity
(可以發送Get、Post、Put等所有請求)。
execute方法:最最最底層、通用的請求方法。
RequestCallback:用於操作請求頭和body,在請求發出
前
執行;ResponseExtractor:解析/提取HTTP響應的數據,而且不需要擔心異常和資源的關閉
RequestCallback.doWithRequest(ClientHttpRequest)
說白了就是拿到ClientHttpRequest
后對他進行繼續處理~
RestTemplate
的acceptHeaderRequestCallback、httpEntityCallback
這些方法可以設置它~
HttpAccessor、InterceptingHttpAccessor
這兩個抽象類不容忽視,HystrixCommand和Ribbon
的邏輯都和它有關系(攔截器)。
HttpAccessor
是個抽象基類,它定義要操作ClientHttpRequestFactory
的公共屬性,它一般不直接使用。
// @since 3.0
public abstract class HttpAccessor {
// RestTemplate默認使用的客戶端工廠:基於源生JDK
private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
// 若要切換成三方庫的底層組件,設置此方法便可
public void setRequestFactory(ClientHttpRequestFactory requestFactory) {
this.requestFactory = requestFactory;
}
... // get方法
// 供給子類非常方便的拿到一個ClientHttpRequest
protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
ClientHttpRequest request = getRequestFactory().createRequest(url, method);
return request;
}
}
它的子類是:InterceptingHttpAccessor
,也還是個抽象實現,主要是管理起了請求的攔截器們:ClientHttpRequestInterceptor
。
InterceptingHttpAccessor
// @since 3.0
// @see InterceptingClientHttpRequestFactory
public abstract class InterceptingHttpAccessor extends HttpAccessor {
// 裝載需要作用在RestTemplate上的攔截器們~~~
private final List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
@Nullable
private volatile ClientHttpRequestFactory interceptingRequestFactory;
// 這里語意是set,所以是完全的替換掉(支持ordered排序哦~~~)
public void setInterceptors(List<ClientHttpRequestInterceptor> interceptors) {
if (this.interceptors != interceptors) {
this.interceptors.clear();
this.interceptors.addAll(interceptors);
AnnotationAwareOrderComparator.sort(this.interceptors);
}
}
// 復寫了父類的這個方法很有意思
// 意思為:若你調用者手動set進來了,那就以調用者設置的工廠為准 否則使用的是InterceptingClientHttpRequestFactory
@Override
public void setRequestFactory(ClientHttpRequestFactory requestFactory) {
super.setRequestFactory(requestFactory);
this.interceptingRequestFactory = null;
}
// 若配置了攔截器,那么默認就使用InterceptingClientHttpRequestFactory,而不再是SimpleClientHttpRequestFactory了~~~
@Override
public ClientHttpRequestFactory getRequestFactory() {
List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
if (!CollectionUtils.isEmpty(interceptors)) {
ClientHttpRequestFactory factory = this.interceptingRequestFactory;
if (factory == null) {
factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
this.interceptingRequestFactory = factory;
}
return factory;
} else {
return super.getRequestFactory();
}
}
}
InterceptingHttpAccessor
最主要的處理邏輯為:若發現調用者設置了請求攔截器,那么它創建的工廠是具有攔截功能的InterceptingClientHttpRequestFactory
,否則就是默認的SimpleClientHttpRequestFactory
。
InterceptingClientHttpRequestFactory
工廠它產生的ClientHttpRequest
是InterceptingClientHttpRequest
,然而它就會執行攔截器的攔截方法嘍:nextInterceptor.intercept(request, body, this)
提問:如有配置有多個請求攔截器,都會執行嗎?
解答:這個千萬不要犯迷糊和輕易下結論:以為沒有迭代它(for循環)而只是iterator.next()
就以為若有多個就只會執行一個,那就大錯特錯了。這里實際是形成了一個執行鏈條,只要攔截器的intercept
方法內最終還調用執行器的intercept()
方法,那么攔截器鏈就會一直執行下去。其根本緣由是第三個參數傳入的是this
,至始至終都是同一個執行器(this=InterceptingRequestExecution
)
RestTemplate
RestTemplate
采用同步方式執行 HTTP 請求的類,底層默認使用JDK
原生 HttpURLConnection API
。它實現了接口RestOperations
,提供了非常多的模版方法(重載方法)讓開發者能更簡單地發送 HTTP 請求。
需要注意的是,RestTemplate
是Spring 3.0
就有了,但在Spring5.0后,Spring官方是推薦使用org.springframework.web.reactive.function.client.WebClient
替代它,特別是對於異步的場景。
RestTemplate
因為使用極其廣泛,so即使到了Spring 5.0,官方只是建議替代,但並沒有標注@Deprecated
,因此至少目前你還可以想咋用就咋用吧。
但是AsyncRestTemplate
是明確標注了@Deprecated
,強烈建議使用org.springframework.web.reactive.function.client.WebClient
去代替,所以在5.0后不建議再使用它了~。
當然還需要說明一點:若你的項目中沒有使用到WebFlux
的技術棧來處理請求,那么也沒必要說為了使用而使用,所以沒必要專門為了它而導包(個人建議)~
// @since 3.0
public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {
// 去classpath探測 是否有這些消息轉換器相關的jar~
// 一般情況下我們都會導jackson2Present~~~
private static boolean romePresent;
private static final boolean jaxb2Present;
private static final boolean jackson2Present;
private static final boolean jackson2XmlPresent;
private static final boolean jackson2SmilePresent;
private static final boolean jackson2CborPresent;
private static final boolean gsonPresent;
private static final boolean jsonbPresent;
...
// 下面四個變量很重要:
// 消息轉換器們(顯然對JSON格式默認是支持得最好的)
private final List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
// 默認的請求異常處理器,Spring5.0后其實可以使用它ExtractingResponseErrorHandler
// 它能夠利用消息換換氣提取你的錯誤內容。並且還支持自定義錯誤碼、錯誤序列等等~
private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler();
// 用於URL的構建
private UriTemplateHandler uriTemplateHandler;
// 默認的返回值提取器~~~~
private final ResponseExtractor<HttpHeaders> headersExtractor = new HeadersExtractor();
// 空構造,應該是平時使用得最多的了:一切都使用默認的組件配置Resource等等
public RestTemplate() {
// 這個幾個消息轉換器是支持的。字節數組、字符串、
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
this.messageConverters.add(new ResourceHttpMessageConverter(false));
this.messageConverters.add(new SourceHttpMessageConverter<>());
// 對form表單提交方式的支持
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
// 接下里便是一些列的判斷,若類路徑上有才會加進來
if (jackson2Present) {
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
}
...
// new DefaultUriBuilderFactory()
this.uriTemplateHandler = initUriTemplateHandler();
}
// 你懂的,若想用OkHttp,也可以在構造時就指定
public RestTemplate(ClientHttpRequestFactory requestFactory) {
this();
setRequestFactory(requestFactory);
}
// 若不想用默認的消息轉換器,也可以自己指定(其實一般都不這么去干,而是后面自己再add進來)
public RestTemplate(List<HttpMessageConverter<?>> messageConverters) {
Assert.notEmpty(messageConverters, "At least one HttpMessageConverter required");
this.messageConverters.addAll(messageConverters);
this.uriTemplateHandler = initUriTemplateHandler();
}
... // 省略上面屬性的get/set犯法們
}
這部分源碼我列出來,都是在對構建一個RestTemplate
實例的准備工作相關方法,包括對各個相關組件的設置。
接下來更重要的便是它實現的接口方法了,我抽出一些關鍵點進行描述說明:
RestTemplate:
@Override
@Nullable
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
//1、new AcceptHeaderRequestCallback(responseType) 它能在發送請求的之前這樣一件事:
// request.getHeaders().setAccept(allSupportedMediaTypes)
RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
// 最終調用的是execute方法,此時URL是個字符串
// responseExtractor返回值提取器使用的是消息轉換器去讀取body噠~
// 返回值就是返回的body本身(不含有返回的響應頭等等信息~)
return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);
}
// 它返回的是ResponseEntity,不會返回null的 最終調用的依舊是execute方法
// 此時候用的就不是消息轉換器的提取器了,而是內部類`ResponseEntityResponseExtractor`(底層還是依賴消息轉換器)
// 但是這個提取器,提取出來的可都是ResponseEntity<T>實例~
@Override
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
return nonNull(execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables));
}
// HEAD請求:很簡單,使用的提取器就是headersExtractor,從返回值里把響應header拿出來即可
@Override
public HttpHeaders headForHeaders(String url, Object... uriVariables) throws RestClientException {
return nonNull(execute(url, HttpMethod.HEAD, null, headersExtractor(), uriVariables));
}
// POST請求
@Override
@Nullable
public URI postForLocation(String url, @Nullable Object request, Object... uriVariables) throws RestClientException {
// 1、HttpEntityRequestCallback 適配:把request適配成一個HttpEntity
// 然后執行前,通過消息轉換器把頭信息、body信息等等都write進去
RequestCallback requestCallback = httpEntityCallback(request);
// 因為需要拿到URI,所以此處使用headersExtractor提取器先拿到響應的header即可~~~
HttpHeaders headers = execute(url, HttpMethod.POST, requestCallback, headersExtractor(), uriVariables);
return (headers != null ? headers.getLocation() : null);
}
// 除了httpEntityCallback()不一樣,其余和get請求一樣
@Override
@Nullable
public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables) throws RestClientException {
RequestCallback requestCallback = httpEntityCallback(request, responseType);
HttpMessageConverterExtractor<T> responseExtractor =
new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
}
// PUT請求:因為沒有返回值,所以不需要返回值提取器。所以,非常的簡單~~~
@Override
public void put(String url, @Nullable Object request, Object... uriVariables) throws RestClientException {
RequestCallback requestCallback = httpEntityCallback(request);
execute(url, HttpMethod.PUT, requestCallback, null, uriVariables);
}
// DELETE請求:也是木有返回值的。
// 並且請注意:DELETE請求這里可都是不能接收body的,不能給請求設置請求體的
// (雖然可能底層httpCLient支持,但這里不支持,請遵守規范)
@Override
public void delete(String url, Object... uriVariables) throws RestClientException {
execute(url, HttpMethod.DELETE, null, null, uriVariables);
}
// OPTIONS請求:和HEAD請求的處理邏輯幾乎一樣
@Override
public Set<HttpMethod> optionsForAllow(String url, Object... uriVariables) throws RestClientException {
ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor();
HttpHeaders headers = execute(url, HttpMethod.OPTIONS, null, headersExtractor, uriVariables);
return (headers != null ? headers.getAllow() : Collections.emptySet());
}
所有方法大體執行邏輯一致,都是和RequestCallback
、responseExtractor
等有關,且最終都是委托給了最為底層的execute()
方法去執行。
你是否疑問:它提供的put方法返回值都是void,若我put請求就有返回值腫么辦呢?那么接下來就介紹更為通用的一個方法:exchange()
RestTemplate:
@Override
public <T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {
// 把請求體適配為HttpEntity
RequestCallback requestCallback = httpEntityCallback(requestEntity, responseType);
// 消息提取器使用ResponseEntityResponseExtractor
ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
// 從上兩個部分就能看到:exchange方法的入參、出參都是非常通用的~~~
return nonNull(execute(url, method, requestCallback, responseExtractor, uriVariables));
}
// ParameterizedTypeReference參數化類型,用於處理泛型
// 上面的responseType就是個Class。這里是個參數化類型~~~~~
@Override
public <T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType, Object... uriVariables) throws RestClientException {
Type type = responseType.getType();
RequestCallback requestCallback = httpEntityCallback(requestEntity, type);
ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(type);
return nonNull(execute(url, method, requestCallback, responseExtractor, uriVariables));
}
// 這個方法就非常精簡了,讓調用者自己去構造RequestEntity,里面是包含了請求的URL和方法等信息的
@Override
public <T> ResponseEntity<T> exchange(RequestEntity<?> requestEntity, Class<T> responseType) throws RestClientException {
RequestCallback requestCallback = httpEntityCallback(requestEntity, responseType);
ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
return nonNull(doExecute(requestEntity.getUrl(), requestEntity.getMethod(), requestCallback, responseExtractor));
}
exchange
所有方法使用的都是HttpEntity
和ResponseEntity
代表請求實體和響應實體,足以見到它設計的通用性。
在Spring3.2后提供了
ParameterizedTypeReference
來處理參數化類型---> 主要是為了處理List等的泛型
可以發現即使是exchange()
方法,最終還是委托給execute/doExecute
去執行的:
RestTemplate:
// 3個execute方法。最終調用的都是doExecute方法
// 它做的一件事:使用UriTemplateHandler把URL的參數填進去~~~
// 底層使用的是我上文介紹的`UriComponentsBuilder`,還是比較簡單的
@Override
@Nullable
public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {
URI expanded = getUriTemplateHandler().expand(url, uriVariables);
return doExecute(expanded, method, requestCallback, responseExtractor);
}
doExecute方法:
@Nullable
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
ClientHttpResponse response = null;
ClientHttpRequest request = createRequest(url, method);
// 如果有回調,那就先回調處理一下子請求
if (requestCallback != null) {
requestCallback.doWithRequest(request);
}
// 真正意義上的發送請求。
// 請注意:如果這里的request是`InterceptingClientHttpRequest`,那就回執行攔截器的intercept方法哦~~~
// 至於什么時候是InterceptingClientHttpRequest呢?這個上面有講的
response = request.execute();
// 處理結果(若有錯誤,那就拋出異常~~~)
handleResponse(url, method, response);
// 請求正常。那就使用返回值提取器responseExtractor提取出內容即可了~~~
return (responseExtractor != null ? responseExtractor.extractData(response) : null);
...
// 關閉響應(ClientHttpResponse繼承了Closeable接口)
finally {
if (response != null) {
response.close();
}
}
}
看完doExecute()
的模板式的實現步驟,就清楚了RestTemplate
從發出一個請求到收到一個響應的完整過程。Spring
設計了多個相關組件,提供鈎子程序讓我們可以干預到流程里面去,最常見的當然就是請求攔截器了,它在Ribbon負載均衡和Hystrix熔斷器里面有很好的應用~
AsyncRestTemplate
它是@since 4.0
新增的用於解決一些異步Http請求的場景,但它壽命比較短,在Spring5.0
就標記為@Deprecated
,而被推薦使用WebClient
去代替它。
它的實現基礎原理是:RestTemplate
+ SimpleAsyncTaskExecutor
任務池的方式去實現的異步請求,返回值均為ListenableFuture
。掌握了RestTemplate
后,它使用起來是沒有什么障礙的
極簡使用Demo Show
看過了原理的描述,我有理由相信你已經爛熟於胸並對RestTemplate
能夠運用自如了。因此關於使用方面,本文只給如下非常簡單的一個Demo Show我認為是夠了的:
public static void main(String[] args) throws IOException {
RestTemplate restTemplate = new RestTemplate();
String pageHtml = restTemplate.getForObject("http://www.baidu.com", String.class);
System.out.println(pageHtml); // 百度首頁的html...
}
解釋一點:這里請求得到的是一個html網頁,所以HttpMessageConverterExtractor
去提取響應時,使用的是StringHttpMessageConverter
去處理的,提取代碼如下:
StringHttpMessageConverter:
@Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
// 從響應頭的contentType里提取(若是application/json,那默認也是urf-8)
// 若沒有指定編碼,就取值getDefaultCharset。比如本處訪問百度,就取值默認值`ISO-8859-1`對body體進行編碼的~
Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
return StreamUtils.copyToString(inputMessage.getBody(), charset);
}
小伙伴把此請求案例可以和上面我使用ClientHttpRequestFactory
發送請求的案例對比(或者和你自己使用HttpClient
步驟對比),感受感受使用RestTemplate
是多么的優雅~
推薦閱讀
RestTemplate組件:ClientHttpRequestFactory、ClientHttpRequestInterceptor、ResponseExtractor【享學Spring MVC】
為何一個@LoadBalanced注解就能讓RestTemplate擁有負載均衡的能力?【享學Spring Cloud】
總結
微服務作為主流的今天,RestTemplate
可謂是一把利器,每個程序員都應該掌握它。深入理解它對實際應用、調優都有很現實的意義,所以我相信本文能夠幫助到你,做到爛熟於胸。
預告一下:下篇文章會原理分析告訴大家為何一個簡單的@LoadBalanced
注解就能讓RestTemplate
擁有負載均衡的能力?
== 若對Spring、SpringBoot、MyBatis等源碼分析感興趣,可加我wx:fsx641385712,手動邀請你入群一起飛 ==
== 若對Spring、SpringBoot、MyBatis等源碼分析感興趣,可加我wx:fsx641385712,手動邀請你入群一起飛 ==