解決 Spring RestTemplate post傳遞參數(shù)時報錯問題
今天跟同事接口聯(lián)調(diào),使用RestTemplate請求服務(wù)端的post接口(使用python開發(fā))。詭異的是,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);
結(jié)果為:
get json : {'status': 0, 'statusInfo': {'global': 'OK'}, 'data': 'http://mydomain/dataDownLoad.cgi?downLoadData=358300d5f9e1cc512efc178caaa0b061'}
500 Internal Server Error
最后經(jīng)過另一位同學(xué)幫忙排查,發(fā)現(xiàn)RestTemplate在postForObject時,不可使用HashMap。而應(yīng)該是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);
結(jié)果為:
post json : {'status': 0, 'statusInfo': {'global': 'OK'}, 'data': 'http://mydomain/dataDownLoad.cgi?downLoadData=f2fc328513886e51b3b67d35043985ae'}
然后我想起之前使用RestTemplate發(fā)起post請求時,使用POJO作為參數(shù),是可行的。再次測試:
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。
到現(xiàn)在為止接口調(diào)通了。但問題的探究才剛剛開始。
RestTemplate的post參數(shù)為什么使用MultiValueMap而不能使用HashMap?
為什么post接口,get請求也可以正確返回?
為什么java服務(wù)端可以接收POJO參數(shù),python服務(wù)端不可以?python服務(wù)端使用CGI(Common Gateway Interface),與cgi有關(guān)系嗎?
何為MultiValueMap
IDEA中command+N,搜索類MultiValueMap,發(fā)現(xiàn)apache的commons-collections包有一個MultiValueMap類,spring-core包中有一個接口MultiValueMap,及其實(shí)現(xiàn)類LinkedMultiValueMap。顯然看spring包。
首先看LinkedMultiValueMap,實(shí)現(xiàn)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轉(zhuǎn)為LinkedMultiValueMap
Map<K, V> toSingleValueMap(); //只保留所有LinkedList的第一個值,轉(zhuǎn)為LinkedHashMap
}
綜上,LinkedMultiValueMap實(shí)際就是Key-LinkedList的map。
RestTemplate怎么處理post參數(shù)
首先查看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是什么樣的呢?如下,實(shí)際是把請求數(shù)據(jù)放在了一個HttpEntity中。如果requestBody是HttpEntity類型,就直接轉(zhuǎn);否則,放在HttpEntity的body中。
//請求內(nèi)容封裝在一個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聯(lián)系上了。
基于本次問題,我們不考慮post數(shù)據(jù)參數(shù)是HttpEntity類型的,只考慮普通POJO。那么,postForObject中對post數(shù)據(jù)的第一步處理,就是放在一個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數(shù)據(jù)中hashMap只含有data一個key,不含status字段,所以會跳過寫的操作,即post請求帶不上參數(shù)。如果修改代碼,當(dāng)不含status字段時,按照父類方法處理,則服務(wù)端可以得到參數(shù)。
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正確處理。
首先判斷是否可以執(zhí)行寫操作,如果可以,執(zhí)行寫操作。
@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中是否含有多個數(shù)據(jù)
//只是普通的K-V,寫form
writeForm((MultiValueMap<String, String>) map, contentType, outputMessage);
}
else {
writeMultipart((MultiValueMap<String, Object>) map, outputMessage);
}
}
既如此,那么post參數(shù)為POJO時,如何呢?
POJO也會被ViewAwareJsonMessageConverter處理,在其writeInternal中,object不是map,所以調(diào)用 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,跟蹤發(fā)現(xiàn),會報錯,返回沒有合適的HttpMessageConverter處理。
使用ViewAwareJsonMessageConverter和使用FormHttpMessageConverter寫數(shù)據(jù)的格式是不一樣的,所以,post POJO后,會返回錯誤,但實(shí)際已將參數(shù)傳遞出去。
所以,對于我們配置的RestTemplate來說,post參數(shù)可以是map(有字段要求),也可以是POJO。即,輸入輸出數(shù)據(jù)由RestTemplate配置的messageConverters決定。
至此,我們已經(jīng)清楚了第一個問題,剩下的問題同樣的思路。跟蹤一下getForObject的處理路徑。get方式請求時,把所有的參數(shù)拼接在url后面,發(fā)給服務(wù)端,就可以把參數(shù)帶到服務(wù)端。
剩下的問題就是python服務(wù)端是怎么處理請求的。首先研究一下CGI。
何為CGI
通用網(wǎng)關(guān)接口(CGI,Common Gateway Interface)是一種Web服務(wù)器和服務(wù)器端程序進(jìn)行交互的協(xié)議。CGI完全獨(dú)立于編程語言,操作系統(tǒng)和Web服務(wù)器。這個協(xié)議可以用vb,c,php,python 來實(shí)現(xiàn)。
工作方式如圖所示:
browser->webServer: HTTP protocol
webServer->CGI腳本: 通過CGI管理模塊調(diào)用腳本
CGI腳本->CGI腳本: 執(zhí)行腳本程序
CGI腳本->webServer: 返回結(jié)果
webServer->browser: HTTP protocol

web服務(wù)器獲取了請求cgi服務(wù)的http請求后,啟動cgi腳本,并將http協(xié)議參數(shù)和客戶端請求參數(shù)轉(zhuǎn)為cgi協(xié)議的格式,傳給cgi腳本。cgi腳本執(zhí)行完畢后,將數(shù)據(jù)返回給web服務(wù)器,由web服務(wù)器返回給客戶端。
cgi腳本怎么獲取參數(shù)呢?
CGI腳本從環(huán)境變量QUERY_STRING中獲取GET請求的數(shù)據(jù)
CGI腳本從stdin(標(biāo)準(zhǔn)輸入)獲取POST請求的數(shù)據(jù),數(shù)據(jù)長度存在環(huán)境變量CONTENT_LENGTH中。
了解CGI大概是什么東東后,看一下python實(shí)現(xiàn)的CGI。
python的CGI模塊,要獲取客戶端的post參數(shù),可以使用cgi.FieldStorage()方法。FieldStorage相當(dāng)于python中的字典,支持多個方法??梢灾С忠话愕膋ey-value,也可以支持key-List<Value>,即類似于MultiValueMap形式的參數(shù)(如多選的表單數(shù)據(jù))。
至此,本問題主要是在于程序怎么傳遞參數(shù),對于spring restTemplate而言,就是messageConverters怎么配置的。
更多關(guān)于RestTemplate post傳遞參數(shù)時報錯問題文章大家看看下面的相關(guān)鏈接
- SpringCloud 服務(wù)負(fù)載均衡和調(diào)用 Ribbon、OpenFeign的方法
- Springcloud基于OpenFeign實(shí)現(xiàn)服務(wù)調(diào)用代碼實(shí)例
- spring cloud openfeign 源碼實(shí)例解析
- 解決SpringBoot框架因post數(shù)據(jù)量過大沒反應(yīng)問題(踩坑)
- Spring實(shí)戰(zhàn)之使用@POSTConstruct和@PreDestroy定制生命周期行為操作示例
- SpringMVC post請求中文亂碼問題解決
- Spring中的后置處理器BeanPostProcessor詳解
- SpringBoot Controller Post接口單元測試示例
- 解決SpringMVC接收不到ajaxPOST參數(shù)的問題
- SpringCloud OpenFeign Post請求400錯誤解決方案
相關(guān)文章
Java 圖片復(fù)制功能實(shí)現(xiàn)過程解析
這篇文章主要介紹了Java 圖片復(fù)制功能實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-10-10
一天時間用Java寫了個飛機(jī)大戰(zhàn)游戲,朋友直呼高手
前兩天我發(fā)現(xiàn)論壇有兩篇飛機(jī)大戰(zhàn)的文章異?;鸨?但都是python寫的,竟然不是我大Java,說實(shí)話作為老java選手,我心里是有那么一些失落的,今天特地整理了這篇文章,需要的朋友可以參考下2021-05-05
使用ByteArrayOutputStream寫入字符串方式
這篇文章主要介紹了使用ByteArrayOutputStream寫入字符串方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12

