2016年6月12日:从 Spring 4.3 开始加入了 OkHttp3ClientHttpRequestFactory
java
本文主要介绍 Spring Web 模块中的 RestTemplate 组件的原理、优缺点、以及如何扩展以知足各类需求。程序员
在介绍 RestTemplate 以前,咱们先来谈谈 HTTP Client,谈谈选择一个优秀的 HTTP Client 实现的的重要性,以及一个优秀的 HTTP Client 应该具有哪些特性。web
在 Java 社区中,HTTP Client 主要有 JDK 的 HttpURLConnection、Apache Commons HttpClient(或被称为 Apache HttpClient 3.x)、Apache HttpComponents Client(或被称为 Apache HttpClient 4.x)、Square 公司开源的 OkHttp。spring
除了这几个纯粹的 HTTP Client 类库之外,还有 Spring 的 RestTemplate、Square 公司的 Retrofit、Netflix 公司的 Feign,以及像 Apache CXF 中的 client 组件。这些框架和类库主要是针对 Web Service 场景,尤为是 RESTful Web Service。它们每每是基于前面提到的 HTTP Client 实现,并在其基础上提供了消息转换、参数映射等对于 Web Service 来讲十分必要的功能。编程
(固然,像 Netty、Mina 这样的网络 IO 框架,实现 HTTP 天然也再也不话下,但这些框架一般过于底层,不会被直接使用)服务器
虽然如今服务间的调用愈来愈多地使用了 RPC 和消息队列,可是 HTTP 依然有适合它的场景。网络
RPC 的优点在于高效的网络传输模型(常使用 NIO 来实现),以及针对服务调用场景专门设计协议和高效的序列化技术。而 HTTP 的优点在于它的成熟稳定、使用实现简单、被普遍支持、兼容性良好、防火墙友好、消息的可读性高。因此在开放 API、跨平台的服务间调用、对性能要求不苛刻的场景中有着普遍的使用。app
正式由于 HTTP 存在着很普遍的应用场景,因此选择一个优秀的 HTTP Client 即是十分重要的。框架
由于目前 HTTP 1.1 不支持多路复用,只有 HTTP Pipeline 这用半复用的模型支持。因此,在须要频繁发送消息的场景中,链接池使必须支持的,以减小频繁创建链接所带来的没必要要的性能损耗。异步
当对端出现问题的时候,长时间的,甚至是无限的超时等待是绝对不能接受的。因此必须必须可以设置超时时间。
HTTP 相关技术(服务器端和客户端)一般被人认为是性能低下的一个重要缘由在于,在很长一段时间里,HTTP 的相关实现缺少对异步的支持。这不只指非阻塞 IO,也包括异步的编程模型。缺少异步编程模型的后果就是,即使 HTTP 协议栈是基于非阻塞 IO 实现的,调用客户端的或者在服务端处理消息的线程有大量时间被浪费在了等待 IO 上面。因此,异步是很是重要的特性。
一般,开发人员但愿面向对象使用各类服务(这里面天然也包括基于 HTTP 协议的服务),而不是直接面对原始的消息和响应开发。因此,透明地将 HTTP 请求和响应进行编解码是十分有必要,由于这能够很大程度地下降开发人员的工做量。
不论一个框架设计的多好,总有一些特殊场景是它们没法原生支持的。这时可扩展性的好坏便体现出来了。
基于上述几点的考虑,RestTemplate 是相对好的选择。缘由在于 RestTemplate 自己基于成熟的 HTTP Client 实现(Apache HttpClient、OkHttp 等),并能够灵活地在这些实现中切换,并且具备良好的扩展性。最重要的是提供了前面几个 HTTP Client 不具有的消息编解码能力。
这里要提一句为何没有本身封装 HTTP Client 的缘由。这个缘由在于想要基于一种 HTTP Client 去提供消息编解码能力和必定的扩展能力并不难,可是若是要设计出一个通用的,对底层实现透明的,具备优秀如 Spring 的扩展性设计的框架并非一件容易事。这里的不易并不在于技术有多高深,而是在于优秀的扩展性设计每每源自从众多优秀程序员、社区和软件公司获得的丰富的一线实践经验,再由像 Spring 转换为最终设计。这样的产品不是一朝一夕就能获得的。在咱们以为本身打造本身的工具以前,咱们能够先深刻了解现有的优秀功能都能作到什么。
欲扬先抑,咱们先来看加入使用 RestTemplate,可能会遇到哪些“坑”。
虽然 spring-web 模块对其它 Spring 模块并无显式的依赖(Maven dependency 的 scope 为 compile),可是对于一些功能,好比异步版本的 RestTemplate,要求必须有 4.1 以上版本的 spring-core 模块。
因此,要想 RestTemplate 彻底发挥其功能,最好能有相近版本的其它的 Spring 模块相配合(spring-core、spring-context、spring-beans、spring-aop)
Spring Web 模块中的 RestTemplate 是一个很不错的面向 RESTful Web 服务的客户端。它提供了不少简化对 RESTful Web 服务调用的功能,例如 Path Parameter 的格式化功能(/hotels/{hotel_id}/books/{book_id},这里的 hotel_id 和 book_id 就是 Path Paramter)、JSON 或 XML 等格式的数据与实体类之间的透明转换等。
所谓默认状况指的是不去扩展 RestTemplate 所提供的类或接口,而是彻底依赖其自己提供的代码。在这种状况下,RestTemplate 仍是有一些不便的地方。例如,它的 Path Parameter 格式化功能,对于普通 HTTP 服务的调用来讲,反而成为了一个缺点,由于普通的 HTTP 服务的 GET 方法常使用 Query Parameter,而不是 Path Parameter。Query Paramter 的形式是 an_http_url?name1=value1&name2=value2
。例如 getOrder.action?order_code=xxx
。若是使用 RestTemplate,做为参数传递给 RestTemplate 的 URL 就必须是 getOrder.action?order_code={order_code}
。若是是固定的参数还好,若是一个 HTTP 服务的 Query Parameter 是可变的,那就很不方便了。
注意,下面涉及到的代码都是基于 spring-web 4.2.6.RELEASE 版本
上面提到,RestTemplate 的 getForEntity
、getForObject
、postForEntity
等方法中的 Map 参数是 uriVariables
,即咱们常说的 Path Param,而非 Query Param(这两个参数的定义能够参照 JAX-RS 中 @PathParam 和 @QueryParam 的定义)。
Path Param 是 URL 的一部分,RESTful 的 Web Service 会按照其定义的 URL Template 从 URL 中解析出其对应的值
RestTemplate 的这种机制面对 RESTful 的 Web Service 无疑是方便的,但不少状况下咱们仍是但愿 RestTemplate 可以在开发人员不用编写额外代码的状况下将 Map 类型的参数当作 Query Param 发送给对端的服务。
幸亏来自 Spring 你们庭的 RestTemplate 也具备良好的可扩展性,其具备一个名为 UriTemplateHandler
扩展点。由于不管是 Path Param 仍是 Query Param,它们都是 URI 的一部分,因此只需实现自定义的 URI 生成机制便可解决这个问题。
经过扩展 DefaultUriTemplateHandler
,咱们能够将 Map<String, ?> uriVariables
也做为 Query Param。具体实现以下:
public class QueryParamsUrlTemplateHandler extends DefaultUriTemplateHandler { @Override public URI expand(String uriTemplate, Map<String, ?> uriVariables) { UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(uriTemplate); for (Map.Entry<String, ?> varEntry : uriVariables.entrySet()) { uriComponentsBuilder.queryParam(varEntry.getKey(), varEntry.getValue()); } uriTemplate = uriComponentsBuilder.build().toUriString(); return super.expand(uriTemplate, uriVariables); } }
上面的实现基于 DefaultUriTemplateHandler
,因此保有了原来设置 Path Param 的功能。
实现这个需求有多种方法,好比经过拦截器。这里使用另外一个方法,经过一个自定义的 ClientHttpRequestFactory
public class CustomHeadersClientHttpRequestFactoryWrapper extends AbstractClientHttpRequestFactoryWrapper { private HttpHeaders customHeaders = new HttpHeaders(); /** * Create a {@code AbstractClientHttpRequestFactoryWrapper} wrapping the given request factory. * * @param requestFactory the request factory to be wrapped */ protected CustomHeadersClientHttpRequestFactoryWrapper(ClientHttpRequestFactory requestFactory) { super(requestFactory); } @Override protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) throws IOException { ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod); for (Map.Entry<String, List<String>> headerEntry : customHeaders.entrySet()) { request.getHeaders().put(headerEntry.getKey(), headerEntry.getValue()); } return request; } public void addHeader(String header, String... values) { customHeaders.put(header, Arrays.asList(values)); } }
RestTemplate 提供了良好的扩展性,可是有些设置是使用 ``
RestTemplate 自己并无作 HTTP 底层的实现,而是利用了现有的技术,如 JDK 或 Apache HttpClient 等。
RestTemplate 须要使用一个实现了 ClientHttpRequestFactory
接口的类为其提供 ClientHttpRequest
实现(另外还有 AsyncClientHttpRequestFactory
对应于异步 HTTP 实现,这里暂且不表)。而 ClientHttpRequest
则实现封装了组装、发送 HTTP 消息,以及解析响应的的底层细节。
目前(4.2.6.RELEASE)的 RestTemplate 主要有四种 ClientHttpRequestFactory
的实现,它们分别是:
HttpURLConnection
的 SimpleClientHttpRequestFactory
HttpComponentsClientHttpRequestFactory
OkHttpClientHttpRequestFactory
Netty4ClientHttpRequestFactory
另外,还有用于提供拦截器功能的 InterceptingClientHttpRequestFactory
。
写消息指的是 requestBody 转换为某一种格式,如 JSON、XML 的数据的过程。
spring-web 模块提供了一个 HttpMessageConverter
接口,用来读写 HTTP 消息。这个接口不只被 RestTemplate 使用,也被 Spring MVC 所使用。
spring-web 模块提供了基于 Jackson、GSON 等类库的 HttpMessageConverter
,用于进行 JSON 或 XML 格式数据的转换。
RestTemplate 在发送消息时,会根据消息的 ContentType 或者 RequestBody 对象自己的一些属性判断到底是使用哪一个 HttpMessageConverter
写消息。
具体来讲,若是 RequestBody 是一个 HttpEntity
的话,会从中读取 ContentType 属性。同时,RequestBody 对象自己也会以为一个 HttpMessageConverter
是否会处理这个对象。例如,ProtobufHttpMessageConverter
会要求 RequestBody 对象必须实现 com.google.protobuf.Message
接口。
读消息指的是读取 HTTP Response 中的数据,转换为用户指定的格式(经过 Class<T> responseType 参数指定)。相似于写消息的处理,读消息的处理也是经过 ContentType 和 responseType 来选择的相应 HttpMessageConverter
来进行的。
RestTemplate 提供了一个 ResponseErrorHandler
的接口,用来处理错误的 Response。能够经过设置自定义的 ResponseErrorHandler
来实现扩展。
根据我上面表达的思想,一个统1、规范和简化 RestTemplate 使用的工具已经产生,不过暂时因为其代码是公司项目的一部分,因此暂时不便公开。并且我但愿是在这个工具通过了更多的实践考验以后再贡献出来会更好。
目前的一个完整使用案例以下:
@Configuration public class SpringConfigurationDemo { @Bean public RestTemplate myRestTemplate() { return RestTemplateBuilder.create() .withClientKey("myRestTemplate") .implementation(HttpClientImplementation.OK_HTTP) .clearMessageConverters() .setMessageConverter(new MappingJackson2HttpMessageConverter(), MediaType.TEXT_PLAIN) .enableAutoQueryParams() .connectTimeout(100) .readTimeout(200) .header(HttpHeaders.USER_AGENT, "MyAgent") .build(); } }
虽然 RestTemplate 是一个很不错的 HTTP Client,但 Netflix 已经开源了一个更好地 HTTP Client 工具 - Feign。它是一个声明式的 HTTP Client,在易用性、可读性等方面大幅领先于现有的工具。我打算稍后写一篇文章分析 Feign 的思想、原理和优势(原理其实不复杂,可是能想到这么作的却没几个,原创的创新思想永远是最难得的)