415错误
4xx类错误属于前端的请求错误,通常都是Http报文无法被后台响应,后台判断前端的参数不正确等,而415错误定义为
Unsupported Media Type 服务器无法处理请求附带的媒体格式
错误发生的环境
ajax发起的请求
$.ajax({
url: "http://localhost:8080/account/",
type: "POST",
dataType: 'json',
data: {
username: $('#username').val(),
password: $('#password').val()
},
success: function (res) {
if (res.code == 1) {
$('#redirect').attr('href', '../catalog/Main.html');
document.getElementById('redirect').click();
} else {
$('#newAccountMessage').text(res.msg);
}
},
error: function (res) {
$('#newAccountMessage').text(res.msg);
}
})
后台的Controller的Restful API
当然这和是不是rest没有关系,rest仅仅是一种架构方式和设计规范罢了,在这个场景中是无关的
@PostMapping("")
@ResponseStatus(HttpStatus.CREATED)
public PlatformResult createAccount(@Validated(CreateGroup.class) @RequestBody Account account) {
if(accountService.getAccount(account.getUsername()) != null) {
throw new BusinessException("Account is exist");
}
account.setFavouriteCategoryId("CATS");
account.setLanguagePreference("english");
accountService.insertAccount(account);
return PlatformResult.success(accountService.getAccount(account.getUsername()));
}
接着来看一下SpringBoot控制台打印的日志和默认返回的封装的错误信息
日志:
2019-06-14 07:20:37.482 WARN 17084 — [nio-8080-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type ‘application/x-www-form-urlencoded;charset=UTF-8’ not supported]
返回的信息:
{
"timestamp":"2019-06-13T23:20:37.490+0000",
"status":415,
"error":"Unsupported Media Type",
"message":"Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported",
"path":"/account/"
}
是一个json字符串,其中每一个字段的大致意思是这个样子的
- timestamp 接收请求发生错误的时间戳
- status http状态码
- error http状态码对应的简短英文信息
- message 错误的简短描述
- path 请求的路径
接着我们再来看一些Chrome显示的抓包
注意这里的Content-Type
前端问题确定
就上面的分析来看,问题可以找到了,就是前端请求的数据格式是‘application/x-www-form-urlencoded’,后台不接收这个格式,造成了415错误
那么这里就涉及两点了
- 为什么前端发送的数据格式是‘application/x-www-form-urlencoded’,ajax用什么方式来变化数据发送的格式
- 后台SpringMVC为什么不接收‘application/x-www-form-urlencoded’这一格式
对于 问题1 我们查看ajax的文档,可以找到两个相关的属性,一个是dataType一个是contentType
- dataType: 其实dataType在这里并没有关系,dataType是 ‘预期的服务器响应的数据类型’,设置这一值会在http报文的Request Headers的Accept里面体现,权值按高低从左到右排列,如果这一值与后台返回的数据不一致那么ajax可能会抛出解析错误的异常
- contentType:发送数据到服务器时所使用的内容类型。默认是:“application/x-www-form-urlencoded”。在上述的ajax中我们没有去设置contentType,所以它发送到服务器的数据类型为"application/x-www-form-urlencoded",这和我们的实际情况与分析的是一致的
contentType的常用四种类型为
- application/x-www-form-urlencoded
- multipart/form-data
- application/json
- text/xml
想要具体了解的可以查看这篇博文!四种常见的 POST 提交数据方式
那么到这里在分析后端之前,我们可以设置contentType值来尝试一下
如果说直接在上面加入contentType:‘application/json’,结果还是会出错,这一问题我们要到分析后端时才能知晓,就刚才而言我们得到的后台错误信息是
JSON parse error: Unrecognized token ‘username’: was expecting (‘true’, ‘false’ or ‘null’)
可能看着这个会让人感到我的username明明类型是String怎么看着像个布尔变量,当然并不是。这一问题关乎jackson,具体它为何报这样的异常这里就不做讨论了
后端问题确定
这里直接给出结论,被@RquestBody注解的内容只能接收Json字符串
综合分析
由 ‘被@RquestBody注解的内容只能接收Json字符串’ 和 加入contentType:'application/json’还是报错这两点我们可以大致确定解决的办法,那就是将ajax发送的数据json序列化之后再发送
使用JSON.stringify(data)来进行json字符串的序列化
const sendData = JSON.stringify({
username: $('#username').val(),
password: $('#password').val()
});
$.ajax({
url: "http://localhost:8080/account/",
type: "POST",
dataType: 'json',
data: {
username: $('#username').val(),
password: $('#password').val()
},
success: function (res) {
if (res.code == 1) {
$('#redirect').attr('href', '../catalog/Main.html');
document.getElementById('redirect').click();
} else {
$('#newAccountMessage').text(res.msg);
}
},
error: function (res) {
$('#newAccountMessage').text(res.msg);
}
})
这样子就可以正常通过了
更多的讨论
通过上面的分析和修改,我们的问题已经解决了
但是这还不够
来做一些测试更深入地讨论一些这个问题
1 在ajax提交的数据中加入Account没有的属性
const sendData = JSON.stringify({
username: $('#username').val(),
password: $('#password').val(),
ajax: true
});
比如像代码中的一样,后台没有报错 (我印象中好像时会报错的,是我记错了还是版本更新了…)
不论是request.getParameter()还是@RequestParam都是无法获取到的,它们无法解析json数据
所以如果是使用@RequestBody来接收数据,那么前端应该是与后台的对象一一对应进行传输
2 去掉@ReuqestBody
如果去掉了@ReuqestBody注解,那么SpringMVC将无法解析json数据但可以解析‘application/x-www-form-urlencoded’的数据
比如这个样子
@PostMapping("")
@ResponseStatus(HttpStatus.CREATED)
public PlatformResult createAccount(@Validated(CreateGroup.class) Account account, @RequestParam("ajax") boolean ajax) {
if(accountService.getAccount(account.getUsername()) != null) {
throw new BusinessException("Account is exist");
}
account.setFavouriteCategoryId("CATS");
account.setLanguagePreference("english");
accountService.insertAccount(account);
return PlatformResult.success(accountService.getAccount(account.getUsername()));
}
前端ajax传输的数据
$.ajax({
url: "http://localhost:8080/account/",
type: "POST",
dataType: 'json',
data: {
username: $('#username').val(),
password: $('#password').val(),
ajax: true
},
success: function (res) {
if (res.code == 1) {
$('#redirect').attr('href', '../catalog/Main.html');
document.getElementById('redirect').click();
} else {
$('#newAccountMessage').text(res.msg);
}
},
error: function (res) {
$('#newAccountMessage').text(res.msg);
}
})
这种方式有一些优势
- 可以附带Account没有的属性,Account具有的属性会自动注入
- 对表单也适用
- bean validation也同样起作用
到底要不要@ReuqestBody注解
好了,最后回到标题
我们究竟什么时候去使用@ReuqestBody,比较之后我觉得大致是以下几点
- 带了@RequestBody的对象与不带的对象,SpringMVC注入的方式是不同的,前者是解析json字符串,由jackson来完成,后者是解析url
- @ReuqestBody可以来规约数据,前后端数据交互应该定义好DTO(Data Transfer Object),这样可以更加规范
- 在一些客户端只能提交json数据的场景,比如小程序默认就是json(当然可以自己设置contentType)