1. 程式人生 > 程式設計 >解決 Spring RestTemplate post傳遞引數時報錯問題

解決 Spring 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,'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,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,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

解決	Spring RestTemplate post傳遞引數時報錯問題

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形式的引數(如多選的表單資料)。

至此,本問題主要是在於程式怎麼傳遞引數,對於spring restTemplate而言,就是messageConverters怎麼配置的。

更多關於RestTemplate post傳遞引數時報錯問題文章大家看看下面的相關連結