- 一、RestTemplate简介
- 二、ClientHttpRequestFactory
-
- 2.1 ClientHttpRequest & ClientHttpResponse
- 2.2 SimpleClientHttpRequestFactory
-
- 2.2.1 HttpURLConnection注意点
- 2.3 HttpComponentsClientHttpRequestFactory
-
- 2.3.1 使用HttpClient发送请求
- 2.3.2 HttpURLConnection、HttpClient、OkHttpClient比较
- 2.4 AbstractClientHttpRequestFactoryWrapper
-
- 2.4.1 InterceptingClientHttpRequestFactory
-
- ClientHttpRequestInterceptor请求拦截器
-
- BasicAuthorizationInterceptor(已过时)
- BasicAuthenticationInterceptor
- 2.4.2 BufferingClientHttpRequestFactory
- 三、ResponseErrorHandler (对响应错误进行处理)
-
- 3.1 DefaultResponseErrorHandler
-
- 3.1.1 HttpClientErrorException
- 3.1.2 HttpServerErrorException (同上代码类似)
- 3.2 ExtractingResponseErrorHandler
- 四、ResponseExtractor (从响应中提取数据)
-
- 4.1 MessageBodyClientHttpResponseWrapper
- 4.2 HttpMessageConverterExtractor
- 五、UriTemplateHandler
-
- 5.1 URI Builder
- 5.2 UriComponents
- 5.3 UriComponentsBuilder
-
- 使用案例
-
RestTemplate是从 Spring3.0 开始支持的一个HTTP请求工具,它提供了常见的REST请求方案的模版,例如GET请求、POST请求、PUT请求、DELETE请求以及一些通用的请求执行方法exchange以及execute。
-
RestTemplate 继承InterceptingHttpAccessor并且实现了RestOperations接口,其中 RestOperations 接口定义了基本的 RESTful 操作,这些操作在 RestTemplate 中都得到了实现。
-
RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端库。它提供了一套接口,然后分别用三种 Java 最常用 Http 连接的库来分别实现这套接口:
- JDK 自带的HttpURLConnection
- Apache 的HttpClient
- OKHttp3
客户端Http请求工厂, 它是个函数式接口,用于根据URI和HttpMethod创建出一个ClientHttpRequest来发送请求
// @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
-
ClientHttpRequest代表请求的客户端,该接口继承自HttpRequest、HttpOutputMessage,只有一个ClientHttpResponse execute() throws IOException 方法。
-
其中HttpURLConnection、HttpComponents(HttpClient)、OkHttp3、Netty4对它都有实现
ClientHttpResponse继承树
使用工厂创建ClientHttpRequest,具体采用哪种http客户端, 使用对应的工厂来创建即可, 发送请求就不需要关心具体的细节了
它是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 // 若值设置为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()方法略,无非就是在线程池里异步完成请求 ... }
Demo Test
public class HttpClientRequestTest { public static void main(String[] args) throws IOException { SimpleClientHttpRequestFactory clientFactory = new SimpleClientHttpRequestFactory(); // ConnectTimeout只有在网络正常的情况下才有效,因此两个一般都设置 clientFactory.setConnectTimeout(5000); //建立连接的超时时间 5秒 clientFactory.setReadTimeout(5000); // 传递数据的超时时间(在网络抖动的情况下,这个参数很有用) ClientHttpRequest clientHttpRequest = clientFactory.createRequest(URI.create("https://www.baidu.com"), HttpMethod.GET); // 发送请求 ClientHttpResponse response = clientHttpRequest.execute(); System.out.println(response.getStatusCode()); // 200 OK System.out.println(response.getStatusText()); // OK System.out.println(response.getHeaders()); // [Content-Length:"2443", Content-Type:"text/html", Server:"bfe", Date:"Tue, 23 Aug 2022 03:43:19 GMT"] // 返回内容 是个InputStream byte[] bytes = FileCopyUtils.copyToByteArray(response.getBody()); System.out.println(new String(bytes, StandardCharsets.UTF_8)); // 百度首页内容的html } }2.2.1 HttpURLConnection注意点
关于HttpURLConnection的API使用,需注意如下几点:
- HttpURLConnection对象不能直接构造,需要通过URL类中的openConnection()方法来获得
-
HttpURLConnection的connect()方法,实际上只是建立了一个与服务器的TCP连接,并没有实际发送HTTP请求。HTTP请求实际上直到我们获取服务器响应数据(如调用getInputStream()、getResponseCode()等方法)时才正式发送出去
配置信息都需要在connect()方法执行之前完成
- HttpURLConnection是基于HTTP协议的,其底层通过socket通信实现。如果不设置超时(timeout),在网络异常的情况下,可能会导致程序僵死而不继续往下执行。所以一定要设置超时时间
- HTTP正文的内容是通过OutputStream流写入的, 向流中写入的数据不会立即发送到网络,而是存在于内存缓冲区中,待流关闭时,根据写入的内容生成HTTP正文
- 调用getInputStream()方法时,返回一个输入流,用于从中读取服务器对于HTTP请求的返回信息。
- HttpURLConnection.connect()不是必须的。当我们需要返回值时,比如我们使用HttpURLConnection.getInputStream()方法的时候它就会自动发送请求了,所以完全没有必要调用connect()方法了
HttpURLConnection使用工具类
public class HttpRequest { private static final Logger logger = LoggerFactory.getLogger(HttpRequest.class); public static String http(String method, String httpUrl, Integer timeout, String... headers) { String message = ""; HttpURLConnection connection = null; InputStream inputStream = null; try { URL url = new URL(httpUrl); connection = (HttpURLConnection) url.openConnection(); if (headers != null && headers.length > 0) { for (int i = 0; i < headers.length; i++) { connection.setRequestProperty(headers[i], headers[i++]); } } connection.setRequestMethod(method); connection.setConnectTimeout(timeout); connection.setReadTimeout(timeout); connection.connect(); inputStream = connection.getInputStream(); message = streamToString(inputStream); } catch (Exception e) { logger.error("url:{}", httpUrl, e); } finally { try { inputStream.close(); connection.disconnect(); } catch (Throwable ignored) { } } return message; } /** * @param inputStream inputStream * @return 字符串转换之后的 */ public static String streamToString(InputStream inputStream) { try(BufferedReader br =new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { StringBuilder builder = new StringBuilder(); String output; while((output = br.readLine())!=null){ builder.append(output); } return builder.toString(); } catch (IOException e) { throw new RuntimeException("Http 服务调用失败",e); } } }2.3 HttpComponentsClientHttpRequestFactory 2.3.1 使用HttpClient发送请求
HttpURLConnection在功能上是有些不足(简单的提交参数可以满足)。绝大部分情况下Web站点的网页可能没这么简单,这些页面并不是通过一个简单的URL就可访问的,可能需要用户登录而且具有相应的权限才可访问该页面。在这种情况下,就需要涉及Session、Cookie的处理了,如果打算使用HttpURLConnection来处理这些细节,当然也是可能实现的,只是处理起来难度就大了。
我们可以Apache开源组织提供了一个HttpClient项目,可以用于发送HTTP请求,接收HTTP响应(包含HttpGet、HttpPost…等各种发送请求的对象)
引入依赖
<dependency> <groupId>org.apache.httpcomponents private HttpClient httpClient; @Nullable private RequestConfig requestConfig; // 这个配置就是可以配置超时等等的类 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); ... } }
另外OkHttp3ClientHttpRequestFactory使用的是okhttp3.OkHttpClient发送请求;Netty4ClientHttpRequestFactory使用的是io.netty.channel.EventLoopGroup
2.3.2 HttpURLConnection、HttpClient、OkHttpClient比较HttpURLConnection
- 优点:JDK内置支持,java的标准类
- 缺点:API不够友好,什么都没封装,对用高级功能使用不方便
HttpClient
- 优点:功能强大,API友好,使用率够高,几乎成为了实际意义上的标准(相当于对HttpURLConnection的封装)
- 缺点:性能稍低(比HttpURLConnection低,但4.3后使用连接池进行了改善),API较臃肿
OkHttpClient : 新一代的Http访问客户端
-
优点:一个专注于性能和易用性的HTTP客户端(节约宽带,Android推荐使用),它设计的首要目标就是高效。提供了最新的 HTTP 协议版本 HTTP/2 和 SPDY 的支持。如果 HTTP/2 和 SPDY 不可用,OkHttp 会使用连接池来复用连接以提高效率
默认情况下,OKHttp会自动处理常见的网络问题,像二次连接、SSL的握手问题。支持文件上传、下载、cookie、session、https证书等几乎所有功能; 支持取消某个请求
连接池:可能是http请求,也可能是https请求 加入池化技术,就不用每次发起请求都新建一个连接(每次连接握手三次,效率太低)
2.4 AbstractClientHttpRequestFactoryWrapper对ClientHttpRequestFactory的一个包装抽象类
它有如下两个子类实现: InterceptingClientHttpRequestFactory, BufferingClientHttpRequestFactory
2.4.1 InterceptingClientHttpRequestFactoryInterceptor拦截的概念,还是蛮重要的。它持有的ClientHttpRequestInterceptor对于我们若想要拦截发出去的请求非常之重要
// @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),否则就使用自己的execute()方法。
// InterceptingClientHttpRequest中的内部类 private class InterceptingRequestExecution implements ClientHttpRequestExecution { private final Iterator<ClientHttpRequestInterceptor> iterator; public InterceptingRequestExecution() { this.iterator = interceptors.iterator(); } @Override public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException { if (this.iterator.hasNext()) { ClientHttpRequestInterceptor nextInterceptor = this.iterator.next(); return nextInterceptor.intercept(request, body, this); } else { HttpMethod method = request.getMethod(); Assert.state(method != null, "No standard HTTP method"); ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method); request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value)); if (body.length > 0) { if (delegate instanceof StreamingHttpOutputMessage) { StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate; streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream)); } else { StreamUtils.copy(body, delegate.getBody()); } } return delegate.execute(); } } }ClientHttpRequestInterceptor请求拦截器
// @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有任何改动,只是把用户名、密码帮你放进了请求头上
BasicAuthenticationInterceptor它是用来代替BasicAuthorizationInterceptor。它使用标准的授权头来处理,参考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); } }
说明:这两个请求拦截器默认都是没有被"装配"的,如果使用需要手动装配
2.4.2 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); } }
继承树
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, HttpServerErrorException
3.1.1 HttpClientErrorException它针对不同的状态码HttpStatus,创建了不同的类型进行返回
- BadRequest、Unauthorized、Forbidden等等都是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); } } ... }3.1.2 HttpServerErrorException (同上代码类似) 3.2 ExtractingResponseErrorHandler
继承自DefaultResponseErrorHandler。它将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; } } }四、ResponseExtractor (从响应中提取数据)
响应提取器:从Response中提取数据。 RestTemplate请求完成后,都是通过它来从ClientHttpResponse提取出指定内容(比如请求头、请求Body体等)
它的直接实现似乎只有HttpMessageConverterExtractor,当然它也是最为重要的一个实现,和HttpMessageConverter相关。
4.1 MessageBodyClientHttpResponseWrapper它的作用就是包装后,提供两个方法hasMessageBody、hasEmptyMessageBody方便了对body体内容进行判断
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()); } }4.2 HttpMessageConverterExtractor
它的处理逻辑理解起来非常简单:利用contentType找到一个消息转换器,最终HttpMessageConverter.read()把消息读出来转换成Java对象。
// @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 来选则一个消息转换器 for (HttpMessageConverter<?> messageConverter : this.messageConverters) { if (messageConverter instanceof GenericHttpMessageConverter) { GenericHttpMessageConverter<?> genericMessageConverter = (GenericHttpMessageConverter<?>) messageConverter; if (genericMessageConverter.canRead(this.responseType, null, contentType)) { if (logger.isDebugEnabled()) { ResolvableType resolvableType = ResolvableType.forType(this.responseType); logger.debug("Reading to [" + resolvableType + "]"); } return (T) genericMessageConverter.read(this.responseType, null, responseWrapper); } } if (this.responseClass != null) { if (messageConverter.canRead(this.responseClass, contentType)) { if (logger.isDebugEnabled()) { String className = this.responseClass.getName(); logger.debug("Reading to [" + className + "] as \"" + contentType + "\""); } return (T) messageConverter.read((Class) this.responseClass, responseWrapper); } } } // 最终return messageConverter.read((Class) this.responseClass, responseWrapper) ... } }
它还有两个内部类的实现如下(都是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来完成。
5.1 URI Builder用来处理URI、URL等和HTTP协议相关的元素,它提供了非常好用、功能强大的URI Builder模式来完成
- Spring MVC从3.1开始提供了一种机制,可以通过UriComponentsBuilder和UriComponents面向对象的构造和编码URI
它表示一个不可变的URI组件集合,将组件类型映射到字符串值, 具有更强大的编码选项和对URI模板变量的支持
- URI:统一资源标识符; URI可以唯一的标识某一资源, 比如学号可以唯一标识学生, 身份证号可以唯一标识一个人等等
- URL:统一资源定位符; URL是URI的子集, 不仅可以唯一标识一个资源,还能告诉你他在哪。 比如某学生在5号公寓楼328寝5床, 这就是一个URL
- 一般构建UriComponents我们使用UriComponentsBuilder构建器
// @since 3.1 是个抽象类。 public abstract class UriComponents implements Serializable { // 捕获URI模板变量名 private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); @Nullable private final String scheme; @Nullable private final String fragment; // 唯一构造,是protected 的 protected UriComponents(@Nullable String scheme, @Nullable String fragment) { this.scheme = scheme; this.fragment = fragment; } ... // 省略它俩的get方法(无set方法) @Nullable public abstract String getSchemeSpecificPart(); @Nullable public abstract String getUserInfo(); @Nullable public abstract String getHost(); // 如果没有设置port,就返回-1 public abstract int getPort(); @Nullable public abstract String getPath(); public abstract List<String> getPathSegments(); @Nullable public abstract String getQuery(); public abstract MultiValueMap<String, String> getQueryParams(); // 此方法是public且是final的哦~ // 注意它的返回值还是UriComponents public final UriComponents encode() { return encode(StandardCharsets.UTF_8); } public abstract UriComponents encode(Charset charset); // 这是它最为强大的功能:对模版变量的支持 // 用给定Map映射中的值替换**所有**URI模板变量 public final UriComponents expand(Map<String, ?> uriVariables) { return expandInternal(new MapTemplateVariables(uriVariables)); } // 给定的是变量数组,那就按照顺序替换 public final UriComponents expand(Object... uriVariableValues) {...} public final UriComponents expand(UriTemplateVariables uriVariables) { ... } // 真正的expand方法,其实还是子类来实现的 abstract UriComponents expandInternal(UriTemplateVariables uriVariables); // 规范化路径移除**序列**,如“path/…”。 // 请注意,规范化应用于完整路径,而不是单个路径段。 public abstract UriComponents normalize(); // 连接所有URI组件以返回完全格式的URI字符串。 public abstract String toUriString(); public abstract URI toUri(); @Override public final String toString() { return toUriString(); } // 拷贝 protected abstract void copyToUriComponentsBuilder(UriComponentsBuilder builder); ... // 提供静态工具方法expandUriComponent和sanitizeSource }
它包含有和Http相关的各个部分:如schema、port、path、query等; 此抽象类有两个实现类:OpaqueUriComponents和HierarchicalUriComponents
由于在实际使用中会使用构建器UriComponentsBuilder来创建实例,所以都是面向抽象类编程,并不需要关心具体实现
5.3 UriComponentsBuilder使用了Builder模式,用于构建UriComponents。实际开发工作中所有的UriComponents都应是通过此构建器构建
public class UriComponentsBuilder implements UriBuilder, Cloneable { ... // 省略所有正则(包括提取查询参数、scheme、port等等等等) ... // 它所有的构造函数都是protected的 // ******************实例化静态方法(7种)****************** // 创建一个空的bulder,里面schema,port等等啥都木有 public static UriComponentsBuilder newInstance() { return new UriComponentsBuilder(); } // 直接从path路径里面,分析出一个builder。常用 public static UriComponentsBuilder fromPath(String path) {...} // 直接从URI路径里面,分析出一个builder(要注意编码问题) public static UriComponentsBuilder fromUri(URI uri) {...} /* String uriString = "/hotels/42?filter={value}"; UriComponentsBuilder.fromUriString(uriString).buildAndExpand("hot&cold"); /hotels/42?filter=hot&cold */ public static UriComponentsBuilder fromUriString(String uri) {} // 直接从httpUrl路径里面,分析出一个builder /* String urlString = "https://example.com/hotels/42?filter={value}"; UriComponentsBuilder.fromHttpUrl(urlString).buildAndExpand("hot&cold"); https://example.com/hotels/42?filter=hot&cold */ public static UriComponentsBuilder fromHttpUrl(String httpUrl) {} // HttpRequest是HttpMessage的子接口。它的原理是:fromUri(request.getURI()) // 然后再调用本类的adaptFromForwardedHeaders(request.getHeaders()) // 解释:从头Forwarded、X-Forwarded-Proto等拿到https、port等设置值~~ public static UriComponentsBuilder fromHttpRequest(HttpRequest request) {} // 和fromUriString()方法差不多 public static UriComponentsBuilder fromOriginHeader(String origin) {} // *******************下面都是实例方法******************* public final UriComponentsBuilder encode() { return encode(StandardCharsets.UTF_8); } public UriComponentsBuilder encode(Charset charset) {} // 调用此方法生成一个UriComponents public UriComponents build() { return build(false); } public UriComponents build(boolean encoded) { // encoded=true,取值就是FULLY_ENCODED 全部编码 // 否则只编码模版或者不编码 return buildInternal(encoded ? EncodingHint.FULLY_ENCODED : (this.encodeTemplate ? EncodingHint.ENCODE_TEMPLATE : EncodingHint.NONE) ); } // buildInternal内部就会自己new子类:OpaqueUriComponents或者HierarchicalUriComponents // 以及执行UriComponents.expand方法了(若指定了参数的话),使用者不用关心了 // 显然这就是个多功能方法了:设置好参数。build后立马Expand public UriComponents buildAndExpand(Map<String, ?> uriVariables) { return build().expand(uriVariables); } public UriComponents buildAndExpand(Object... uriVariableValues) {} //build成为一个URI。注意这里编码方式是:EncodingHint.ENCODE_TEMPLATE @Override public URI build(Object... uriVariables) { return buildInternal(EncodingHint.ENCODE_TEMPLATE).expand(uriVariables).toUri(); } @Override public URI build(Map<String, ?> uriVariables) { return buildInternal(EncodingHint.ENCODE_TEMPLATE).expand(uriVariables).toUri(); } // @since 4.1 public String toUriString() { ... } // ====重构/重新设置Builder==== public UriComponentsBuilder uri(URI uri) {} public UriComponentsBuilder uriComponents(UriComponents uriComponents) {} @Override public UriComponentsBuilder scheme(@Nullable String scheme) { this.scheme = scheme; return this; } @Override public UriComponentsBuilder userInfo(@Nullable String userInfo) { this.userInfo = userInfo; resetSchemeSpecificPart(); return this; } public UriComponentsBuilder host(@Nullable String host){ ... } ... // 省略其它部分 // 给URL后面拼接查询参数(键值对) @Override public UriComponentsBuilder query(@Nullable String query) {} // 遇上相同的key就替代,而不是直接在后面添加了(上面query是添加) @Override public UriComponentsBuilder replaceQuery(@Nullable String query) {} @Override public UriComponentsBuilder queryParam(String name, Object... values) {} ... replaceQueryParam // 可以先单独设置参数,但不expend哦~ public UriComponentsBuilder uriVariables(Map<String, Object> uriVariables) {} @Override public Object clone() { return cloneBuilder(); } // @since 4.2.7 public UriComponentsBuilder cloneBuilder() { return new UriComponentsBuilder(this); } ... }
Demo Test
public static void main(String[] args) { String url; UriComponents uriComponents = UriComponentsBuilder.newInstance() //.encode(StandardCharsets.UTF_8) .scheme("https").host("www.baidu.com").path("/test").path("/{template}") //此处{}就成 不要写成${} //.uriVariables(传一个Map).build(); .build().expand("myhome"); // 此效果同上一句,但推荐这么使用,方便一些 url = uriComponents.toUriString(); System.out.println(url); // https://www.baidu.com/test/myhome // 从URL字符串中构造(注意:toUriString方法内部是调用了build和expend方法的) System.out.println(UriComponentsBuilder.fromHttpUrl(url).toUriString()); // https://www.baidu.com/test/myhome System.out.println(UriComponentsBuilder.fromUriString(url).toUriString()); // https://www.baidu.com/test/myhome // 给URL中放添加参数 query和replaceQuery uriComponents = UriComponentsBuilder.fromHttpUrl(url).query("name=中国&age=18").query("&name=二次拼接").build(); url = uriComponents.toUriString(); // 效果描述:&test前面这个&不写也是木有问题的。并且两个name都出现了 System.out.println(uriComponents.toUriString()); // https://www.baidu.com/test/myhome?name=中国&name=二次拼接&age=18 uriComponents = UriComponentsBuilder.fromHttpUrl(url).query("name=中国&age=18").replaceQuery("name=二次拼接").build(); url = uriComponents.toUriString(); // 这种够狠:后面的直接覆盖前面“所有的”查询串 System.out.println(uriComponents.toUriString()); // https://www.baidu.com/test/myhome?name=二次拼接 //queryParam/queryParams/replaceQueryParam/replaceQueryParams // queryParam:一次性指定一个key,queryParams一次性可以搞多个key url = "https://www.baidu.com/test/myhome"; // 重置一下 uriComponents = UriComponentsBuilder.fromHttpUrl(url).queryParam("name","中国","美国").queryParam("age",18) .queryParam("name","英国").build(); url = uriComponents.toUriString(); // 发现是不会有repalace的效果的 System.out.println(uriComponents.toUriString()); // https://www.baidu.com/test/myhome?name=中国&name=美国&name=英国&age=18 // 关于repalceParam相关方法,交给各位自己去试验吧~~~ // 不需要domain,构建局部路径,它也是把好手 uriComponents = UriComponentsBuilder.fromPath("").path("/test").build(); // .fromPath("/").path("/test") --> /test // .fromPath("").path("/test") --> /test // .fromPath("").path("//test") --> /test // .fromPath("").path("test") --> /test System.out.println(uriComponents.toUriString()); // /test?name=fsx }
使用这种方式来构建URL还是非常方便的,它的容错性非常高,写法灵活且不容易出错。
- URI构建的任意部分(包括查询参数、scheme等等)都是可以用{}这种形式的模版参数的
- 被替换的模版中还支持这么来写:/myurl/{name:[a-z]}/show,这样用expand也能正常赋值
public abstract class AbstractRemoteHelper { @Autowired private RestTemplate restTemplate; protected abstract URI buildURI(String apiPath, Map<String, Object> params); protected <T> T executeForBean(String apiPath, Map<String, Object> params) { return this.restTemplate.exchange( buildURI(apiPath, params), HttpMethod.GET, null, new ParameterizedTypeReference<T>() { } ).getBody(); } protected String executeForRaw(String apiPath, Map<String, Object> params) { restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8)); return this.restTemplate.exchange( buildURI(apiPath, params), HttpMethod.GET, null, String.class ).getBody(); } protected String executeForRawPost(String apiPath, JSONObject object) { restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8)); HttpEntity<Object> requestEntity = new HttpEntity<>(object, null); return this.restTemplate.exchange( buildURI(apiPath, null), HttpMethod.POST, requestEntity, String.class ).getBody(); } protected String executeForRawAndHeaderPost(String apiPath, @Nullable Map<String, Object> params, @Nullable Map<String, String> headers) { restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8)); HttpHeaders httpHeaders = null; if (headers != null) { httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); Iterator<Map.Entry<String, String>> it = headers.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, String> next = it.next(); httpHeaders.add(next.getKey(), next.getValue()); } } HttpEntity<Object> requestEntity = new HttpEntity<>(params, httpHeaders); return this.restTemplate.exchange( buildURI(apiPath, null), HttpMethod.POST, requestEntity, String.class ).getBody(); } }
@Component public class MatrixHelper extends AbstractRemoteHelper { @Value("${matrix.url}") private String matrixUrl; private static final String STATISTIC_REPORT = "/usoppu/getUsoppuParamChart"; private static final String LOG_BY_CONDITION_REPORT = "/usoppu/getUsoppuLogByCondition"; public List<SoaStatisticEntity> getUsoppuParamChartData(Map<String, Object> params) { params.put("appId", "AppTest"); params.put("contrast", 0); params.put("alertType", 0); String result = this.executeForRaw(MatrixHelper.STATISTIC_REPORT, params); JSONObject data = JSON.parseObject(result); List<SoaStatisticEntity> allList = new ArrayList<>(); if (data != null) { JSONArray records = data.getJSONArray("data"); List<SoaStatisticEntity> latencyList = records.stream().map(list -> { SoaStatisticEntity entity = new SoaStatisticEntity(); JSONObject t = (JSONObject) list; entity.setAppId(t.getString("name")); entity.setNumber(t.getBigDecimal("value").stripTrailingZeros()); return entity; }).collect(Collectors.toList()); if(CollectionUtils.isNotEmpty(latencyList)){ allList.addAll(latencyList); } } return allList; } public JSONArray getUsoppuLogByCondition(Map<String, Object> params,Integer pageIndex) { params.put("appId", "AppTest"); params.put("size", 20); params.put("current", pageIndex); params.put("isError", 1); params.put("sortOrder", 0); String result = this.executeForRaw(MatrixHelper.LOG_BY_CONDITION_REPORT, params); JSONObject data = JSON.parseObject(result); if (data != null) { JSONArray records = data.getJSONObject("data").getJSONArray("records"); return records; } return new JSONArray(); } @Override protected URI buildURI(String apiPath, Map<String, Object> params) { UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(matrixUrl) .path(apiPath); if (MapUtils.isNotEmpty(params)) { params.forEach((key, value) -> builder.queryParam(key, value)); } URI uri = builder.build(true).encode().toUri(); return uri; } }