RestTemplate post如何傳遞參數


背景

今天跟同事接口聯調,使用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

pic

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怎么配置的。

——————————————————
喵喵還要多努力學習啊~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM