springMvc中前台ajax传json数据后台controller接受对象为null

一般地,出现这种情况很大一部分原因是对ajax中的参数作用不熟悉或者对@RequestBody注解不熟悉。

在jquery的ajax中,如果没加contentType:"application/json",那么data就应该对应的是json对象,反之,如果加了contentType:"application/json",那么ajax发送的就必须是字符串。为什么呢?contentType参数指定的是浏览器将发送什么样类型的编码,比如   

     text/html : HTML格式

     text/plain :纯文本格式      

     text/xml :  XML格式

     mage/gif :gif图片

     image/jpeg :jpg图片格式 

     image/png:png图片格式

     application/json  :json数据格式

     application/pdf   :pdf格式  

     application/octet-stream : 二进制流数据

等等。。。

而你不指定contentType则代表为默认的application/x-www-form-urlencoded(表单)类型,这种类型有一个好处,它可以支持很多种情况,并能配合查询字符串(key1=value1&key2=vlaue2)的形式发送到服务器。而且这种默认类型可以很好地配合参数processData(默认为true,可以不用管他),该参数为true的时候,开启自动转化功能,只要是一个合法对象,都能将该对象自动地转化成查询字符串的形式。所以,在contentType默认的情况下(即不写),ajax里的data参数既可以这么传:

        $.ajax({

         url:"http://xxxxxx",

         data: { "username" : "ccc" } ,  //查询字符串的映射形式(即对象),在默认模式下提交时会自动地转化成查询字符串

         .....     

});

也可以这么传:

        $.ajax({

         url:"http://xxxxxx",

         data: "username=ccc",   //查询字符串形式

         .....     

});

所以看到这里,差不多也能知道为什么指定了contentType : "application/json"后,data就只能是字符串而且必须是json字符串了,首先你指定json格式,那提交格式肯定与json有关,再一个,因为你指定了"application/json"就意味着你放弃了"application/x-www-form-urlencoded"这种默认模式,默认模式虽可以自动地转化对象,不见得"application/json"这种格式就会自动帮你转,而事实证明,它确实不会帮你自动转。

 

下面便是两种犯错的例子:

1>前台ajax多加了contentType:"application/json",data却错传成json对象:

后台处理:(employee该pojo对象里有username和password等String字段)

结果都为null

将contentType去掉后,

后台成功反射出对象:

 

 

2>ajax中没加contentType:"application/json",data却直接使用了json字符串(和上面的一个道理)

后台情况:

有人就会说,去掉“application/json” 后的默认模式应该是支持字符串和对象的啊,是的,默认模式确实是可以支持两者,但是要看清楚,默认默认支持的字符串是什么?是json字符串吗?不!查询字符串。

那么如何在application/json这种非默认模式下让Mvc后台接收到收据呢?这里就需要用到@RequestBody注解了,很巧,这个注解就是专门用来处理非默认模式下的请求的。该注解会提取你传过来的json字符串(注意是json字符串,不是查询字符串),并将提取到的信息绑定到对象中。如果上图加了"application/json"那么controller就应该这么接收:

@RequestMapping("xxx")

......

public String test(@RequestBody  Employee  employee) {

......

}

 

3>第三点是真的有点恶心的一点,找了好久才找到。。

那就是  有些  关键的属性在Mvc层中反射失败,会导致其他所有属性都为null

比如上面的joindate对应的pojo是Date,本来input框里的值是 Thu Dec 30 00:00:00 CST 1999 ,传到后台用Date接收,但是显然格式不对,于是Date合成出了错,然后坑爹的就来了,因为这个特殊的属性反射失败,Mvc层就将请求打回去然后导致浏览器报错400 bad request ,从而导致其他所有属性都为null。最后把joindate属性注释掉,后台什么属性的值都收到了。。

希望能够帮到大家!

 

补充:在"application/json"的时候,ajax请求对跨域支持似乎不好,默认模式下的ajax成功跨域请求一旦改成了"application/json"就会报跨域请求的错

 

附加部分(有兴趣可以继续往下)

针对评论里关于application/jsonapplication/x-www-form-urlencoded 孰好孰坏问题。正如评论所提到的,application/json就应该尽量避免使用吗?

看了很多资料,最主要的看法无非就是application/json能支持解析复杂又嵌套的对象,而认为application/x-www-form-urlencoded在面对复杂、对象+数组嵌套的往往就束手无策了。可事实真是这样吗?application/x-www-form-urlencoded就真的就不能解决复杂+对象嵌套+数组嵌套的情况吗?

思考

我们知道application/x-www-form-urlencoded默认模式下给后台传的是 key/value 形式的查询字符串,也就是所谓的QueryString。而springmvc会切割前台传来的 key/value查询字符串,并将切割到的信息绑定到controller方法对应的参数名中,从而我们能直接根据声明的参数名拿到对应的值。看到这里我们应该已经知道,为了能在默认模式下让后台接收到一个复杂+多重嵌套的对象,关键在于如何将该复杂对象序列化成springmvc能认识的QueryString。

前面我们提到过默认模式下会开启对象的自动转换,即自动帮你把对象转成QueryString,然后再把该QueryString传给后台。我们可以通过三个步骤:

1. let qs = JQuery.param(obj) 或者 let qs =  $.param(obj) 

2. let encoded_qs = decodeURIComponent(qs);

3. console.log(encoded_qs)

来查看一个对象究竟被自动序列化成了什么样子的QueryString,下面是一些测试结果

{passengers:[{"name":"Egor", "role":"pilot"},{"name":"DHH", "role":"2pilot"}]}

passengers[0][name]=Egor&passengers[0][role]=pilot&passengers[1][name]=DHH&passengers[1][role]=2pilot

{a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: "you're outta ram" }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}

a[a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a][a]=you're outta ram

然后根据springmvc切割key/value查询字符串的特点,我们试着在controller的方法里声明下对应的key

可以看到后台成功取到了对应的值

其实不管他嵌套的再多,哪怕是上面多重嵌套的a[a][a][a][a][a]...,其本质上都只是QueryString的一个key而已,所以只要是正确的key,后台都能取到对应的值

看到这里可能有人要问了,虽然上面证明了可以从复杂+嵌套的结构中取到特定的值,但是自己想要的并不只是局限于取到特定的值,而是希望后台自动给我封装成一个对象,即我能直接从controller的方法参数中拿到一个完整的复杂嵌套对象。只不过,这样行的通吗?

再次思考

解决问题的出发点其实还是原来的原则,即如何将复杂对象序列化成springmvc能认识的QueryString  [1*]只不过这里有些小区别,更贴切的讲应该是如何将复杂对象序列化成springmvc能进行嵌套绑定的QueryString  [2*]

[1*][2*]对应的字符串是有区别的,举个例子,还是上面的那个passenger对象:

{passengers:[{"name":"Egor", "role":"pilot"},{"name":"DHH", "role":"2pilot"}]}

后台对应的bean对象应该是这种结构:

class Passengers {

    List<Passenger> passengers;
    //get set方法
    //...
}

class Passenger {
    
    String name;

    String role;
    //get set方法
    //...
}

但是如果我们了解过springmvc的参数绑定就应该知道,为了让后台自动绑定好一个完整的对象,他的QueryString不应该是这样

passengers[0][name]=Egor&passengers[0][role]=pilot&passengers[1][name]=DHH&passengers[1][role]=2pilot

而应该是这样(即[2*]所对应的QueryString):

passengers[0].name=Egor&passengers[0].role=pilot&passengers[1].name=DHH&passengers[1].role=2pilot

也就是说,如果能将JQuery.param序列化后的QueryString转化成[2*]对应的QueryString,那么一切问题也就解决了

于是,我写了份转换的代码,如下:
 

//定义字符栈
    var stack_chars = [];
    //定义[]括号栈
    var stack_parentheses = [];
	
	//定义这个下标变量是为了解决 {arr:[1,2,3]} ——> arr[]=1&arr[]=2&arr[]=3 这种情况
	//因为 arr[]=1&arr[]=2&arr[]=3 应该变成 arr[0]=1&arr[1]=2&arr[2]=3,所以定义了stackIdx是为了完成括号里下标的递增
    var stackIdx = -1;

    //判断当前读取的字符是否在[]中
    function inParentheses() {
        return stack_parentheses.length == 1;
    }

	//通过调用该方法完成将复杂对象obj转换成springmvc支持嵌套绑定的QueryString
    function serialize(obj) {
        let jqueryStr = decodeURIComponent(jQuery.param(obj));
        let serializeStr = "";
        for (let index = 0; index < jqueryStr.length; index++) {
            const c = jqueryStr[index];
            if (c === "[" || c === "]") {
                //入括号栈
                stack_parentheses.push(c);
                //检查是否栈满, 并进行相关操作
                serializeStr += checkCharsStack();
            } else {
                if (inParentheses()) {
                    stack_chars.push(c);
                } else {
                    //不在[]中的字符直接拼接到结果串中
                    serializeStr += c;
                }
            }
        }
        return serializeStr;
    }

    function checkCharsStack() {
        //栈满
        if (stack_parentheses.length >= 2) {
            let c;
            let str = "";
            let arr = [];
            while ((c = stack_chars.pop()) !== undefined) {
                //这里把pop出的内容又push进arr是为了防止字符顺序颠倒
                arr.push(c);
            }
            while ((c = arr.pop()) !== undefined) {
                str += c;
            }
            //[]出桟
            stack_parentheses.pop();
            stack_parentheses.pop();
            //如果符号栈为空只能说明JQuery序列化了没有key的数组,即序列化了这种 {arr:[1,2,3]} ——> arr[]=1&arr[]=2&arr[]=3
            if(str === ""){
                return "["+(++stackIdx)+"]";
            }else{
                stackIdx = -1;
            }
            //如果是其他字符,比如arr[1][name]应该转化成arr[1].name
            if (isNaN(parseInt(str))) {
                return "." + str;
            } else {
                //如果是数字,比如arr[1],转化后仍是arr[1], 即不需要动他
                return "[" + str + "]";
            }
        }
        return "";
    }

通过直接调用serialize(obj) 方法便可将复杂又多重嵌套的对象转换成[2*]对应的QueryString,于是,就可以直接提交给后台了,springmvc会自动帮你绑定好一个完整的复杂+嵌套对象。

(js文件传到码云上了点这里,里面有更详细说明)

下面我们来测试一个复杂而又多重嵌套的对象:

后台情况:

浏览器打印后台返回结果:

可以看到,application/x-www-form-urlencoded同样实现了多重嵌套的复杂对象到后台的传递,而且相对于application/json,在后台既不用手动写代码去合成bean对象,也不用手动声明@RequestBody注解,是不是也很方便?

就我个人而言,看了上面的例子后,在非特殊情况下会有些更倾向application/x-www-form-urlencoded了,跨域问题既没有application/json那么多,安全问题也不像application/json那样会有一定概率被一些植入恶意的回调代码,而且能很方便的传输和接受复杂而又多重嵌套的对象。

那么,application/json是不是就一点用都没有了呢?当然不是,比如你要调用别人的接口,那么就很可能要指定application/json格式,因为接口提供方就是这么规定的,下面就是一个例子:

而且在其他方面,json的数据格式传输时带宽会更小,尤其是在传输数组时,如果数组元素很多,那么application/json传输的数据量就会比application/x-www-form-urlencoded要少很多,自己可以拿上面的代码试试。同时,json的解析速度也会更快。总之,选择用谁个人觉得还是得看自己喜好以及团队里的规范和要求。

最后,还是希望大家能有所收获,如有问题也欢迎指出

  • 59
    点赞
  • 139
    收藏
    觉得还不错? 一键收藏
  • 25
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值