User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Spring   ›   Spring Notes   ›   正文
Spring Notes

RestTemplate组件:ClientHttpRequestFactory—ClientHttpRequestInterceptor—ResponseExtractor

2020-08-20 21:54:08
819  0 0
参考目录 隐藏
1) ClientHttpRequestFactory
2) SimpleClientHttpRequestFactory
3) 关于HttpURLConnection的API使用,需注意如下几点:
4) 使用哪一个底层http库?
5) 关于HttpURLConnection、HttpClient、OkHttpClient的简单比较:
6) AbstractClientHttpRequestFactoryWrapper
7) InterceptingClientHttpRequestFactory(重要)
8) ClientHttpRequestInterceptor
9) BasicAuthorizationInterceptor:
10) BasicAuthenticationInterceptor:
11) BufferingClientHttpRequestFactory
12) ResponseErrorHandler
13) DefaultResponseErrorHandler
14) HttpClientErrorException:
15) ExtractingResponseErrorHandler
16) ResponseExtractor
17) UriTemplateHandler

阅读完需:约 22 分钟

本文为深入了解Spring提供的Rest调用客户端RestTemplate开山,对它相关的一些组件做讲解。

ClientHttpRequestFactory

它是个函数式接口,用于根据URI和HttpMethod创建出一个ClientHttpRequest来发送请求~

ClientHttpRequest它代表请求的客户端,该接口继承自HttpRequest、HttpOutputMessage,只有一个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;
}

它的继承树如下:

可以直观的看到,我们可以使用Apache的HttpClient、OkHttp3、Netty4都可,但这些都需要额外导包,默认情况下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为示例演示使用三方库:

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

案例使用

案例内容仅仅只需把上例第一句话换成使用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代替~


关于HttpURLConnection、HttpClient、OkHttpClient的简单比较:

  • 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 HttpClient,Android5.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);
	}

}

InterceptingClientHttpRequest的execute()方法的特点是:若存在拦截器,交给给拦截器去执行发送请求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来完成。

SpringMVC—URI Builder模式(UriComponents/UriComponentsBuilder)

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

0 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 593910浏览

随机文章
Kotlin-协程(专)—协程调度(三十四)
4年前
SpringBoot—三种跨域场景总结
4年前
Redis笔记—做延迟消息队列
5年前
SpringBoot—JPA整合
5年前
Java—并发编程(二)synchronized关键字
4年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 593910 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付