The PUT Method with AJAX in Spring 3.2 doesn't work

喜欢而已 提交于 2019-12-05 10:14:13
Arjan

Unless one is using only path parameters, processing a regular HTTP PUT needs some more work.

Since Spring 3.1, HttpPutFormContentFilter can be used to make @RequestParam work for application/x-www-form-urlencoded data:

Filter that makes form encoded data available through the ServletRequest.getParameter*() family of methods during HTTP PUT requests.

The Servlet spec requires form data to be available for HTTP POST but not for HTTP PUT requests. This filter intercepts HTTP PUT requests where content type is 'application/x-www-form-urlencoded', reads form encoded content from the body of the request, and wraps the ServletRequest in order to make the form data available as request parameters just like it is for HTTP POST requests.

However: this filter consumes the request's input stream, making it unavailable for converters such as FormHttpMessageConverter, like used for @RequestBody MultiValueMap<String, String> or HttpEntity<MultiValueMap<String, String>>. As a result, once you have configured the above filter in your application, you will get "IOException: stream closed" when invoking methods that use other converters that also expect raw application/x-www-form-urlencoded PUT data.


Alternatively one can do everything manually, using @RequestBody or HttpEntity<?>:

@RequestMapping(value="ajax/UpdateUserRole", method=RequestMethod.PUT,
    produces = MediaType.TEXT_PLAIN_VALUE)
public @ResponseBody String updateUserRole(
    @RequestBody final MultiValueMap<String, String> data,
    final HttpServletResponse response) {
  Map<String, String> params = data.toSingleValueMap();
  String id = params.get("id");
  String a = params.get("a");
  String b = params.get("b");
  if(id == null || a == null || b == null) {
    response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    return null;
  }
  return "id = " + id;
}

See also an example using WebDataBinder, or use:

public ResponseEntity<String> updateUserRole(
    final HttpEntity<MultiValueMap<String, String>> entity) {
  Map<String, String> params = entity.getBody().toSingleValueMap();
  String id = params.get("id");
  ...


Note that for testing, using MockMvc's mockMvc.perform(put(url).param(name, value)) would actually also work with the code form the question, even though it would fail in a servlet container. But MockMvc is not running in such servlet container, hence is fooling you a bit.

MockMvc's .param(name, value) also works nicely with HttpPutFormContentFilter. But when using MockMvc to test @RequestBody or HttpEntity<?>, one also needs to create any application/x-www-form-urlencoded PUT content manually. Like:

mockMvc.perform(put(url).content("id=" + URLEncoder.encode(id, "UTF-8")
  + "&a=" + URLEncoder.encode(a, "UTF-8") + "&b=" + ...)

To be able to simply use .param(name, value), just like for GET and POST, one could define:

public static RequestPostProcessor convertParameters() {
  return new RequestPostProcessor() {
    @Override
    public MockHttpServletRequest postProcessRequest(
        final MockHttpServletRequest request) {
      if ("PUT".equalsIgnoreCase(request.getMethod()) {
        Map<String, String[]> params = request.getParameterMap();
        if (params != null) {
          StringBuilder content = new StringBuilder();
          for (Entry<String, String[]> es : params.entrySet()) {
            for (String value : es.getValue()) {
              try {
                content.append(URLEncoder.encode(es.getKey(), "UTF-8"))
                  .append("=")
                  .append(URLEncoder.encode(value, "UTF-8"))
                  .append("&");
              }
              catch (UnsupportedEncodingException e) {
                throw new IllegalArgumentException("UTF-8 not supported");
              }
            }
          }
          request.setParameters(new HashMap<String, String[]>());
          request.setContent(content.toString().getBytes());
          request.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
        }
      }
      return request;
    }
  };
}

...and then use .with(convertParameters()) next to .param(name, value):

mockMvc.perform(put(url)
    .with(convertParameters())
    .param("id", id).param("a", a).param("b", b) ...)

Given all the above, simply using HttpPutFormContentFilter for application/x-www-form-urlencoded data really makes life easier.

When the browser is not sending application/x-www-form-urlencoded data, but things such as JSON, then trying to map to MultiValueMap will yield 415 Unsupported Media Type. Instead, use something like @RequestBody MyDTO data or HttpEntity<MyDTO> entity as explained in Parsing JSON in Spring MVC using Jackson JSON.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!