你的位置:首页 > Java教程

[Java教程]SpringMVC无法获取请求中的参数的问题的调查与解决


使用框架可以节约开发时间,但有时由于隐藏了一些实现细节,导致对底层的原理知之不详,碰到问题时不知道该从哪一个层面入手解决。因此我特意记录了下面这个典型问题的调查和解决过程供参考。

 

事情是这样的,我们原来有一个移动端调用的发表评论的API,是几年前在NET平台上开发的,移植到JAVA后,发现安卓版APP无法正常发表汉字评论。

基于SpringMVC创建的JAVA版API接口大致如下,经调查发现,关键的content参数,在Controller层检查结果为空。

 

	@RequestMapping(value = "/Test.api")	public Object test(			HttpServletRequest request,			HttpServletResponse response,  		        @RequestParam(value = "content", required = false, defaultValue="") String content) {            // 在这里,content的值为空          }

用Charles抓包检查Post的Form数据,确实有字段content,且有汉字值。但检查其Raw数据居然为这样的形式:content=%u611f%u53d7%u4e00%u4e0b%u8d85%u4eba%u7684%u808c%u8089%uff0c

 

我们知道,目前java常用的URLEncoder类,一般将汉字转换成"%xy"的形式,xy是两位16进制的数值,不会出现%u后面跟4个字符这种情况。

%u开头代表这是一种Unicode编码格式,后面的四个字符是二字节unicode的四位16进制码。在Charles软件上,支持这种解码,所以可以正常看到抓包数据中的汉字。

 

但是我们从SpringMVC框架层面统一指定了encoding为UTF-8,根据@RequestParam注解,使用UTF-8进行content参数的解码时,必然异常,由此导致了Post过来的content字段丢失。

 

和安卓团队确认,发现过去他们确实采用了自己独有的Encode方法对Post数据进行编码:

  public static String UrlEncodeUnicode(final String s)  {    if (s == null)    {      return null;    }    final int length = s.length();    final StringBuilder builder = new StringBuilder(length); // buffer    for (int i = 0; i < length; i++)    {      final char ch = s.charAt(i);      if ((ch & 0xff80) == 0)      {        if (Utils.IsSafe(ch))        {          builder.append(ch);        }        else if (ch == ' ')        {          builder.append('+');        }        else        {          builder.append("%");          builder.append(Utils.IntToHex((ch >> 4) & 15));          builder.append(Utils.IntToHex(ch & 15));        }      }      else      {        builder.append("%u");        builder.append(Utils.IntToHex((ch >> 12) & 15));        builder.append(Utils.IntToHex((ch >> 8) & 15));        builder.append(Utils.IntToHex((ch >> 4) & 15));        builder.append(Utils.IntToHex(ch & 15));      }    }    return builder.toString();  }

采用这种方式的原因已经不可考证,并且安卓团队已经决定将在未来版本中放弃该编码方式,采用JAVA常用的Encoder类进行UTF-8的编码。问题定位后,决定新版API中必须要兼容新旧两种编码方式。

但是目前SpringMVC的@RequestParam注解负责了请求数据的解码,我们从哪一层切入,截获请求数据,判断其编码方式,并动态选用不同的解码方式来处理呢?

 

经过DEBUG,觉得下面两种方式是可行的。

 

解决问题的方法1:

修改API的接口形式,放弃@RequestParam注解,使用@RequestBody注解,直接获取POST请求的Raw数据,在Controller层独立解码

  @RequestMapping(value = "/Test.api")  public Object test(      HttpServletRequest request,      HttpServletResponse response,          @RequestBody String body) {        // 在这里,可以获得如下的raw数据: Id=185904&content=%u611f%u53d7%u4e00%u4e0b         // 可以自己对raw数据进行解析和解码 (具体的解码方式暂不考虑)    }

 

解决问题的方法2:

通过自定义Filter的形式,在doFilter()方法中获取request的getInputStream(),也可以得到raw数据,解析后通过setAttribute()方法可以保存request中。

但这个操作过程和@RequestBody注解相比,要改web.

但有一个需要特别注意的事项,就是getInputStream()是一个一次性的动作,一旦被执行了,如果其他地方用到,就无效了。参考下面代码的注释:

package org.jiagoushi.api.aop;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.util.Map;import javax.servlet.Filter;import javax.servlet.FilterChain;import javax.servlet.FilterConfig;import javax.servlet.ServletException;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class MyFilter implements Filter {  protected FilterConfig filterConfig;  String encoding = null;  public void destroy() {    this.filterConfig = null;  }  /**   * 初始化   */  public void init(FilterConfig filterConfig) {    this.filterConfig = filterConfig;  }  /**   * 过滤处理   */  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {    // String s = servletRequest.getInputStream()    String line = "";    StringBuilder body = new StringBuilder();    int counter = 0;        InputStream stream;    stream = servletRequest.getInputStream();        //读取POST提交的数据内容    BufferedReader reader = new BufferedReader(new InputStreamReader(stream));    while ((line = reader.readLine()) != null) {      if(counter > 0){        body.append("\r\n");      }      body.append(line);      counter++;    }        //POST请求的raw数据可以获得    System.out.println(body);        //注意事项:因为servletRequest.getInputStream()被调用过1次,以后再调用也没有了。    //下面的getParameterMap()和getParameter()方法,本质上也是去getInputStream(),
//所以都无法再获取到任何参数了 HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; Map map = request.getParameterMap(); String v = request.getParameter("content"); // 继续执行下一个 filter, 无一下个 filter 则执行请求 chain.doFilter(request, response); }}

要让上面的Filter生效,web.

  <filter>    <filter-name>myFilter</filter-name>    <filter-class>org.jiagoushi.api.aop.MyFilter</filter-class>  </filter>  <filter-mapping>    <filter-name>myFilter</filter-name>    <url-pattern>/Test.api</url-pattern>  </filter-mapping>

 

此问题的调查告一段落,欢迎讨论。