怎样解决SpringRestTemplatepost传递参数时报错问题

发布时间:2021-10-15 16:11:37 作者:柒染
来源:亿速云 阅读:141

怎样解决SpringRestTemplatepost传递参数时报错问题,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。

RestTemplate的post参数为什么使用MultiValueMap而不能使用HashMap?

今天跟同事接口联调,使用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类对象,然后再处理请求。

Overridepublic <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),如下:

@Overrideprotected 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形式的参数(如多选的表单数据)。

看完上述内容,你们掌握怎样解决SpringRestTemplatepost传递参数时报错问题的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注亿速云行业资讯频道,感谢各位的阅读!

推荐阅读:
  1. 解决Azure automation 报错问题
  2. 如何解决pymysql查询语句中带有in时传递参数的问题

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

spring rest

上一篇:java使用同步需要注意什么

下一篇:java同步怎么用

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》