背景

今天跟同事接口联调,使用RestTemplate请求服务端的post接口(使用python开发)。诡异的是,post请求,返回500 Internal Server Error,而使用get请求,返回正常。代码如下:

 HashMap<String, Object> hashMap = Maps.newHashMap();
hashMap.put("data", JSONObject.toJSONString(params));
url = "http://mydomain/dataDownLoad.cgi?data={data}";
json = restTemplate.getForObject(url, String.class, hashMap);
System.out.println("get json : " + json); url = "http://mydomain/dataDownLoad.cgi";
json = restTemplate.postForObject(url, hashMap, String.class);
System.out.println("hasmap post json : " + json);

结果为:

get json : {'status': 0, 'statusInfo': {'global': 'OK'}, 'data': 'http://mydomain/dataDownLoad.cgi?downLoadData=358300d5f9e1cc512efc178caaa0b061'}

500 Internal Server Error

最后经过另一位同学帮忙排查,发现RestTemplate在postForObject时,不可使用HashMap。而应该是MultiValueMap。改为如下:

MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();
paramMap.add("data", JSONObject.toJSONString(params));
url = "http://mydomain/dataDownLoad.cgi";
json = restTemplate.postForObject(url, paramMap, String.class);
System.out.println("post json : " + json);

结果为:

post json : {'status': 0, 'statusInfo': {'global': 'OK'}, 'data': 'http://mydomain/dataDownLoad.cgi?downLoadData=f2fc328513886e51b3b67d35043985ae'}

然后我想起之前使用RestTemplate发起post请求时,使用POJO作为参数,是可行的。再次测试:

url = "http://mydomain/dataDownLoad.cgi";
PostData postData = new PostData();
postData.setData(JSONObject.toJSONString(params));
json = restTemplate.postForObject(url, paramMap, String.class);
System.out.println("postData json : " + json);

返回:500 Internal Server Error。

到现在为止接口调通了。但问题的探究才刚刚开始。

  • RestTemplate的post参数为什么使用MultiValueMap而不能使用HashMap?
  • 为什么post接口,get请求也可以正确返回?
  • 为什么java服务端可以接收POJO参数,python服务端不可以?python服务端使用CGI(Common Gateway Interface),与cgi有关系吗?

何为MultiValueMap

IDEA中command+N,搜索类MultiValueMap,发现apache的commons-collections包有一个MultiValueMap类,spring-core包中有一个接口MultiValueMap,及其实现类LinkedMultiValueMap。显然看spring包。

首先看LinkedMultiValueMap,实现MultiValueMap接口,只有一个域:Map<K, List<V>> targetMap = new LinkedHashMap<K, List<V>>()。 其中value为new LinkedList<V>()。再看接口方法:

public interface MultiValueMap<K, V> extends Map<K, List<V>> {

	V getFirst(K key);  //targetMap.get(key).get(0)

	void add(K key, V value); //targetMap.get(key).add(value)

	void set(K key, V value); //targetMap.set(key, Lists.newLinkedList(value))

	void setAll(Map<K, V> values); //将普通map转为LinkedMultiValueMap

	Map<K, V> toSingleValueMap(); //只保留所有LinkedList的第一个值,转为LinkedHashMap

}

综上,LinkedMultiValueMap实际就是Key-LinkedList的map。

RestTemplate怎么处理post参数

首先查看RestTemplate源码,首先将请求封装成HttpEntityRequestCallback类对象,然后再处理请求。

Override
public <T> T postForObject(String url, Object request, Class<T> responseType, Object... uriVariables)
throws RestClientException {
//请求包装成httpEntityCallback
RequestCallback requestCallback = httpEntityCallback(request, responseType);
HttpMessageConverterExtractor<T> responseExtractor =
new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger);
//处理请求
return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
}

那么HttpEntityRequestCallback是什么样的呢?如下,实际是把请求数据放在了一个HttpEntity中。如果requestBody是HttpEntity类型,就直接转;否则,放在HttpEntity的body中。

//请求内容封装在一个HttpEntity对象中。
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;
}
}

接着看一下HttpEntity源码:

public class HttpEntity<T> {
private final HttpHeaders headers;
private final T body;
public HttpEntity(T body) {
this.body = body;
}
} public class HttpHeaders implements MultiValueMap<String, String>, Serializable{
......
}

至此,与MultiValueMap联系上了。

基于本次问题,我们不考虑post数据参数是HttpEntity类型的,只考虑普通POJO。那么,postForObject中对post数据的第一步处理,就是放在一个HttpEntity类型(header为MultiValueMap类型,body为泛型)的body中。

再看处理请求的部分:

Object requestBody = requestEntity.getBody();
Class<?> requestType = requestBody.getClass();
HttpHeaders requestHeaders = requestEntity.getHeaders();
MediaType requestContentType = requestHeaders.getContentType();
for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
if (messageConverter.canWrite(requestType, requestContentType)) {
if (!requestHeaders.isEmpty()) {
httpRequest.getHeaders().putAll(requestHeaders);
}
((HttpMessageConverter<Object>) messageConverter).write(
requestBody, requestContentType, httpRequest);
return;
}
}

通过配置的HttpMessageConverter来处理。

    <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
<constructor-arg ref="ky.clientHttpRequestFactory"/>
<property name="errorHandler">
<bean class="org.springframework.web.client.DefaultResponseErrorHandler"/>
</property>
<property name="messageConverters">
<list>
<bean class="org.springframework.http.converter.FormHttpMessageConverter"/>
<bean class="cn.com.autodx.common.jsonView.ViewAwareJsonMessageConverter"/>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
<value>application/json</value>
</list>
</property>
</bean>
</list>
</property>
</bean>

符合要求的只有ViewAwareJsonMessageConverter,其自定义处理如下。post数据中hashMap只含有data一个key,不含status字段,所以会跳过写的操作,即post请求带不上参数。如果修改代码,当不含status字段时,按照父类方法处理,则服务端可以得到参数。

protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
if(object instanceof Map) {
Map map = (Map)object;
HashMap statusInfo = new HashMap();
//不含有status字段,跳过
Object status = map.get("status");
if(status != null) {
int code = Integer.parseInt(String.valueOf(status));
if(0 != code) {
super.writeInternal(object, outputMessage);
} else {
statusInfo.put("global", "OK");
map.put("statusInfo", statusInfo);
super.writeInternal(object, outputMessage);
}
}
} else {
super.writeInternal(object, outputMessage);
}
}

而使用MultiValueMap会由FormHttpMessageConverter正确处理。

首先判断是否可以执行写操作,如果可以,执行写操作。

	@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
if (!MultiValueMap.class.isAssignableFrom(clazz)) {
return false;
}
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.isCompatibleWith(mediaType)) {
return true;
}
}
return false;
} @Override
@SuppressWarnings("unchecked")
public void write(MultiValueMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
if (!isMultipart(map, contentType)) { //LinkedList中是否含有多个数据
//只是普通的K-V,写form
writeForm((MultiValueMap<String, String>) map, contentType, outputMessage);
}
else {
writeMultipart((MultiValueMap<String, Object>) map, outputMessage);
}
}

既如此,那么post参数为POJO时,如何呢?

POJO也会被ViewAwareJsonMessageConverter处理,在其writeInternal中,object不是map,所以调用 super.writeInternal(object, outputMessage),如下:

@Override
protected void writeInternal(Object obj, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
OutputStream out = outputMessage.getBody();
String text = JSON.toJSONString(obj, features);
byte[] bytes = text.getBytes(charset);
out.write(bytes);
}

如果注释掉ViewAwareJsonMessageConverter,跟踪发现,会报错,返回没有合适的HttpMessageConverter处理。

使用ViewAwareJsonMessageConverter和使用FormHttpMessageConverter写数据的格式是不一样的,所以,post POJO后,会返回错误,但实际已将参数传递出去。

所以,对于我们配置的RestTemplate来说,post参数可以是map(有字段要求),也可以是POJO。即,输入输出数据由RestTemplate配置的messageConverters决定。

至此,我们已经清楚了第一个问题,剩下的问题同样的思路。跟踪一下getForObject的处理路径。get方式请求时,把所有的参数拼接在url后面,发给服务端,就可以把参数带到服务端。

剩下的问题就是python服务端是怎么处理请求的。首先研究一下CGI。

何为CGI

通用网关接口(CGI,Common Gateway Interface)是一种Web服务器和服务器端程序进行交互的协议。CGI完全独立于编程语言,操作系统和Web服务器。这个协议可以用vb,c,php,python 来实现。

工作方式如图所示:

browser->webServer: HTTP protocol
webServer->CGI脚本: 通过CGI管理模块调用脚本
CGI脚本->CGI脚本: 执行脚本程序
CGI脚本->webServer: 返回结果
webServer->browser: HTTP protocol

web服务器获取了请求cgi服务的http请求后,启动cgi脚本,并将http协议参数和客户端请求参数转为cgi协议的格式,传给cgi脚本。cgi脚本执行完毕后,将数据返回给web服务器,由web服务器返回给客户端。

cgi脚本怎么获取参数呢?

  • CGI脚本从环境变量QUERY_STRING中获取GET请求的数据
  • CGI脚本从stdin(标准输入)获取POST请求的数据,数据长度存在环境变量CONTENT_LENGTH中。

了解CGI大概是什么东东后,看一下python实现的CGI

python的CGI模块,要获取客户端的post参数,可以使用cgi.FieldStorage()方法。FieldStorage相当于python中的字典,支持多个方法。可以支持一般的key-value,也可以支持key-List<Value>,即类似于MultiValueMap形式的参数(如多选的表单数据)。

参考资料:

我所了解的cgi

Web是如何运行的: HTTP 和 CGI

总结

至此,本问题主要是在于程序怎么传递参数,对于spring restTemplate而言,就是messageConverters怎么配置的。

——————————————————

喵喵还要多努力学习啊~

RestTemplate post如何传递参数的更多相关文章

  1. Java RestTemplate传递参数

    最近使用Spring 的 RestTemplate 工具类请求接口的时候发现参数传递的一个坑,也就是当我们把参数封装在Map里面的时候,Map 的类型选择. 使用RestTemplate post请求 ...

  2. Feign发送Get请求时,采用POJO对象传递参数的最终解决方案 Request method 'POST' not supported (附带其余好几个坑)

    yml: feign: httpclient: enabled: true properties: #feign feign.httpclient.enabled=true <!-- https ...

  3. Vue 给子组件传递参数

    Vue 给子组件传递参数 首先看个例子吧 原文 html <div class="container" id="app"> <div clas ...

  4. [转] C++的引用传递、指针传递参数在java中的相应处理方法

    原文出处:[http://blog.csdn.net/conowen/article/details/7420533] 首先要明白一点,java是没有指针这个概念的. 但是要实现C++的引用传递.指针 ...

  5. 记一次WinForm程序中主进程打开子进程并传递参数的操作过程(进程间传递参数)

    目标:想在WinForm程序之间传递参数.以便子进程作出相应的处理. 一种错误的方法 父进程的主程序: ProcessStartInfo psi = new ProcessStartInfo(); p ...

  6. 在 Angularjs 中 ui-sref 和 $state.go 如何传递参数

    1 ui-sref.$state.go 的区别 ui-sref 一般使用在 <a>...</a>: <a ui-sref="message-list" ...

  7. Linux线程体传递参数的方法详解

    传递参数的两种方法 线程函数只有一个参数的情况:直接定义一个变量通过应用传给线程函数. 例子 #include #include using namespace std; pthread_t thre ...

  8. 【hadoop】如何向map和reduce脚本传递参数,加载文件和目录

    本文主要讲解三个问题:       1 使用Java编写MapReduce程序时,如何向map.reduce函数传递参数.       2 使用Streaming编写MapReduce程序(C/C++ ...

  9. python 函数传递参数的多种方法

    python中函数根据是否有返回值可以分为四种:无参数无返回值,无参数有返回值,有参数无返回值,有参数有返回值. Python中函数传递参数的形式主要有以下五种,分别为位置传递,关键字传递,默认值传递 ...

随机推荐

  1. Scikit-Learn与决策树

    Scikit-Learn(决策树)可以用于方法分类和回归. 一.分类 sklearn.tree.DecisionTreeClassifier(criterion='gini', splitter='b ...

  2. opencv+python3.4的人脸识别----2017-7-19

    opencv3.1  +  python3.4 第一回合(抄代码,可实现):人脸识别涉及一个级联表,目前能力还无法理解. 流程:1.读取图像---2.转换为灰度图---3.创建级联表---4.对灰度图 ...

  3. 【SignalR学习系列】4. SignalR广播程序

    创建项目 创建一个空的 Web 项目,并在 Nuget 里面添加 SignalR,jQuery UI 包,添加以后项目里包含了 jQuery,jQuery.UI ,和 SignalR 的脚本. 服务端 ...

  4. 64位系统下8G内存仅使用到4G问题的解决方法

    笔记本:联想E46G 当前bios版本:25CN32WW 内存:DDR3 133 4G × 2 问题:bios信息显示8G,win7和ubuntu 在64位下使用情况仅4G 准备工作1:bios版本和 ...

  5. 初学 Python(十一)——切片

    初学 Python(十一)--切片 初学 Python,主要整理一些学习到的知识点,这次是切片. #-*- coding:utf-8 -*- ''''' 切片 ''' L = ['name','age ...

  6. PowerShell 脚本中的密码

    引言 笔者在<PowerShell 远程执行任务>一文中提到了在脚本中使用用户名和密码的基本方式: $Username = 'xxxx' $Password = 'yyyy' $Pass ...

  7. CRM权限管理

    CRM权限管理 一.概念 权限管理就是管理用户对于资源的操作.本 CRM 系统的权限(也称作资源)是基于角色操作权限来实现的,即RBAC(Role-Based Access Control,基于角色的 ...

  8. Linux 下 安装jdk 1.7

    Linux 下 安装jdk 1.7 参考百度经验 http://jingyan.baidu.com/album/ce09321b7c111f2bff858fea.html?picindex=6 第一步 ...

  9. TeamCity : Build 失败条件

    允许用户配置 Build 失败的条件是很有用的功能,它是我们配置复杂 Build 流程的基础.TeamCity 为用户自定义 Build 失败条件提供了很好的支持.这些条件大体上可以分为两类,分别是: ...

  10. 利用AD采集获取外部温度传感器的值

    #include "led.h" #include "delay.h" #include "key.h" #include "sy ...