RestTemplate的使用和原理你都爛熟於胸了嗎?【享學Spring MVC】

每篇一句

人圓月圓心圓,人和家和國和—中秋節快樂

前言

在閱讀本篇之前,建議先閱讀開山篇效果更佳。RestTemplate是Spring提供的用於訪問Rest服務的客戶端工具,它提供了多種便捷訪問遠程Http服務的方法,能夠大大提高客戶端的編寫效率
弱弱呼籲一句:對於那些在Spring環境下還在使用HttpClient(或其它Client)的同學,今兒看完本文後,建議切換到RestTemplate (有特殊需求的當然除外嘍~)。

RestTemplate簡化了與http服務的通信,程序代碼可以給它提供URL,並提取結果。它默認使用的JDK 的HttpURLConnection進行通信,然而我們是可以通過RestTemplate.setRequestFactory切換到不同的HTTP源:如Apache HttpComponentsNettyOkHttp等等。

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請求它傳參是有兩種方式的:

  1. Form Data方式:我們用from表單提交的方式就是它;使用ajax(注意:這裏指的是jQuery的ajax,而不是源生js的)默認的提交方式也是它~
    在這裏插入圖片描述
  2. request payload方式:多部分方式/json方式
    在這裏插入圖片描述
    在這裏插入圖片描述

這兩種方式是通過Content-Type來區別的:若是application/x-www-form-urlencoded那就是formdata方式;若是application/json或者multipart/form-data等方式那就是request payload方式

jQuery在執行post請求時,默認會給你設置Content-Typeapplication/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後對他進行繼續處理~
RestTemplateacceptHeaderRequestCallback、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工廠它產生的ClientHttpRequestInterceptingClientHttpRequest,然而它就會執行攔截器的攔截方法嘍:nextInterceptor.intercept(request, body, this)




RestTemplate

RestTemplate採用同步方式執行 HTTP 請求的類,底層默認使用JDK原生 HttpURLConnection API。它實現了接口RestOperations,提供了非常多的模版方法(重載方法)讓開發者能更簡單地發送 HTTP 請求。

需要注意的是,RestTemplateSpring 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());
	}

所有方法大體執行邏輯一致,都是和RequestCallbackresponseExtractor等有關,且最終都是委託給了最爲底層的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所有方法使用的都是HttpEntityResponseEntity代表請求實體和響應實體,足以見到它設計的通用性。

在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】

總結

微服務作爲主流的今天,RestTemplate可謂是一把利器,每個程序員都應該掌握它。深入理解它對實際應用、調優都有很現實的意義,所以我相信本文能夠幫助到你,做到爛熟於胸。
預告一下:下篇文章會原理分析告訴大家爲何一個簡單的@LoadBalanced註解就能讓RestTemplate擁有負載均衡的能力?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章