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.xml加Filter配置等,更麻烦一些,不一定要用。

但有一个需要特别注意的事项,就是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.xml中需要增加如下配置,注意Filter调用顺序和web.xml中写的先后顺序移植

    <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>

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

时间: 2024-10-10 14:27:53

SpringMVC无法获取请求中的参数的问题的调查与解决的相关文章

SpringMVC无法获取请求中的参数的问题的调查与解决(2)

由于Request的getInputSteam()一旦获取一次后,就再也无法获取了 在实际项目中导致下面的问题: 1,多个拦截器,Filter都需要从InputStream中拿数据的情况无法处理: 2,InputStream被拦截器,Filter拿走了,Controller层无法getParameter(),也无法使用@RequestParam 注解来自动匹配设置参数. 参考了多篇处理getInputStream()相关的博文后,采取了下面的解决方案. 1,通过继承HttpServletRequ

如何获取url中的参数并传递给iframe中的报表

在使用报表软件时,用户系统左边一般有目录树,点击报表节点就会在右侧网页的iframe中显示出报表,同时点击的时候也会传递一些参数给网页,比如时间和用户信息等.如何使网页中的报表能够获取到传递过来的参数呢?以下用报表软件FineReport简单介绍一些. 具体实现过程 将报表生成页面时,给网页添加onload事件,首先获取url中的参数,然后嫁接到iframe的src上,或者通过获得的参数拼接处完整的报表url赋给iframe的src. <html> <head> <title

JavaScript获取地址栏中的参数

1.获取地址栏中的参数 (1)若地址栏中的地址是: http://10.124.36.56:8080/CMOD/index.jsp?name=you&password=123456&type=student (2)获取地址栏中的最后一个参数type 2.实现的JS function getAddressURLParam(paramName) { //构造一个含有目标参数的正则表达式的对象 var reg = new RegExp("(^|&)" + paramN

C#获取URL中的参数

//获取URL中id参数  var ids = YK.Common.Util.WebKit.QueryParamValue("ids", "");         /// <summary>         /// 获取url参数的字符串类型值         /// </summary>         /// <param name="key">参数名</param>         /// &

vue中如何不通过路由直接获取url中的参数

前言:为什么要不通过路由直接获取url中的参数? vue中使用路由的方式设置url参数,但是这种方式必须要在路径中附带参数,而且这个参数是需要在vue的路由中提前设置好的. 相对来说,在某些情况下直接在url后面拼接?mid=100的方式传递参数更灵活,你不需要设置路由,只需要在url后拼接参数即可,但是这种方式就需要通过javascript获取并提取url中的参数,通过传统的方式直接在页面中获取是行不通的了,因为vue中是无法通过location.search()来获取url问号之后的内容的.

js获取url中的参数,并保证获取到的参数不乱码

  //网上比较经典的js获取url中的参数的方法  function getQueryString(name) {      var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");      var r = window.location.search.substr(1).match(reg);      if ( r != null ){        

javaScript获取url中的参数

var urlTools = { //获取RUL参数值 getUrlParam: function(name) { /*?videoId=identification */ var params = decodeURI(window.location.search); /* 截取?号后面的部分 index.html?act=doctor,截取后的字符串就是?act=doctor */ var reg = new RegExp("(^|&)"+ name +"=([^&

jQuery 获取url中的参数

//获取url中的参数 function getUrlParam(name) { var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); //构造一个含有目标参数的正则表达式对象 var r = window.location.search.substr(1).match(reg); //匹配目标参数 if (r != null) return unescape(r[2]); retur

全选全不选,返回字符串长度,汉字计数为2,获取url中的参数 ,函数防抖,函数节流

/* 全选/全不选 */ function selectAll(objSelect) { if (objSelect.checked == true) { $("input[name='chkId']").attr("checked", true); $("input[name='chkAll']").attr("checked", true); } else if (objSelect.checked == false) {