【Spring-web】RestTemplate源码学习

2016-12-22   by 安静的下雪天  http://www.cnblogs.com/quiet-snowy-day/p/6210288.html

前言

在Web开发工作中,有一部分开发任务是不需要写web页面的。比如,本地服务在集成某些第三方的功能的时候(访问其他RESTful资源),通过转发URL请求到第三方服务,获取应答信息。这些应答信息不需要渲染到画面上,而是返回给客户端(APP或者其他web应用)。本地服务对于第三方服务来说是客户端;对于整体系统而言,就像是一个中转站。
这种开发内容除了业务逻辑,剩下的基本都是套路代码,而Spring从3.0版本开始,为我们提供了封装好的访问HTTP的模板代码RestTemplate。

看了公司的几套服务代码,都是自己封装的HttpClient\HttpRequest\HttpResponse,封装的代码中并没有什么特殊之处,不明白为什么没有使用框架提供的类。而且,基本上每个工程里都有重复实现的封装代码,我问经理是不是打成jar包来引用更方便,经理说他不喜欢引用jar包。? Are you just kidding me?
本来还想提出一些改进建议,但是现在的系统能够正常平稳的运行,考虑了一下还是算了。呵呵————

虽然这些服务是随着业务逐步添加的,不可能在一开始就设计那么完美,但是我真的忍不住想吐槽。与上一家公司相比,真心觉得在代码规范和技术管理上需要加强。

练习使用了一下RestTemplate,顺便学学Spring源码。先来看看Java Doc

/**
 * <strong>Spring‘s central class for synchronous client-side HTTP access.</strong>
 * It simplifies communication with HTTP servers, and enforces RESTful principles.
 * It handles HTTP connections, leaving application code to provide URLs
 * (with possible template variables) and extract results.
 *
 * <p><strong>Note:</strong> by default the RestTemplate relies on standard JDK
 * facilities to establish HTTP connections. You can switch to use a different
 * HTTP library such as Apache HttpComponents, Netty, and OkHttp through the
 * {@link #setRequestFactory} property.
 *
 * <p>The main entry points of this template are the methods named after the six main HTTP methods:
 * <table>
 * <tr><th>HTTP method</th><th>RestTemplate methods</th></tr>
 * <tr><td>DELETE</td><td>{@link #delete}</td></tr>
 * <tr><td>GET</td><td>{@link #getForObject}</td></tr>
 * <tr><td></td><td>{@link #getForEntity}</td></tr>
 * <tr><td>HEAD</td><td>{@link #headForHeaders}</td></tr>
 * <tr><td>OPTIONS</td><td>{@link #optionsForAllow}</td></tr>
 * <tr><td>POST</td><td>{@link #postForLocation}</td></tr>
 * <tr><td></td><td>{@link #postForObject}</td></tr>
 * <tr><td>PUT</td><td>{@link #put}</td></tr>
 * <tr><td>any</td><td>{@link #exchange}</td></tr>
 * <tr><td></td><td>{@link #execute}</td></tr> </table>
 *
 * <p>In addition the {@code exchange} and {@code execute} methods are generalized versions of
 * the above methods and can be used to support additional, less frequent combinations (e.g.
 * HTTP PATCH, HTTP PUT with response body, etc.). Note however that the underlying HTTP
 * library used must also support the desired combination.
 *
 * <p>For each HTTP method there are three variants: two accept a URI template string
 * and URI variables (array or map) while a third accepts a {@link URI}.
 * Note that for URI templates it is assumed encoding is necessary, e.g.
 * {@code restTemplate.getForObject("http://example.com/hotel list")} becomes
 * {@code "http://example.com/hotel%20list"}. This also means if the URI template
 * or URI variables are already encoded, double encoding will occur, e.g.
 * {@code http://example.com/hotel%20list} becomes
 * {@code http://example.com/hotel%2520list}). To avoid that use a {@code URI} method
 * variant to provide (or re-use) a previously encoded URI. To prepare such an URI
 * with full control over encoding, consider using
 * {@link org.springframework.web.util.UriComponentsBuilder}.
 *
 * <p>Internally the template uses {@link HttpMessageConverter} instances to
 * convert HTTP messages to and from POJOs. Converters for the main mime types
 * are registered by default but you can also register additional converters
 * via {@link #setMessageConverters}.
 *
 * <p>This template uses a
 * {@link org.springframework.http.client.SimpleClientHttpRequestFactory} and a
 * {@link DefaultResponseErrorHandler} as default strategies for creating HTTP
 * connections or handling HTTP errors, respectively. These defaults can be overridden
 * through {@link #setRequestFactory} and {@link #setErrorHandler} respectively.
 *
 * @author Arjen Poutsma
 * @author Brian Clozel
 * @author Roy Clarkson
 * @author Juergen Hoeller
 * @since 3.0
 * @see HttpMessageConverter
 * @see RequestCallback
 * @see ResponseExtractor
 * @see ResponseErrorHandler
 * @see AsyncRestTemplate
 */

Java Doc

尝试翻译如下:

RestTemplate是 Spring中客户端同步访问HTTP的核心类。它简化了与HTTP服务器的通信,执行RESTful原则。

它能处理HTTP链接,委托应用程序代码(使用合适的模板变量)来装配URL,并提取应答信息。

注意:默认情况下,RestTemplate依赖标准JDK工具来创建HTTP链接。通过设置(HttpAccessor.setRequestFactory)属性,你可以转而使用像Apache HttpComponents、Netty、OkHttp这样的HTTP库。

该模板类的主要切入点为以下几个方法(并对应着HTTP的六个主要方法):

另外,exchange和execute方法提供了以上方法的通用版本,用来支持额外的、不常用的组合(如:HTTP PATCH,带有消息体的HTTP PUT,等等)。注意,无论怎样使用底层HTTP库,都必须支持必要的组合。

对于每个HTTP方法都有3个变体:

其中两个方法的接收参数是URI模式字符串和URI变量(array or map),第三个的接收参数是java.net.URI。

注意,需要为URI模式串假定编码格式,如:restTemplate.getForObject("http://example.com/hotel list") 变为 "http://example.com/hotel%20list"。

这同时也意味着,如果URI模式串或URI变量已经编码,会产生重复编码,如:"http://example.com/hotel%20list" 变成了 "http://example.com/hotel%2520list"。

为了避免使用URI方法变体来提供(或重用)预编码的URI,可以考虑使用UriComponentsBuilder,来制定可以完全控制编码的URI。

在模板内部使用HttpMessageConverter实例来实现HTTP消息与POJO类的相互转换。

主要的MIME类型的转换器已经默认注册,你也可以通过setMessageConverters(List<HttpMessageConverter<?>>)方法来注册额外的转换器。

本模板分别使用SimpleClientHttpRequestFactory 和 DefaultResponseErrorHandler作为默认策略,来创建HTTP链接、处理HTTP错误。通过HttpAccessor.setRequestFactory(ClientHttpRequestFactory) 和 setErrorHandler(ResponseErrorHandler)方法可以分别覆盖之前的默认设置。

补充:

重载的3个方法怎么选择呢?建议选择URL参数类型为String的那个两个方法。因为当这个参数是一个非URI格式的,需要进行转换,而URI的构造函数会抛出一个检查异常URISyntaxException,该异常必须捕获。另外两个重载方法则避免了捕获异常,所以上面表格中推荐的方法的第一个参数都是String类型。


写了两个测试方法,博主喜欢用Junit ?

package com.practice;

import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

import junit.framework.TestCase;
import net.sf.json.JSONObject;

import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

public class RestTemplateTest extends TestCase {

    RestTemplate restTpl;

    @Before
    public void setUp() {
        restTpl = new RestTemplate();
    }

    @Test
    public void testGet() {
        Map<String, Object> paramsMap = new HashMap<String, Object>();
        paramsMap.put("cityCode", "xxxxxxxxxx");
        paramsMap.put("key", "xxxxxxxxxxxxxxxxxxxxx");

        String url = "http://xxx.xxx.xxx.xxx:8080/xxx/xxxx?xxxx&config=xxx&cityCode={cityCode}&key={key}";
        String respStr = restTpl.getForObject(url, String.class, paramsMap);
        System.out.println(respStr);

        JSONObject respJson = restTpl.getForObject(url, JSONObject.class, paramsMap);
        System.out.println(respJson);
    }

    @Test
    public void testPost() throws Exception {
        String posturl = "http://xxx.xxx.xxx.xxx:8080/xxxx/xxxx/xxxxx";

        JSONObject metadata = new JSONObject();
        metadata.put("dddddd", "xxxxx");
        metadata.put("ssssss", "xxxxxx");
        metadata.put("flag", true);

        JSONObject paramsJson = new JSONObject();
        paramsJson.put("mmmmm", "mmm");
        paramsJson.put("nnnnn", "nnn");
        paramsJson.put("password", "xxxxxxxxxxx");
        paramsJson.put("metadata", metadata);

        String params = "RequestJson=" + URLEncoder.encode(paramsJson.toString(), "utf-8");

        MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
        headers.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");

        HttpEntity<Object> hpEntity = new HttpEntity<Object>(params, headers);

        ResponseEntity<String> respEntity = restTpl.postForEntity(posturl, hpEntity, String.class);
        System.out.println(respEntity);
    }

}

然而,就是这么简单的几行代码还不断出错~~~~~~~

之前postForEntity方法的第二个参数(请求消息体的类型)直接使用了JSONObject对象,发生了以下错误,

查看了spring-web源码,发现源码工程中引用的jackson-databind包的版本是2.8.1。改了jar包版本后,可以继续运行了。

然而,又错了,见下图。

这次是因为请求信息不完整,测试的服务端对POST消息体的格式有要求,而我没有设置Content-Type,请求头的信息只有Accept和Content-Length两项。

创建Map对象并设定头信息,改用HttpEntity作为发送对象,终于顺利执行了。



那么,RestTemplate中是如何处理请求头信息的呢?

以GET方法为例,getForObject()方法中有这么一句【RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);】

其中 acceptHeaderRequestCallback 方法返回了 AcceptHeaderRequestCallback 类的实例。

AcceptHeaderRequestCallback 是 RestTemplate 的内部类,实现了RequestCallback接口,该接口只有一个方法doWithRequest。

AcceptHeaderRequestCallback 实现了doWithRequest 方法:根据响应实体类型responseType,遍历所有的消息转换器,找到适合的,然后再从这些转换器中找到所有支持的媒体类型,最后将所有支持的媒体类型设置到请求头部Accept中。

再来看POST方法,postForEntity()方法中有【RequestCallback requestCallback = httpEntityCallback(request, responseType);】

其中 httpEntityCallback 方法返回了 HttpEntityRequestCallback 类的实例。

HttpEntityRequestCallback 也是RestTemplate 的内部类,它继承了 AcceptHeaderRequestCallback 类,重写了doWithRequest方法:首先调用父类doWithRequest方法,完成请求头部设定;然后,根据响应实体类型,通过遍历找到适合的消息转换器;最后通过消息转换器将POST消息体写入请求实体中。

这里为什么使用内部类呢?复习下内部类的作用。

1. 隐藏了实现细节。AcceptHeaderRequestCallback 和 HttpEntityRequestCallback 的访问限定都是private;另外,在内部类中实现接口,而不是由 RestTemplate 类implement 多个接口,也就避免了对外部调用者暴露 doWithRequest 方法及其实现。

2. 内部类可以访问包含类的所有元素。在内部类的 doWithRequest 方法中访问了 RestTemplate 类的成员变量messageConverters,该变量是所有消息转换器的列表。

3. 多继承,这里是内部类的之间有继承关系,也算间接多继承?RestTemplate 类是继承了 InterceptingHttpAccessor 类,实现了 RestOperations 接口。

4. 避免因父类和实现接口中有重名的方法,而进行不必要的修改。这点到是没有明显体现,RestTemplate的继承类和两个接口中没有重名的方法。


请求和响应消息解析是怎么解析的?

是否用到某种设计模式?

RestTemplate发送的是同步请求,那么异步请求是如何处理呢?

感觉这个坑越挖越深,需要花点时间好好看看……

不用发愁博客没有内容可写了:P

时间: 2024-10-06 14:02:39

【Spring-web】RestTemplate源码学习的相关文章

Spring源码学习笔记(3)

Spring源码学习笔记(三) 前言----     最近花了些时间看了<Spring源码深度解析>这本书,算是入门了Spring的源码吧.打算写下系列文章,回忆一下书的内容,总结代码的运行流程.推荐那些和我一样没接触过SSH框架源码又想学习的,阅读郝佳编著的<Spring源码深度解析>这本书,会是个很好的入门. DispatcherServlet 实现核心功能 和普通的 Servelt 类一样, DispatcherServlet 中的 doGet() 和 doPost() 方法

spring源码学习(1)——spring整体架构和设计理念

Spring是在Rod Johnson的<Expert One-On-One J2EE Development and Design >的基础上衍生而来的.主要目的是通过使用基本的javabean来完成以前只能用EJB完成的事情降低企业应用的复杂性.这一系列源码学习是基于Spring-4.3.11版本的. 一.Spring的整体架构 如图所示,spring可以被总结为一下几个部分: (1)Core Container 为Spring的核心容器,包含Beans,Core,Context和SpEL

Spring源码学习笔记(1)

SpringMVC源码学习笔记(一) 前言----   最近花了些时间看了<Spring源码深度解析>这本书,算是入门了Spring的源码吧.打算写下系列文章,回忆一下书的内容,总结代码的运行流程.推荐那些和我一样没接触过SSH框架源码又想学习的,阅读郝佳编著的<Spring源码深度解析>这本书,会是个很好的入门.  进入正文,首先贴上SpringMVC的图片(来自https://docs.spring.io/spring/docs/current/spring-framework

Spring 源码学习(二) IOC容器启动过程

这一节主要是记录一下Spring Ioc 容器的启动过程. Spring 的 Ioc 容器是怎么被加载和使用的? web容器为它提供了宿主环境 ServlectContext,  Tomcat 启动时会读取web.xml. 并且实例化web.xml中配置的ContextLoaderListener ,下面看一下ContextLoaderListener的创建过程: 在实例化ContextLoaderListener 之后,通过接口回调执行ContextLoaderListener 类中的cont

Spring源码学习笔记(6)

Spring源码学习笔记(六) 前言-- 最近花了些时间看了<Spring源码深度解析>这本书,算是入门了Spring的源码吧.打算写下系列文章,回忆一下书的内容,总结代码的运行流程.推荐那些和我一样没接触过SSH框架源码又想学习的,阅读郝佳编著的<Spring源码深度解析>这本书,会是个很好的入门. 上一篇中我们梳理到 Spring 加载 XML 配置文件, 完成 XML 的解析工作,接下来我们将进入 Spring 加载 bean 的逻辑. 我们使用 Spring 获取 XML

Spring源码学习的初步体会

Spring源码学习的初步体会: 深入学习和巩固java的基础知识,其中的java知识范围全部,可以边研究源码边巩固复习基础知识 体会其中用到的设计思想:其中包含的设计原则和设计模式. 加深对spring的理解,在业务开发中使用spring更容易和深入,提高了生产率.

Spring源码学习笔记(5)

Spring源码学习笔记(五) 前言-- 最近花了些时间看了<Spring源码深度解析>这本书,算是入门了Spring的源码吧.打算写下系列文章,回忆一下书的内容,总结代码的运行流程.推荐那些和我一样没接触过SSH框架源码又想学习的,阅读郝佳编著的<Spring源码深度解析>这本书,会是个很好的入门 写下一句话,开篇不尴尬  ----  上篇文章中梳理到 Spring 加载资源文件后开始解析 Bean, 现在我们从两个解析函数 parseDefaultElement() 和 par

Spring源码学习笔记(7)

Spring源码学习笔记(七) 前言-- 最近花了些时间看了<Spring源码深度解析>这本书,算是入门了Spring的源码吧.打算写下系列文章,回忆一下书的内容,总结代码的运行流程.推荐那些和我一样没接触过SSH框架源码又想学习的,阅读郝佳编著的<Spring源码深度解析>这本书,会是个很好的入门 写前说句话, 开篇不尴尬 ---- 接下的这一篇当中, 我们将来回顾 Spring 中 AOP 功能的实现流程.  早上精力充沛, 开始新一天的学习 \(^o^)/~ 接触过 Spri

Spring源码学习-容器BeanFactory(一) BeanDefinition的创建-解析资源文件

写在前面 从大四实习至今已一年有余,作为一个程序员,一直没有用心去记录自己工作中遇到的问题,甚是惭愧,打算从今日起开始养成写博客的习惯.作为一名java开发人员,Spring是永远绕不过的话题,它的设计精巧,代码优美,值得每一名开发人员学习阅读. 在我最开始学习javaEE时,第一次接触Spring是从一个S(Struts)S(Spring)H(Herbinate)的框架开始.由java原生开发到框架开发转换过程中,那时我的印象里Struts负责控制层,herbinate负责数据层,而Sprin