RestTemplate相关组件ClientHttpRequestInterceptor享学Spring-MVC

33次阅读

共计 20435 个字符,预计需要花费 52 分钟才能阅读完成。

每篇一句

做事的人和做梦的人最大的区别就是行动力

前言

本文为深入了解 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,手动邀请你入群一起飞 ==

正文完
 0