关于RestTemplate的使用深度解析

目录
  • 一、概述
    • 选择一个优秀的 HTTP Client 的重要性
    • 优秀的 HTTP Client 需要具备的特性
      • 连接池
      • 超时时间设置(连接超时、读取超时等)
      • 是否支持异步
      • 请求和响应的编解码
      • 可扩展性
    • 答案
  • 二、使用 RestTemplate 的缺点
    • 依赖 Spring 其它模块
    • 默认情况下 RestTemplate 存在的不足
  • 三、扩展 RestTemplate
    • 设置 Query Params
    • 设置自定义的 HTTP Header
    • 简化配置
  • 四、RestTemplate 原理解析
    • HTTP Client 实现
    • 写消息
    • 读消息
    • 错误处理
  • 后记

从 Spring 4.3 开始加入了 OkHttp3ClientHttpRequestFactory

一、概述

本文主要介绍 Spring Web 模块中的 RestTemplate 组件的原理、优缺点、以及如何扩展以满足各种需求。

在介绍 RestTemplate 之前,我们先来谈谈 HTTP Client,谈谈选择一个优秀的 HTTP Client 实现的的重要性,以及一个优秀的 HTTP Client 应该具备哪些特性。

在 Java 社区中,HTTP Client 主要有 JDK 的 HttpURLConnection、Apache Commons HttpClient(或被称为 Apache HttpClient 3.x)、Apache HttpComponents Client(或被称为 Apache HttpClient 4.x)、Square 公司开源的 OkHttp。

除了这几个纯粹的 HTTP Client 类库以外,还有 Spring 的 RestTemplate、Square 公司的 Retrofit、Netflix 公司的 Feign,以及像 Apache CXF 中的 client 组件。这些框架和类库主要是针对 Web Service 场景,尤其是 RESTful Web Service。它们往往是基于前面提到的 HTTP Client 实现,并在其基础上提供了消息转换、参数映射等对于 Web Service 来说十分必要的功能。

(当然,像 Netty、Mina 这样的网络 IO 框架,实现 HTTP 自然也不再话下,但这些框架通常过于底层,不会被直接使用)

选择一个优秀的 HTTP Client 的重要性

虽然现在服务间的调用越来越多地使用了 RPC 和消息队列,但是 HTTP 依然有适合它的场景。

RPC 的优势在于高效的网络传输模型(常使用 NIO 来实现),以及针对服务调用场景专门设计协议和高效的序列化技术。而 HTTP 的优势在于它的成熟稳定、使用实现简单、被广泛支持、兼容性良好、防火墙友好、消息的可读性高。所以在开放 API、跨平台的服务间调用、对性能要求不苛刻的场景中有着广泛的使用。

正式因为 HTTP 存在着很广泛的应用场景,所以选择一个优秀的 HTTP Client 便是十分重要的。

优秀的 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 的缺点

欲扬先抑,我们先来看加入使用 RestTemplate,可能会遇到哪些“坑”。

依赖 Spring 其它模块

虽然 spring-web 模块对其它 Spring 模块并没有显式的依赖(Maven dependency 的 scope 为 compile),但是对于一些功能,比如异步版本的 RestTemplate,要求必须有 4.1 以上版本的 spring-core 模块。

所以,要想 RestTemplate 完全发挥其功能,最好能有相近版本的其它的 Spring 模块相配合(spring-core、spring-context、spring-beans、spring-aop)

默认情况下 RestTemplate 存在的不足

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 是可变的,那就很不方便了。

三、扩展 RestTemplate

注意,下面涉及到的代码都是基于 spring-web 4.2.6.RELEASE 版本

设置 Query Params

上面提到,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 的功能。

设置自定义的 HTTP Header

实现这个需求有多种方法,比如通过拦截器。这里使用另一个方法,通过一个自定义的 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 Client 实现

RestTemplate 本身并没有做 HTTP 底层的实现,而是利用了现有的技术,如 JDK 或 Apache HttpClient 等。

RestTemplate 需要使用一个实现了 ClientHttpRequestFactory 接口的类为其提供 ClientHttpRequest 实现(另外还有 AsyncClientHttpRequestFactory 对应于异步 HTTP 实现,这里暂且不表)。而 ClientHttpRequest 则实现封装了组装、发送 HTTP 消息,以及解析响应的的底层细节。

目前(4.2.6.RELEASE)的 RestTemplate 主要有四种 ClientHttpRequestFactory 的实现,它们分别是:

  • 基于 JDK HttpURLConnection 的 SimpleClientHttpRequestFactory
  • 基于 Apache HttpComponents Client 的 HttpComponentsClientHttpRequestFactory
  • 基于 OkHttp 2(OkHttp 最新版本为 3,有较大改动,包名有变动,不和老版本兼容)的 OkHttpClientHttpRequestFactory
  • 基于 Netty4 的 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 来实现扩展。

后记

根据我上面表达的思想,一个统一、规范和简化 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 的思想、原理和优点(原理其实不复杂,但是能想到这么做的却没几个,原创的创新思想永远是最可贵的)

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Spring RestTemplate具体使用详解

    1.什么是REST? REST(RepresentationalState Transfer)是Roy Fielding 提出的一个描述互联系统架构风格的名词.REST定义了一组体系架构原则,您可以根据这些原则设计以系统资源为中心的Web 服务,包括使用不同语言编写的客户端如何通过 HTTP处理和传输资源状态. 为什么称为 REST?Web本质上由各种各样的资源组成,资源由URI 唯一标识.浏览器(或者任何其它类似于浏览器的应用程序)将展示出该资源的一种表现方式,或者一种表现状态.如果用户在该页

  • Springboot RestTemplate 简单使用解析

    前言 spring框架提供的RestTemplate类可用于在应用中调用rest服务,它简化了与http服务的通信方式,统一了RESTful的标准,封装了http链接, 我们只需要传入url及返回值类型即可. 相较于之前常用的HttpClient,RestTemplate是一种更优雅的调用RESTful服务的方式.该类主要用到的函数有:exchange.getForEntity.postForEntity等.我主要用的是后面两个函数,来执行发送get跟post请求. 首先是RestTemplat

  • SpringBoot RestTemplate 简单包装解析

    RestTemplate设计是为了Spring更好的请求并解析Restful风格的接口返回值而设计的,通过这个类可以在请求接口时直接解析对应的类. 在SpringBoot中对这个类进行简单的包装,变成一个工具类来使用,这里用到的是getForEntity和postForEntity方法,具体包装的代码内容 如下: package cn.eangaie.demo.util; import com.alibaba.fastjson.JSONObject; import org.springframe

  • 基于RestTemplate的使用方法(详解)

    1.postForObject :传入一个业务对象,返回是一个String 调用方: BaseUser baseUser=new BaseUser(); baseUser.setUserid(userid); baseUser.setPass(pass); String postForObject = restTemplate.postForObject(this.getURL()+"/user/login", baseUser, String.class); return postF

  • 详解SpringBoot中RestTemplate的几种实现

    RestTemplate的多种实现 使用JDK默认的http library 使用Apache提供的httpclient 使用Okhttp3 @Configuration public class RestConfig { @Bean public RestTemplate restTemplate(){ RestTemplate restTemplate = new RestTemplate(); return restTemplate; } @Bean("urlConnection"

  • 关于RestTemplate的使用深度解析

    目录 一.概述 选择一个优秀的 HTTP Client 的重要性 优秀的 HTTP Client 需要具备的特性 连接池 超时时间设置(连接超时.读取超时等) 是否支持异步 请求和响应的编解码 可扩展性 答案 二.使用 RestTemplate 的缺点 依赖 Spring 其它模块 默认情况下 RestTemplate 存在的不足 三.扩展 RestTemplate 设置 Query Params 设置自定义的 HTTP Header 简化配置 四.RestTemplate 原理解析 HTTP

  • 深度解析MySQL启动时报“The server quit without updating PID file”错误的原因

    很多童鞋在启动mysql的时候,碰到过这个错误, 首先,澄清一点,出现这个错误的前提是:通过服务脚本来启动mysql.通过mysqld_safe或mysqld启动mysql实例并不会报这个错误. 那么,出现这个错误的原因具体是什么呢? 哈哈,对分析过程不care的童鞋可直接跳到文末的总结部分~ 总结 下面,来分析下mysql的服务启动脚本 脚本完整内容如下: #!/bin/sh # Copyright Abandoned 1996 TCX DataKonsult AB & Monty Progr

  • JavaScript中 this 指向问题深度解析

    JavaScript 中的 this 指向问题有很多文章在解释,仍然有很多人问.上周我们的开发团队连续两个人遇到相关问题,所以我不得不将关于前端构建技术的交流会延长了半个时候讨论 this 的问题. 与我们常见的很多语言不同,JavaScript 函数中的 this 指向并不是在函数定义的时候确定的,而是在调用的时候确定的.换句话说, 函数的调用方式决定了 this 指向 . JavaScript 中,普通的函数调用方式有三种:直接调用.方法调用和 new 调用.除此之外,还有一些特殊的调用方式

  • 原理深度解析Vue的响应式更新比React快

    前言 我们都知道 Vue 对于响应式属性的更新,只会精确更新依赖收集的当前组件,而不会递归的去更新子组件,这也是它性能强大的原因之一. 例子 举例来说 这样的一个组件: <template> <div> {{ msg }} <ChildComponent /> </div> </template> 我们在触发 this.msg = 'Hello, Changed~'的时候,会触发组件的更新,视图的重新渲染. 但是 <ChildCompone

  • 深度解析Django REST Framework 批量操作

    我们都知道Django rest framework这个库,默认只支持批量查看,不支持批量更新(局部或整体)和批量删除. 下面我们来讨论这个问题,看看如何实现批量更新和删除操作. DRF基本情况 我们以下面的代码作为例子: models: from django.db import models # Create your models here. class Classroom(models.Model): location = models.CharField(max_length=128)

  • 深度解析C语言中的变量作用域、链接和存储期的含义

    在c中变量有三种性质: 1.存储期限:变量的存储期限决定了变量占用的内存空间什么时候会被释放,具有动态存储期限的变量会在所属的程序块被执行时获得内存空间,在结束时释放内存空间.具有静态存储期限的变量在程序运行的整个期间都会占用内存空间. 2.作用域:变量有块作用域也有文件作用域,结合序章第一张图可以明白块作用域是在某些程序块内起作用,文件作用域是在整个c文件之内起作用. 3.链接:链接是各个文件之间的关系,具有内部链接的变量只在本文件内起作用,具有外部链接的变量可以在不同文件内起作用.具有无链接

  • Java中关于String StringBuffer StringBuilder特性深度解析

    1.String String类:字符串是常量,使用一对""引起来表示.他们的值在创建之后不能修改. 1.String声明为final的,不可被继承 2.String实现了Serializable接口,表示字符串时支持序列化的. 实现了Comparable接口:表示String可以比较大小 3.String内部定义了final char[] value用于存储字符串数据 4.String:代表不可变的字符序列.简称:不可变性 体现: 1.当对字符串重新赋值时,需要重写指定内存区域赋值,

  • Java面向对象的封装特征深度解析

    目录 面向对象三大特征 封装 private关键字--实现类封装 访问器方法和更改器方法 包--类的集合 导入包 从人的角度理解包 不加访问权限--实现包封装 总结 在上一篇文章中,我们了解了面向对象的基础内容,这一篇将会更加深入地了解面向对象的特征. 面向对象三大特征 面向对象语言有三大特征: 封装 继承 多态 封装 对一个类实现封装,意味着限制其它类对该类数据的访问. 简单来讲,封装就是隐藏数据,就是保护对象的数据.对象,听起来总是那么地抽象,为了更好地理解封装,我将对象具体指向人,从人的角

  • 深度解析SpringBoot中@Async引起的循环依赖

    目录 事故时间线 猜想 什么是循环依赖 什么是@Async 啊,昨晚发版又出现了让有头大的循环依赖问题,按理说Spring会为我们解决循环依赖,但是为什么还会出现这个问题呢?为什么在本地.UAT以及PRE环境都没有出现这个问题,但是到了PROD环境就出现了这个问题呢?本文将从事故时间线.及时止损.复盘分析等几个方面为大家带来详细的分析,干货满满! 事故时间线 本着"先止损.后复盘分析"的原则,我们来看一下这次发版事故的时间线. 2021年11月16日晚23点00分00秒开始发版,此时集

  • 深度解析Python线程和进程

    目录 什么是进程 什么是线程 线程与进程的区别 并行与并发 Python中的多进程 Python中进程操作 线程 Python的threading模块 锁Lock: 全局解释器锁(GIL) 参考文章: 什么是进程 进程就是操作系统中执行的一个程序,操作系统以进程为单位分配存储空间,每个进程都有自己的地址空间.数据栈以及其他用于跟踪进程执行的辅助数据,操作系统管理所有进程的执行,为它们合理的分配资源. 每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信.由于进程比较重量,占据独立的内存,

随机推荐