The PUT Method with AJAX in Spring 3.2 doesn't

2019-04-29 17:43发布

问题:

I'm trying to call a method in Spring (3.2.0) via AJAX using the following jQuery 1.6.

function updateRoleEnabled(id)
{
    $.ajax({
            datatype:"json",                        
            type: "PUT",
            url: "/wagafashion/ajax/UpdateUserRole.htm",
            data: "id="+id+"&t="+new Date().getTime(),
            success: function(response)
            {
            },
            error: function(e)
            {
                alert('Error: ' + e);
            }
    });
}

It attempts to invoke the following method in Spring.

@RequestMapping(value=("ajax/UpdateUserRole"), method=RequestMethod.PUT)
public @ResponseBody void updateUserRole(@RequestParam(value=("id")) String id)
{
    System.out.println("id = "+id);
}

FireFox responds with the following error.

HTTP Status 405 - Request method 'GET' not supported

type Status report

message Request method 'GET' not supported

description The specified HTTP method is not allowed for the requested resource (Request method 'GET' not supported).

Apache Tomcat/6.0.26

It works with the GET and POST methods and JSON (with Jackson-2.1.1) also works fine in other parts of the application.


If you need to see the dispatcher-servlet.xml file, the full contents is as follows.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"

       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
       http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">

    <context:component-scan base-package="controller" />
    <context:component-scan base-package="validatorbeans" />

    <mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" >
        <mvc:message-converters register-defaults="false">
        <bean id="jacksonMessageConverter" p:supportedMediaTypes="application/json" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
    </mvc:message-converters>
    </mvc:annotation-driven>

    <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
        <property name="favorPathExtension" value="false" />
        <property name="favorParameter" value="false" />
        <property name="ignoreAcceptHeader" value="false" />
        <property name="mediaTypes" >
            <value>
                atom=application/atom+xml
                html=text/html
                json=application/json
                *=*/*
            </value>
        </property>
    </bean>

    <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <prop key="org.springframework.web.multipart.MaxUploadSizeExceededException">
                    fileUploadingFailure
                </prop>
            </props>
        </property>
    </bean>
    <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="index.htm">indexController</prop>
            </props>
        </property>
    </bean>

    <bean id="viewResolver"
          class="org.springframework.web.servlet.view.InternalResourceViewResolver"
          p:prefix="/WEB-INF/jsp/"
          p:suffix=".jsp" />

    <bean name="indexController"
          class="org.springframework.web.servlet.mvc.ParameterizableViewController"
          p:viewName="index" />
</beans>

How to make HTTP methods other than GET and POST work in Spring 3.2?


EDIT:

Based on the comment below, the following is my entire web.xml file.

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/applicationContext.xml
            /WEB-INF/spring-security.xml
        </param-value>
    </context-param>
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>



    <filter>
        <filter-name>NoCacheFilter</filter-name>
        <filter-class>filter.NoCacheFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>NoCacheFilter</filter-name>
        <url-pattern>/admin_side/*</url-pattern>
    </filter-mapping>


    <filter>
        <filter-name>FileUploadFilter</filter-name>
        <filter-class>com.ckfinder.connector.FileUploadFilter</filter-class>
        <init-param>
            <param-name>sessionCookieName</param-name>
            <param-value>JSESSIONID</param-value>
        </init-param>
        <init-param>
            <param-name>sessionParameterName</param-name>
            <param-value>jsessionid</param-value>
        </init-param>
    </filter>


    <filter-mapping>
        <filter-name>FileUploadFilter</filter-name>
        <url-pattern>
                    /ckfinder/core/connector/java/connector.java
     </url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>multipartFilter</filter-name>
        <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>multipartFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


    <filter>
        <filter-name>httpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>httpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


    <filter>
        <filter-name>openSessionInViewFilter</filter-name>
        <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
        <init-param>
            <param-name>singleSession</param-name>
            <param-value>false</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>openSessionInViewFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>





    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <listener>
        <description>ServletContextListener</description>
        <listener-class>listener.UnregisterDatabaseDrivers</listener-class>
    </listener>
    <servlet>
        <servlet-name>ConnectorServlet</servlet-name>
        <servlet-class>com.ckfinder.connector.ConnectorServlet</servlet-class>
        <init-param>
            <param-name>XMLConfig</param-name>
            <param-value>/WEB-INF/config.xml</param-value>
        </init-param>
        <init-param>
            <param-name>debug</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>ConnectorServlet</servlet-name>
        <url-pattern>
                    /ckfinder/core/connector/java/connector.java
            </url-pattern>
    </servlet-mapping>


     <listener>
        <listener-class>
          org.springframework.security.web.session.HttpSessionEventPublisher
        </listener-class>
      </listener>




    <error-page>
        <description>Missing login</description>
        <error-code>401</error-code>
        <location>/WEB-INF/jsp/admin_side/ErrorPage.jsp</location>
    </error-page>

    <error-page>
        <description>Forbidden directory listing</description>
        <error-code>403</error-code>
        <location>/WEB-INF/jsp/admin_side/ErrorPage.jsp</location>
    </error-page>

    <error-page>
        <description>Missing page</description>
        <error-code>404</error-code>
        <location>/WEB-INF/jsp/admin_side/ErrorPage.jsp</location>
    </error-page>

    <error-page>
        <description>Uncaught exception</description>
        <error-code>500</error-code>
        <location>/WEB-INF/jsp/admin_side/ErrorPage.jsp</location>
    </error-page>

    <error-page>
        <description>Unsupported servlet method</description>
        <error-code>503</error-code>
        <location>/WEB-INF/jsp/admin_side/ErrorPage.jsp</location>
    </error-page>



    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>2</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>*.htm</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>redirect.jsp</welcome-file>
    </welcome-file-list>
</web-app>

回答1:

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.