RestTemplate分析

RestTemplate

示例

服务端
@RequestMapping("test")
    public String test(@RequestParam("uid") int uid) {
        logger.info("uid = {}", uid);
        return "test";
    }
客户端

代码一

public void send() {
        Map<String, Object> params = new HashMap<String, Object>();
        params.put("uid", 123);
        restTemplate.postForObject("http://localhost:8080/test", params, String.class, new HashMap<String, Object>());
    }

代码二

public void send() {
        MultiValueMap<String, Object> params = new LinkedMultiValueMap<String, Object>();
        params.add("uid", 123);
        restTemplate.postForObject("http://localhost:8080/test", params, String.class, new HashMap<String, Object>());
    }

经过测试发现代码二的请求能被服务端正常处理,而代码一则提示uid参数未能提供。

分析

客户端
public <T> T postForObject(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables)
            throws RestClientException {

        RequestCallback requestCallback = httpEntityCallback(request, responseType);
        HttpMessageConverterExtractor<T> responseExtractor =
                new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger);
        return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
    }

可以看到request和responseType被封装成了一个HttpEntityRequestCallback对象,继续跟HttpEntityRequestCallback源码

private HttpEntityRequestCallback(Object requestBody, Type responseType) {
            super(responseType);
            if (requestBody instanceof HttpEntity) {
                this.requestEntity = (HttpEntity<?>) requestBody;
            }
            else if (requestBody != null) {
                this.requestEntity = new HttpEntity<Object>(requestBody);
            }
            else {
                this.requestEntity = HttpEntity.EMPTY;
            }
        }

可以看到不管是HashMap还是MultiValueMap,request部分最终都被第二个if处理,即放在了HttpEntity的body中。

接下来看HttpEntityRequestCallback的剩余部分代码

public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
            super.doWithRequest(httpRequest);
            if (!this.requestEntity.hasBody()) {
                HttpHeaders httpHeaders = httpRequest.getHeaders();
                HttpHeaders requestHeaders = this.requestEntity.getHeaders();
                if (!requestHeaders.isEmpty()) {
                    httpHeaders.putAll(requestHeaders);
                }
                if (httpHeaders.getContentLength() < 0) {
                    httpHeaders.setContentLength(0L);
                }
            }
            else {
                Object requestBody = this.requestEntity.getBody();
                Class<?> requestBodyClass = requestBody.getClass();
                Type requestBodyType = (this.requestEntity instanceof RequestEntity ?
                        ((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass);
                HttpHeaders requestHeaders = this.requestEntity.getHeaders();
                MediaType requestContentType = requestHeaders.getContentType();
                for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
                    if (messageConverter instanceof GenericHttpMessageConverter) {
                        GenericHttpMessageConverter<Object> genericMessageConverter = (GenericHttpMessageConverter<Object>) messageConverter;
                        if (genericMessageConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {
                            if (!requestHeaders.isEmpty()) {
                                httpRequest.getHeaders().putAll(requestHeaders);
                            }
                            if (logger.isDebugEnabled()) {
                                if (requestContentType != null) {
                                    logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
                                            "\" using [" + messageConverter + "]");
                                }
                                else {
                                    logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
                                }

                            }
                            genericMessageConverter.write(
                                    requestBody, requestBodyType, requestContentType, httpRequest);
                            return;
                        }
                    }
                    else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {
                        if (!requestHeaders.isEmpty()) {
                            httpRequest.getHeaders().putAll(requestHeaders);
                        }
                        if (logger.isDebugEnabled()) {
                            if (requestContentType != null) {
                                logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
                                        "\" using [" + messageConverter + "]");
                            }
                            else {
                                logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
                            }

                        }
                        ((HttpMessageConverter<Object>) messageConverter).write(
                                requestBody, requestContentType, httpRequest);
                        return;
                    }
                }
                String message = "Could not write request: no suitable HttpMessageConverter found for request type [" +
                        requestBodyClass.getName() + "]";
                if (requestContentType != null) {
                    message += " and content type [" + requestContentType + "]";
                }
                throw new RestClientException(message);
            }
        }

很明显,程序会执行第一个else中的逻辑,我们分解来看

Object requestBody = this.requestEntity.getBody();
                Class<?> requestBodyClass = requestBody.getClass();
                Type requestBodyType = (this.requestEntity instanceof RequestEntity ?
                        ((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass);
                HttpHeaders requestHeaders = this.requestEntity.getHeaders();
                MediaType requestContentType = requestHeaders.getContentType();

requestBody拿到的就是我们的request部分,即HashMap或MultiValueMap。requestBodyClass和requestBodyType都取决于我们传递的request。MediaType为null。

继续分析,接下来会遍历所有的HttpMessageConverter,这些对象在RestTemplate的构造函数中被初始化

public RestTemplate() {
        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(new StringHttpMessageConverter());
        this.messageConverters.add(new ResourceHttpMessageConverter());
        this.messageConverters.add(new SourceHttpMessageConverter<Source>());
        this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());

        if (romePresent) {
            this.messageConverters.add(new AtomFeedHttpMessageConverter());
            this.messageConverters.add(new RssChannelHttpMessageConverter());
        }

        if (jackson2XmlPresent) {
            this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
        }
        else if (jaxb2Present) {
            this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
        }

        if (jackson2Present) {
            this.messageConverters.add(new MappingJackson2HttpMessageConverter());
        }
        else if (gsonPresent) {
            this.messageConverters.add(new GsonHttpMessageConverter());
        }
    }

实际测试中,我们加入了Gson的依赖,实际的MessageConvertor如下

messageconvertors.png

在遍历过程中会先检查MessageConvertor是否为GenericHttpMessageConverter的子类,如果是则判断是否可以写入,如果能写入则执行写入操作并返回,否则直接判断能否写入,能则执行写入操作

GenericHttpMessageConverter<Object> genericMessageConverter = (GenericHttpMessageConverter<Object>) messageConverter;
                        if (genericMessageConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {
                            if (!requestHeaders.isEmpty()) {
                                httpRequest.getHeaders().putAll(requestHeaders);
                            }
                            genericMessageConverter.write(
                                    requestBody, requestBodyType, requestContentType, httpRequest);
                            return;
                        }
}
else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {
                        if (!requestHeaders.isEmpty()) {
                            httpRequest.getHeaders().putAll(requestHeaders);
                        }
                        if (logger.isDebugEnabled()) {
                            if (requestContentType != null) {
                                logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
                                        "\" using [" + messageConverter + "]");
                            }
                            else {
                                logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
                            }

                        }
                        ((HttpMessageConverter<Object>) messageConverter).write(
                                requestBody, requestContentType, httpRequest);
                        return;
                    }

在这些MessageConvertor中只有GsonHttpMessageConverter是GenericHttpMessageConverter的子类,且排在最后。因此,遍历过程中会先判断前六个convertor,能写入则执行写入,最后才是GsonHttpMessageConvertor。接下来,我们依次分析这些HTTPMessageConvertor。

HttpMessageConvertor data type media type
ByteArrayHttpMessageConverter byte[] Null,application/octet-stream
StringHttpMessageConverter String Null,MediaType.TEXT_PLAIN, MediaType.ALL
ResourceHttpMessageConverter Resource Null,MediaType.ALL
Jaxb2RootElementHttpMessageConverter 被XmlRootElement注解标识 Null,MediaType.ALL
AllEncompassingFormHttpMessageConverter MultiValueMap子类 Null,MediaType.ALL,MediaType.APPLICATION_FORM_URLENCODED,MediaType.MULTIPART_FORM_DATA
GsonHttpMessageConverter all Null,MediaType.ALL,MediaType.APPLICATION_JSON_UTF8, new MediaType("application", "*+json", DEFAULT_CHARSET)

可以看到MultiValueMap子类的数据会被AllEncompassingFormHttpMessageConverter处理,而HashMap类型的数据会被最后的GsonHTTPMessageConvertor处理。

接下来看看AllEncompassingFormHttpMessageConverter和GsonHttpMessageConverter的write方法中分别写入了什么。

AllEncompassingFormHttpMessageConverter

FormHttpMessageConvertor.png

做了两件事情

  1. 将MediaType置为application/x-www-form-urlencoded(示例程序中)
  2. 将request中的key value通过&=拼接并写入到body中

GsonHttpMessageConverter就不截图了,也做了两件事情

  1. 将MediaType置为application/json;charset=UTF-8(示例程序中)
  2. 将request转成json并写入到body中

至此,我们知道了传MultiValueMap和HashMap的区别

request type media type body
MultiValueMap application/x-www-form-urlencoded &=拼接的字符串
HashMap application/json;charset=UTF-8 json字符串

自此,客户端的区别分析完毕。接下来看服务端。

服务端

先来看看RequestParam和RequestBody两个注解的区别

@RequestParam

用来处理Content-Type: 为 application/x-www-form-urlencoded编码的内容。(Http协议中,如果不指定Content-Type,则默认传递的参数就是application/x-www-form-urlencoded类型)

RequestParam可以接受简单类型的属性,也可以接受对象类型。
实质是将Request.getParameter() 中的Key-Value参数Map利用Spring的转化机制ConversionService配置,转化成参数接收对象或字段。

tip

Content-Type: application/x-www-form-urlencoded的请求中,
get 方式中queryString的值,和post方式中 body data的值都会被Servlet接受到并转化到Request.getParameter()参数集中,所以@RequestParam可以获取的到。

以tomcat为例,org.apache.catalina.connector.Request#parseParameters方法中部分代码如下

tomcat-request.png

Content-Type如果不是application/x-www-form-urlencoded,则直接返回。否则就会将body中的参数解析到HttpServletRequest的Parameters中,这样我们通过getParameter方法就可以拿到。

@RequestBody

处理HttpEntity传递过来的数据,一般用来处理非Content-Type: application/x-www-form-urlencoded编码格式的数据。

  • GET请求中,因为没有HttpEntity,所以@RequestBody并不适用。
  • POST请求中,通过HttpEntity传递的参数,必须要在请求头中声明数据的类型Content-Type,SpringMVC通过使用HandlerAdapter 配置的HttpMessageConverters来解析HttpEntity中的数据,然后绑定到相应的bean上。

关键点在于Content_Type,即在客户端分析中提到的MediaType。对于HashMap的request来说,其MediaType是json(GsonHTTPMessageConvertor处理),因此会被RequestBody所处理,RequestParam拿不到参数。而对于MultiValueMap的request来说,其MediaType是application/x-www-form-urlencoded(AllEncompassingFormHttpMessageConverter处理),因此会被RequestParam处理,RequestBody无法处理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 6.1 公钥密钥加密原理 6.1.1 基础知识 密钥:一般就是一个字符串或数字,在加密或者解密时传递给加密/解密算...
    AndroidMaster阅读 4,006评论 1 8
  • Spring的模型-视图-控制器(MVC)框架是围绕一个DispatcherServlet来设计的,这个Servl...
    alexpdh阅读 2,641评论 0 3
  • 一、简介 HttpClient是Apache基金会的一个开源网络库,功能十分强大,API数量众多,但正是由于庞大的...
    AndroidMaster阅读 3,041评论 0 4
  • 投资自己 我们终其一生的追求,就是要靠自己的努力,让围我而生存的人过得平安踏实。想要遇见更好的自己,...
    享受福袋阅读 207评论 0 0