RestTemplate相關組件:ClientHttpRequestInterceptor【享學Spring MVC】


每篇一句

做事的人和做夢的人最大的區別就是行動力

前言

本文為深入了解Spring提供的Rest調用客戶端RestTemplate開山,對它相關的一些組件做講解。

Tips:請注意區分RestTemplateRedisTemplate哦~

ClientHttpRequestFactory

它是個函數式接口,用於根據URIHttpMethod創建出一個ClientHttpRequest來發送請求~

ClientHttpRequest它代表請求的客戶端,該接口繼承自HttpRequestHttpOutputMessage,只有一個ClientHttpResponse execute() throws IOException方法。其中Netty、HttpComponents、OkHttp3,HttpUrlConnection對它都有實現~

// @since 3.0  RestTemplate這個體系都是3.0后才有的
@FunctionalInterface
public interface ClientHttpRequestFactory {	

	// 返回一個ClientHttpRequest,這樣調用其execute()方法就可以發送rest請求了~
	ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException;
}

它的繼承樹如下:
在這里插入圖片描述
可以直觀的看到,我們可以使用ApacheHttpClientOkHttp3Netty4都可,但這些都需要額外導包,默認情況下Spring使用的是java.net.HttpURLConnection

HttpClient最新版本:4.5.10
OkHttp最新版本:4.1.1(雖然版本號是4,但是GAV還是3哦:com.squareup.okhttp3)
Netty最新版本:4.1.39.Final(它的5版本可以宣告已死)

Spring4.0是新增了一個對異步支持的AsyncClientHttpRequestFactory(Spring5.0后標記為已廢棄):

// 在Spring5.0后被標記為過時了,被org.springframework.http.client.reactive.ClientHttpConnector所取代(但還是可用的嘛)
@Deprecated
public interface AsyncClientHttpRequestFactory {

	// AsyncClientHttpRequest#executeAsync()返回的是ListenableFuture<ClientHttpResponse>
	// 可見它的異步是通過ListenableFuture實現的
	AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) throws IOException;
}

使用工廠創建ClientHttpRequest ,然后我們發請求就不用關心具體httpClient內部的細節了(可插拔使用二方庫、三方庫)

SimpleClientHttpRequestFactory

它是Spring內置默認的實現,使用的是JDK內置的java.net.URLConnection作為client客戶端。

public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory {

	private static final int DEFAULT_CHUNK_SIZE = 4096;
	@Nullable
	private Proxy proxy; //java.net.Proxy
	private boolean bufferRequestBody = true; // 默認會緩沖body
	
	// URLConnection's connect timeout (in milliseconds).
	// 若值設置為0,表示永不超時 @see URLConnection#setConnectTimeout(int)
	private int connectTimeout = -1;
	// URLConnection#setReadTimeout(int) 
	// 超時規則同上
	private int readTimeout = -1;
	
	//Set if the underlying URLConnection can be set to 'output streaming' mode.
	private boolean outputStreaming = true;

	// 異步的時候需要
	@Nullable
	private AsyncListenableTaskExecutor taskExecutor;
	... // 省略所有的set方法
	
	@Override
	public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
		
		// 打開一個HttpURLConnection
		HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
		// 設置超時時間、請求方法等一些參數到connection
		prepareConnection(connection, httpMethod.name());

		//SimpleBufferingClientHttpRequest的excute方法最終使用的是connection.connect();
		// 然后從connection中得到響應碼、響應體~~~
		if (this.bufferRequestBody) {
			return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
		} else {
			return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
		}
	}

	// createAsyncRequest()方法略,無非就是在線程池里異步完成請求
	...
}

需要注意的是:JDK <1.8 doesn't support getOutputStream with HTTP DELETE,也就是說如果JDK的版本低於1.8的話,那么Delete請求是不支持body體的。

Demo Show:

public static void main(String[] args) throws IOException {
    SimpleClientHttpRequestFactory clientFactory  = new SimpleClientHttpRequestFactory();
	
	// ConnectTimeout只有在網絡正常的情況下才有效,因此兩個一般都設置
    clientFactory.setConnectTimeout(5000); //建立連接的超時時間  5秒
    clientFactory.setReadTimeout(5000); // 傳遞數據的超時時間(在網絡抖動的情況下,這個參數很有用)

    ClientHttpRequest client = clientFactory.createRequest(URI.create("https://www.baidu.com"), HttpMethod.GET);
    // 發送請求
    ClientHttpResponse response = client.execute();
    System.out.println(response.getStatusCode()); //200 OK
    System.out.println(response.getStatusText()); // OK
    System.out.println(response.getHeaders()); //

    // 返回內容 是個InputStream
    byte[] bytes = FileCopyUtils.copyToByteArray(response.getBody());
    System.out.println(new String(bytes, StandardCharsets.UTF_8)); // 百度首頁內容的html
}

關於HttpURLConnection的API使用,需注意如下幾點:

  1. HttpURLConnection對象不能直接構造,需要通過URL類中的openConnection()方法來獲得
  2. HttpURLConnection的connect()函數,實際上只是建立了一個與服務器的TCP連接,並沒有實際發送HTTP請求。HTTP請求實際上直到我們獲取服務器響應數據(如調用getInputStream()、getResponseCode()等方法)時才正式發送出去
    1. 配置信息都需要在connect()方法執行之前完成
  3. HttpURLConnection是基於HTTP協議的,其底層通過socket通信實現。如果不設置超時(timeout),在網絡異常的情況下,可能會導致程序僵死而不繼續往下執行。請務必100%設置
  4. HTTP正文的內容是通過OutputStream流寫入的, 向流中寫入的數據不會立即發送到網絡,而是存在於內存緩沖區中,待流關閉時,根據寫入的內容生成HTTP正文
  5. 調用getInputStream()方法時,返回一個輸入流,用於從中讀取服務器對於HTTP請求的返回信息。
  6. HttpURLConnection.connect()不是必須的。當我們需要返回值時,比如我們使用HttpURLConnection.getInputStream()方法的時候它就會自動發送請求了,所以完全沒有必要調用connect()方法了(沒必要先建立Tcp嘛~)。

使用哪一個底層http庫?

我們知道HttpURLConnection它在功能上是有些不足的(簡單的提交參數可以滿足)。絕大部分情況下Web站點的網頁可能沒這么簡單,這些頁面並不是通過一個簡單的URL就可訪問的,可能需要用戶登錄而且具有相應的權限才可訪問該頁面。在這種情況下,就需要涉及Session、Cookie的處理了,如果打算使用HttpURLConnection來處理這些細節,當然也是可能實現的,只是處理起來難度就大了。

這個時候,Apache開源組織提供了一個HttpClient項目,可以用於發送HTTP請求,接收HTTP響應(包含HttpGet、HttpPost...等各種發送請求的對象)。

它不會緩存服務器的響應,不能執行HTML頁面中嵌入的Javascript代碼;也不會對頁面內容進行任何解析、處理

因此,下面我就讓Spring使用HttpClient為示例演示使用三方庫:
1、導包

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.10</version>
</dependency>

Tips:Requires Apache HttpComponents 4.3 or higher, as of Spring 4.0.

2、案例使用
案例內容僅僅只需把上例第一句話換成使用HttpComponentsClientHttpRequestFactory它的實例,其余都不用變化即可成功看到效果。可以看看這個類它具體做了什么

// @since 3.1 3.1后出現的。
public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean {

	private HttpClient httpClient;
	@Nullable
	private RequestConfig requestConfig; // 這個配置就是可以配置超時等等亂七八糟client屬性的類
	private boolean bufferRequestBody = true;

	//=========下面是構造函數們=========
	public HttpComponentsClientHttpRequestFactory() {
		// HttpClientBuilder.create().useSystemProperties().build();
		// 所有若是這里,配置超時時間可以這么來設置也可:
		// System.setProperty(”sun.net.client.defaultConnectTimeout”, “5000″);
		this.httpClient = HttpClients.createSystem();
	}
	// 當然可以把你配置好了的Client扔進來
	public HttpComponentsClientHttpRequestFactory(HttpClient httpClient) {
		this.httpClient = httpClient;
	}
	... // 省略設置超時時間。。。等等屬性的一些get/set
	// 超時信息啥的都是保存在`RequestConfig`里的


	@Override
	public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
		HttpClient client = getHttpClient(); // 拿到你指定的client)=(或者系統缺省的)
		// switch語句邏輯:HttpMethod == GET --> HttpGet HEAD --> HttpHead ...
		HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
		postProcessHttpRequest(httpRequest);
		...
	}
}

實際使用的是HttpClient完成的請求。另外OkHttp3ClientHttpRequestFactory使用的是okhttp3.OkHttpClient發送請求;Netty4ClientHttpRequestFactory使用的是io.netty.channel.EventLoopGroup。此處就不一一例舉了

Spring5.0以后,Netty4ClientHttpRequestFactory過期了,建議使用org.springframework.http.client.reactive.ReactorClientHttpConnector代替~


關於HttpURLConnectionHttpClientOkHttpClient的簡單比較:
  • HttpURLConnection
    - 優點:JDK內置支持,java的標准類
    - 缺點:API不夠友好,什么都沒封裝,用起來太原始,不方便(這其實有時候也算優點,原始就證明好控~)
  • HttpClient
    - 優點:功能強大,API友好,使用率夠高,幾乎成為了實際意義上的標准(相當於對HttpURLConnection的封裝)
    - 缺點:性能稍低(比HttpURLConnection低,但4.3后使用連接池進行了改善),API較臃腫,其實Android已經棄用了它~
  • OkHttpClient:新一代的Http訪問客戶端
    - 優點:一個專注於性能和易用性的HTTP客戶端(節約寬帶,Android推薦使用),它設計的首要目標就是高效。提供了最新的 HTTP 協議版本 HTTP/2 和 SPDY 的支持。如果 HTTP/2 和 SPDY 不可用,OkHttp 會使用連接池來復用連接以提高效率
    - 暫無。

在這里插入圖片描述
關於Apache HttpClientAndroid5.0之后已經廢棄使用它了(API太多,太重),推薦使用更輕量的HttpUrlConnection。(Java開發還是推薦用HttpClient

OkHttp優點較多:支持SPDY,可以合並多個到同一個主機的請求;OkHttp實現的諸多技術如:連接池,gziping,緩存等;OkHttp 處理了很多網絡疑難雜症:會從很多常用的連接問題中自動恢復。如果您的服務器配置了多個IP地址,當第一個IP連接失敗的時候,OkHttp會自動嘗試下一個IP;OkHttp是一個Java的HTTP+SPDY客戶端開發包,同時也支持Android。默認情況下,OKHttp會自動處理常見的網絡問題,像二次連接、SSL的握手問題。支持文件上傳、下載、cookie、session、https證書等幾乎所有功能。支持取消某個請求

綜上所述,不管是Java還是Android,我推薦的自然都是OkHttp(OkHttp使用Okio進行數據傳輸。都是Square公司自家的,Square公司還出了一個Retrofit庫配合OkHttp戰斗力翻倍)~~~

池化技術一般用於長連接,那么像Http這種適合連接池嗎?
HttpClient 4.3以后中使用了PoolingHttpClientConnectionManager連接池來管理持有連接,同一條TCP鏈路上,連接是可以復用的。HttpClient通過連接池的方式進行連接持久化(所以它這個連接池其實是tcp的連接池。它里面有一個很重要的概念:Route的概念,代表一條線路。比如baidu.com是一個route,163.com是一個route...)。

連接池:可能是http請求,也可能是https請求
加入池話技術,就不用每次發起請求都新建一個連接(每次連接握手三次,效率太低)


AbstractClientHttpRequestFactoryWrapper

對其它ClientHttpRequestFactory的一個包裝抽象類,它有如下兩個子類實現

InterceptingClientHttpRequestFactory(重要)

Interceptor攔截的概念,還是蠻重要的。它持有的ClientHttpRequestInterceptor對於我們若想要攔截發出去的請求非常之重要(比如全鏈路壓測中,可以使用它設置token之類的~)

// @since 3.1
public class InterceptingClientHttpRequestFactory extends AbstractClientHttpRequestFactoryWrapper {
	// 持有所有的請求攔截器
	private final List<ClientHttpRequestInterceptor> interceptors;

	public InterceptingClientHttpRequestFactory(ClientHttpRequestFactory requestFactory, @Nullable List<ClientHttpRequestInterceptor> interceptors) {
		super(requestFactory);
		// 攔截器只允許通過構造函數設置進來,並且並沒有提供get方法方法~
		this.interceptors = (interceptors != null ? interceptors : Collections.emptyList());
	}

	// 此處返回的是一個InterceptingClientHttpRequest,顯然它肯定是個ClientHttpRequest嘛~
	@Override
	protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) {
		return new InterceptingClientHttpRequest(requestFactory, this.interceptors, uri, httpMethod);
	}

}

InterceptingClientHttpRequestexecute()方法的特點是:若存在攔截器,交給給攔截器去執行發送請求return nextInterceptor.intercept(request, body, this),否則就自己上。


ClientHttpRequestInterceptor

關於請求攔截器,Spring MVC內置了兩個最基礎的實現
在這里插入圖片描述
BasicAuthorizationInterceptor

// @since 4.3.1  但在Spring5.1.1后推薦使用BasicAuthenticationInterceptor
@Deprecated
public class BasicAuthorizationInterceptor implements ClientHttpRequestInterceptor {
	
	private final String username;
	private final String password;
	
	// 注意:username不允許包含:這個字符,但是密碼是允許的
	public BasicAuthorizationInterceptor(@Nullable String username, @Nullable String password) {
		Assert.doesNotContain(username, ":", "Username must not contain a colon");
		this.username = (username != null ? username : "");
		this.password = (password != null ? password : "");
	}

	@Override
	public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
		// 用戶名密碼連接起來后,用Base64對字節碼進行編碼~
		String token = Base64Utils.encodeToString((this.username + ":" + this.password).getBytes(StandardCharsets.UTF_8));
	
		// 放進請求頭:key為`Authorization`  然后執行請求的發送
		request.getHeaders().add("Authorization", "Basic " + token);
		return execution.execute(request, body);
	}
}

這個攔截器木有對body有任何改動,只是把用戶名、密碼幫你放進了請求頭上。

需要注意的是:若你的header里已經存在了Authorization這個key,這里也不會覆蓋的,這會添加哦。但並不建議你有覆蓋現象~

BasicAuthenticationInterceptor
它是用來代替上類的。它使用標准的授權頭來處理,參考HttpHeaders#setBasicAuth、HttpHeaders#AUTHORIZATION

public class BasicAuthenticationInterceptor implements ClientHttpRequestInterceptor {
	private final String username;
	private final String password;
	// 編碼,一般不用指定
	@Nullable
	private final Charset charset;
	... // 構造函數略

	@Override
	public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {

		HttpHeaders headers = request.getHeaders();
		// 只有當請求里不包含`Authorization`這個key的時候,此處才會設置授權頭哦
		if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) {
			
			// 這個方法是@since 5.1之后才提供的~~~~~
			// 若不包含此key,就設置標准的授權頭(根據用戶名、密碼) 它內部也有這如下三步:
			
			// String credentialsString = username + ":" + password;
			// byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(charset));
			// String encodedCredentials = new String(encodedBytes, charset);
			
			// 注意:它內部最終還是調用set(AUTHORIZATION, "Basic " + encodedCredentials);這個方法的
			headers.setBasicAuth(this.username, this.password, this.charset);
		}
		return execution.execute(request, body);
	}
}

說明:這兩個請求攔截器雖是Spring提供,但默認都是沒有被"裝配"的,所親需要,請手動裝配~

BufferingClientHttpRequestFactory

包裝其它ClientHttpRequestFactory,使得具有緩存的能力。若開啟緩存功能(有開關可控),會使用BufferingClientHttpRequestWrapper包裝原來的ClientHttpRequest。這樣發送請求后得到的是BufferingClientHttpResponseWrapper響應。


ResponseErrorHandler

用於確定特定響應是否有錯誤的策略接口。

// @since 3.0
public interface ResponseErrorHandler {

	// response里是否有錯
	boolean hasError(ClientHttpResponse response) throws IOException;
	// 只有hasError = true時才會調用此方法
	void handleError(ClientHttpResponse response) throws IOException;
	 // @since 5.0
	default void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
		handleError(response);
	}
}

繼承樹如下:
在這里插入圖片描述

DefaultResponseErrorHandler

Spring對此策略接口的默認實現,RestTemplate默認使用的錯誤處理器就是它。

// @since 3.0
public class DefaultResponseErrorHandler implements ResponseErrorHandler {

	// 是否有錯誤是根據響應碼來的,所以請嚴格遵守響應碼的規范啊
	// 簡單的說4xx和5xx都會被認為有錯,否則是無錯的  參考:HttpStatus.Series
	@Override
	public boolean hasError(ClientHttpResponse response) throws IOException {
		int rawStatusCode = response.getRawStatusCode();
		HttpStatus statusCode = HttpStatus.resolve(rawStatusCode);
		return (statusCode != null ? hasError(statusCode) : hasError(rawStatusCode));
	}
	...
	// 處理錯誤
	@Override
	public void handleError(ClientHttpResponse response) throws IOException {
		HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
		if (statusCode == null) {
			throw new UnknownHttpStatusCodeException(response.getRawStatusCode(), response.getStatusText(), response.getHeaders(), getResponseBody(response), getCharset(response));
		}
		handleError(response, statusCode);
	}
	
	// protected方法,子類對它有復寫
	protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
		String statusText = response.getStatusText();
		HttpHeaders headers = response.getHeaders();
		byte[] body = getResponseBody(response); // 拿到body,把InputStream轉換為字節數組
		Charset charset = getCharset(response); // 注意這里的編碼,是從返回的contentType里拿的~~~
		
		// 分別針對於客戶端錯誤、服務端錯誤 包裝為HttpClientErrorException和HttpServerErrorException進行拋出
		// 異常內包含有狀態碼、狀態text、頭、body、編碼等等信息~~~~
		switch (statusCode.series()) {
			case CLIENT_ERROR:
				throw HttpClientErrorException.create(statusCode, statusText, headers, body, charset);
			case SERVER_ERROR:
				throw HttpServerErrorException.create(statusCode, statusText, headers, body, charset);
			default:
				throw new UnknownHttpStatusCodeException(statusCode.value(), statusText, headers, body, charset);
		}
	}
	...
}

到這里就可以給大家解釋一下,為何經常能看到客戶端錯誤,然后還有狀態碼+一串信息了,就是因為這兩個異常。


HttpClientErrorException:

public class HttpClientErrorException extends HttpStatusCodeException {
	...
	public static HttpClientErrorException create(
			HttpStatus statusCode, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) {

		switch (statusCode) {
			case BAD_REQUEST:
				return new HttpClientErrorException.BadRequest(statusText, headers, body, charset);
			case UNAUTHORIZED:
				return new HttpClientErrorException.Unauthorized(statusText, headers, body, charset);
			case FORBIDDEN:
				return new HttpClientErrorException.Forbidden(statusText, headers, body, charset);
			case NOT_FOUND:
				return new HttpClientErrorException.NotFound(statusText, headers, body, charset);
			case METHOD_NOT_ALLOWED:
				return new HttpClientErrorException.MethodNotAllowed(statusText, headers, body, charset);
			case NOT_ACCEPTABLE:
				return new HttpClientErrorException.NotAcceptable(statusText, headers, body, charset);
			case CONFLICT:
				return new HttpClientErrorException.Conflict(statusText, headers, body, charset);
			case GONE:
				return new HttpClientErrorException.Gone(statusText, headers, body, charset);
			case UNSUPPORTED_MEDIA_TYPE:
				return new HttpClientErrorException.UnsupportedMediaType(statusText, headers, body, charset);
			case TOO_MANY_REQUESTS:
				return new HttpClientErrorException.TooManyRequests(statusText, headers, body, charset);
			case UNPROCESSABLE_ENTITY:
				return new HttpClientErrorException.UnprocessableEntity(statusText, headers, body, charset);
			default:
				return new HttpClientErrorException(statusCode, statusText, headers, body, charset);
		}
	}
	...
}

它針對不同的狀態碼HttpStatus,創建了不同的類型進行返回,方便使用者控制,這在監控上還是蠻有意義的

BadRequest、Unauthorized、Forbidden...等等都是HttpClientErrorException的子類

HttpServerErrorException代碼類似,略~


ExtractingResponseErrorHandler

繼承自DefaultResponseErrorHandler。在RESTful大行其道的今天,Spring5.0開始提供了此類。它將http錯誤響應利用HttpMessageConverter轉換為對應的RestClientException

// @since 5.0 它出現得還是很晚的。繼承自DefaultResponseErrorHandler 
// 若你的RestTemplate想使用它,請調用RestTemplate#setErrorHandler(ResponseErrorHandler)設置即可
public class ExtractingResponseErrorHandler extends DefaultResponseErrorHandler {
	private List<HttpMessageConverter<?>> messageConverters = Collections.emptyList();
	
	// 對響應碼做緩存
	private final Map<HttpStatus, Class<? extends RestClientException>> statusMapping = new LinkedHashMap<>();
	private final Map<HttpStatus.Series, Class<? extends RestClientException>> seriesMapping = new LinkedHashMap<>();

	// 構造函數、set方法給上面兩個Map賦值。因為我們可以自己控制哪些狀態碼應該報錯,哪些不應該了~
	// 以及可以自定義:那個狀態碼拋我們自定義的異常,哪一系列狀態碼拋我們自定義的異常,這個十分的便於我們做監控
	... // 省略構造函數和set方法。。。


	// 增加緩存功能~~~  否則在交給父類
	@Override
	protected boolean hasError(HttpStatus statusCode) {
		if (this.statusMapping.containsKey(statusCode)) {
			return this.statusMapping.get(statusCode) != null;
		} else if (this.seriesMapping.containsKey(statusCode.series())) {
			return this.seriesMapping.get(statusCode.series()) != null;
		} else {
			return super.hasError(statusCode);
		}
	}

	// 這個它做的事:extract:提取
	@Override
	public void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
		if (this.statusMapping.containsKey(statusCode)) {
			extract(this.statusMapping.get(statusCode), response);
		} else if (this.seriesMapping.containsKey(statusCode.series())) {
			extract(this.seriesMapping.get(statusCode.series()), response);
		} else {
			super.handleError(response, statusCode);
		}
	}


	private void extract(@Nullable Class<? extends RestClientException> exceptionClass, ClientHttpResponse response) throws IOException {
		if (exceptionClass == null) {
			return;
		}

		// 這里使用到了ResponseExtractor返回值提取器,從返回值里提取內容(本文是提取異常)
		HttpMessageConverterExtractor<? extends RestClientException> extractor =
				new HttpMessageConverterExtractor<>(exceptionClass, this.messageConverters);
		RestClientException exception = extractor.extractData(response);
		if (exception != null) { // 若提取到了異常信息,拋出即可
			throw exception;
		}
	}
}

若你想定制請求異常的處理邏輯,你也是可以自定義這個接口的實現的,當然還是建議你通過繼承DefaultResponseErrorHandler來擴展~


ResponseExtractor

響應提取器:從Response中提取數據。RestTemplate請求完成后,都是通過它來從ClientHttpResponse提取出指定內容(比如請求頭、請求Body體等)~
在這里插入圖片描述
它的直接實現似乎只有HttpMessageConverterExtractor,當然它也是最為重要的一個實現,和HttpMessageConverter相關。
在解釋它之前,先看看這個:MessageBodyClientHttpResponseWrapper,它的特點:它不僅可以通過實際讀取輸入流來檢查響應是否有消息體,還可以檢查其長度是否為0(即空)

// @since 4.1.5  它是一個訪問權限是default的類,是對其它ClientHttpResponse的一個包裝
class MessageBodyClientHttpResponseWrapper implements ClientHttpResponse {
	private final ClientHttpResponse response;
	// java.io.PushbackInputStream
	@Nullable
	private PushbackInputStream pushbackInputStream;
	
	// 判斷相應里是否有body體
	// 若響應碼是1xx 或者是204;或者getHeaders().getContentLength() == 0 那就返回false  否則返回true
	public boolean hasMessageBody() throws IOException {
		HttpStatus status = HttpStatus.resolve(getRawStatusCode());
		if (status != null && (status.is1xxInformational() || status == HttpStatus.NO_CONTENT || status == HttpStatus.NOT_MODIFIED)) {
			return false;
		}
		if (getHeaders().getContentLength() == 0) {
			return false;
		}
		return true;
	}

	// 上面是完全格局狀態碼(ContentLength)來判斷是否有body體的~~~這里會根據流來判斷
	// 如果response.getBody() == null,返回true
	// 若流里有內容,最終就用new PushbackInputStream(body)包裝起來~~~
	public boolean hasEmptyMessageBody() throws IOException {
		...
	}
	
	...  // 其余接口方法都委托~
	@Override
	public InputStream getBody() throws IOException {
		return (this.pushbackInputStream != null ? this.pushbackInputStream : this.response.getBody());
	}
}

它的作用就是包裝后,提供兩個方法hasMessageBody、hasEmptyMessageBody方便了對body體內容進行判斷

// @since 3.0 泛型T:the data type
public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
	// java.lang.reflect.Type
	private final Type responseType;
	// 這個泛型也是T,表示數據的Class嘛~
	// 該calss有可能就是上面的responseType
	@Nullable
	private final Class<T> responseClass;
	// 重要:用於消息解析的轉換器
	private final List<HttpMessageConverter<?>> messageConverters;
	... // 省略構造函數


	// 從ClientHttpResponse 里提取值
	@Override
	@SuppressWarnings({"unchecked", "rawtypes", "resource"})
	public T extractData(ClientHttpResponse response) throws IOException {
		MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
		// 若沒有消息體(狀態碼不對 或者 消息體為空都被認為是木有)
		if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
			return null;
		}
	
		// content-type若響應頭header里沒有指定,那默認是它MediaType.APPLICATION_OCTET_STREAM
		MediaType contentType = getContentType(responseWrapper);
		
		// 遍歷所有的messageConverters,根據contentType 來選則一個消息轉換器
		// 最終return messageConverter.read((Class) this.responseClass, responseWrapper)
		...
	}
}

它的處理邏輯理解起來非常簡單:利用contentType找到一個消息轉換器,最終HttpMessageConverter.read()把消息讀出來轉換成Java對象。

它還有兩個內部類的實現如下(都是RestTemplate的私有內部類):

RestTemplate:

	// 提取為`ResponseEntity`  最終委托給HttpMessageConverterExtractor完成的
	private class ResponseEntityResponseExtractor<T> implements ResponseExtractor<ResponseEntity<T>> {

		@Nullable
		private final HttpMessageConverterExtractor<T> delegate;

		public ResponseEntityResponseExtractor(@Nullable Type responseType) {
			// 顯然:只有請求的返回值不為null 才有意義~
			if (responseType != null && Void.class != responseType) {
				this.delegate = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
			} else {
				this.delegate = null;
			}
		}

		// 數據提取。都是交給`delegate.extractData(response)`做了,然后new一個ResponseEntity出來包裝進去
		// 若木有返回值(delegate=null),那就是一個`ResponseEntity`實例,body為null
		@Override
		public ResponseEntity<T> extractData(ClientHttpResponse response) throws IOException {
			if (this.delegate != null) {
				T body = this.delegate.extractData(response);
				return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).body(body);
			}
			else {
				return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).build();
			}
		}
	}

	// 提取請求頭
	private static class HeadersExtractor implements ResponseExtractor<HttpHeaders> {
		@Override
		public HttpHeaders extractData(ClientHttpResponse response) {
			return response.getHeaders();
		}
	}

UriTemplateHandler

這個組件它用於定義用變量擴展uri模板的方法

// @since 4.2 出現較晚  
// @see RestTemplate#setUriTemplateHandler(UriTemplateHandler)
public interface UriTemplateHandler {
	URI expand(String uriTemplate, Map<String, ?> uriVariables);
	URI expand(String uriTemplate, Object... uriVariables);
}

關於URI的處理,最終都是委托給UriComponentsBuilder來完成。若對這塊還存在一定疑問的,強烈強烈強烈 參考這里

推薦閱讀

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

總結

本文介紹的組件是去理解RestTemplate必備的組件們,屬於開山篇。因為RestTemplate使用頻繁,並且經常需要調優,因此我寄希望大家也能對它做較為深入的了解,這也是我寫本系列的目的,共勉。

== 若對Spring、SpringBoot、MyBatis等源碼分析感興趣,可加我wx:fsx641385712,手動邀請你入群一起飛 ==
== 若對Spring、SpringBoot、MyBatis等源碼分析感興趣,可加我wx:fsx641385712,手動邀請你入群一起飛 ==


免責聲明!

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



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