一般地,出现这种情况很大一部分原因是对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/json和application/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的解析速度也会更快。总之,选择用谁个人觉得还是得看自己喜好以及团队里的规范和要求。
最后,还是希望大家能有所收获,如有问题也欢迎指出