RestTemplate
示例
服务端
@RequestMapping("test")
public String test(@RequestParam("uid") int uid) {
logger.info("uid = {}", uid);
return "test";
}
客户端
代码一
public void send() {
Map params = new HashMap();
params.put("uid", 123);
restTemplate.postForObject("http://localhost:8080/test", params, String.class, new HashMap());
}
代码二
public void send() {
MultiValueMap params = new LinkedMultiValueMap();
params.add("uid", 123);
restTemplate.postForObject("http://localhost:8080/test", params, String.class, new HashMap());
}
经过测试发现代码二的请求能被服务端正常处理,而代码一则提示uid参数未能提供。
分析
客户端
public T postForObject(String url, Object request, Class responseType, Map uriVariables)
throws RestClientException {
RequestCallback requestCallback = httpEntityCallback(request, responseType);
HttpMessageConverterExtractor responseExtractor =
new HttpMessageConverterExtractor(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
可以看到不管是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
很明显,程序会执行第一个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());
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如下
在遍历过程中会先检查MessageConvertor是否为GenericHttpMessageConverter的子类,如果是则判断是否可以写入,如果能写入则执行写入操作并返回,否则直接判断能否写入,能则执行写入操作
GenericHttpMessageConverter genericMessageConverter = (GenericHttpMessageConverter) 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) 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
做了两件事情
- 将MediaType置为application/x-www-form-urlencoded(示例程序中)
- 将request中的key value通过&=拼接并写入到body中
GsonHttpMessageConverter就不截图了,也做了两件事情
- 将MediaType置为application/json;charset=UTF-8(示例程序中)
- 将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方法中部分代码如下
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无法处理。