• 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,手动邀请你入群一起飞 ==

  • 相关阅读:
    阿里云esc 安装 mysql8.0
    阿里云esc 登录时的相关提示
    C# web项目 log4net 使用
    MVC 全局异常捕获
    datetimepicker 基础使用/select2 基础使用
    C# 从登陆开始 MVC4+BOOTSTRAP
    Android如何导入语言资源
    Android自带邮件含中文的附件用HTML打开乱码问题的解决
    android 解决输入法遮挡输入框的问题
    repo代码简单解读
  • 原文地址:https://www.cnblogs.com/yourbatman/p/11532777.html
Copyright © 2020-2023  润新知