@RequestBody底层实现和实际使用时存在的问题
前言
要实现@RequestBody封装对象的功能要满足以下条件
- 前端提交数据列表的时候用json对象的序列化格式提交
- 后台controller方法的参数是实体类,并且用@RequestBody注解标记
底层实现
流程
- 流程图
- 流程说明(含部分源码)
- 浏览器发出请求之后前端控制器请求处理器映射器处理请求,获取Handler执行链
- 前端控制器请求处理器适配器(RequestMappingHandlerAdapter)进行相关适配
- 适配器调用HandlerMethodArgumentResolver的reolveArgument方法的实现去处理请求参数
3.1 reolveArgument源码
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
//调用适配最合适数据转换器将参数转换成对象,具体如下3.2:
Object arg = this.readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) {
//新建一个数据装订器,将封装对象传入
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
this.validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
//将封装对象的装订结果放入ModelAndViewContainer容器中,方便后面其他地方的获取
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
//将封装对象再适配然后返回
return this.adaptArgumentIfNecessary(arg, parameter);
}
3.2 readWithMessageConverters方法解读
先将请求封装成InputMessage流对象
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
//转换请求的类型
HttpServletRequest servletRequest = (HttpServletRequest)webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
//将请求封装成流对象
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
//调用readWithMessageConverters的具体实现
Object arg = this.readWithMessageConverters(inputMessage, parameter, paramType);
if (arg == null && this.checkRequired(parameter)) {
throw new HttpMessageNotReadableException("Required request body is missing: " + parameter.getExecutable().toGenericString(), inputMessage);
} else {
return arg;
}
}
readWithMessageConverters处理请求参数的实际逻辑
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
boolean noContentType = false;
MediaType contentType;
try {
//获取请求的contentType,可得提交数据的传递方式(字符串类型),看是表单提交还是json还是。。。
contentType = inputMessage.getHeaders().getContentType();
} catch (InvalidMediaTypeException var16) {
throw new HttpMediaTypeNotSupportedException(var16.getMessage());
}
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
Class<?> contextClass = parameter.getContainingClass();
Class<T> targetClass = targetType instanceof Class ? (Class)targetType : null;
if (targetClass == null) {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
targetClass = resolvableType.resolve();
}
HttpMethod httpMethod = inputMessage instanceof HttpRequest ? ((HttpRequest)inputMessage).getMethod() : null;
Object body = NO_VALUE;
AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage message;
try {
label94: {
message = new AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage(inputMessage);
/*
这里是封装参数的重点逻辑
*/
//获取所有数据转换器,并添加迭代器进行遍历
Iterator var11 = this.messageConverters.iterator();
HttpMessageConverter converter;
Class converterType;
GenericHttpMessageConverter genericConverter;
//开始循环逐个取出数据转换器
while(true) {
if (!var11.hasNext()) {
break label94;
}
//将取出的转换器强转成GenericHttpMessageConverter(意图此处不深究)
converter = (HttpMessageConverter)var11.next();
converterType = converter.getClass();
genericConverter = converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter)converter : null;
if (genericConverter != null) {
//调用所有转换器都去实现的HttpMessageConverter接口的canRead方法
// 若返回true则此转换器可用于当前请求参数的转换,即跳出循环
// 若返回false则不可用,继续遍历其他转换器
if (genericConverter.canRead(targetType, contextClass, contentType)) {
break;
}
} else if (targetClass != null && converter.canRead(targetClass, contentType)) {
break;
}
}
if (message.hasBody()) {
HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
//穿UR参数类型,要封装成的对象的类型和请求数据流,调用转换器的read方法,将请求参数封装成bean对象
body = genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : converter.read(targetClass, msgToUse);
body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
} else {
body = this.getAdvice().handleEmptyBody((Object)null, message, parameter, targetType, converterType);
}
}
} catch (IOException var17) {
throw new HttpMessageNotReadableException("I/O error while reading input message", var17, inputMessage);
}
if (body != NO_VALUE) {
LogFormatUtils.traceDebug(this.logger, (traceOn) -> {
String formatted = LogFormatUtils.formatValue(body, !traceOn);
return "Read \"" + contentType + "\" to [" + formatted + "]";
});
return body;
} else if (httpMethod != null && SUPPORTED_METHODS.contains(httpMethod) && (!noContentType || message.hasBody())) {
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
} else {
return null;
}
}
- 将参数列表封装成java对象之后,回到resolverArgument方法,将对象进行进一步装订,放入ModelAndViewContain容器中并返回
- 逐步返回到适配器中,在返回中,逐步对封装对象进行再封装
- 适配器将对象与请求的方法的参数进行绑定,再进行其他适配,然后掉Handler(Controller)的方法
补充
- 转换器示例
HttpMessageConverter接口有很多个实现类,都是不同的数据转换器所以在寻找适配请求参数的转换器的时候,我们要获取适配器生成好的所有转换器,然后逐个遍历找出适用的那一个
//负责读取资源文件和写出资源文件数据
ResourceHttpMessageConverter
//负责读取form提交的数据(能读取的数据格式为 application/x-www-form-urlencoded,不能读取multipart/form-data格式数据);负责写入application/x-www-from-urlencoded和multipart/form-data格式的数据
FormHttpMessageConverter
//负责读取和写入json格式的数据
MappingJacksonHttpMessageConverter
//负责读取和写入 xml 中javax.xml.transform.Source定义的数据
SouceHttpMessageConverter
//负责读取和写入xml 标签格式的数据
Jaxb2RootElementHttpMessageConverter
//负责读取和写入Atom格式的数据
AtomFeedHttpMessageConverter
//负责读取和写入RSS格式的数据
RssChannelHttpMessageConverter
HttpMessageConverter接口定义了如下五个方法,所有转换器都要实现这些方法。在@RequestBody功能实现过程中,比较重要的是canRead和read这两个方法。我寻找使用的转换器时,遍历转换器集合,逐个调用canRead(canWrite)方法判断该转换器是否可用;找到适用的转换器后调用read(write)方法去将请求参数装换成Bean对象
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> var1, @Nullable MediaType var2);
boolean canWrite(Class<?> var1, @Nullable MediaType var2);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;
void write(T var1, @Nullable MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}
实际使用
- 表单提交的时候后台可用@RequestBody标记String类型的参数body去获取整个请求,这里只是直接获取键值对字符串,不进行封装
<form action="anno/testRequestBody1" method="post"><br>
姓名:<input type="text" name="name"><br>
密码:<input type="text" name="password"><br>
<input type="submit" value="提交">
</form>
@RequestMapping("/testRequestBody1")
public String testRequestBody1(@RequestBody String body) throws UnsupportedEncodingException {
body = URLDecoder.decode(body, "utf-8");
System.out.println(body);
return "success";
}
2.若前端表单提交,后台用@RequestBody标记实体类去接收会报错
Content type ‘application/x-www-form-urlencoded;charset=UTF-8’ not supported
- 表单提交一
<form action="anno/testRequestBody5" method="post"><br>
姓名:<input type="text" name="name"><br>
密码:<input type="text" name="password"><br>
<input type="submit" value="提交">
</form>
- 表单提交二
function testAjax6() {
$.ajax({
url:"anno/testRequestBody5",
type:"post",
data:'{"name": "嘿嘿", "password": "123"}',
//不写contentType也是默认表单提交
contentType:"application/x-www-form-urlencoded;charset=utf-8",
dataType:"json",
success:function (data) {
alert(data);
}
})
}
- 后台处理
@RequestMapping(value = "/testRequestBody5")
public void testRequestBody5(@RequestBody User user) {
System.out.println("testRequestBody5方法执行了");
System.out.println(user);
}
以上两种方式都会报错
- 正确的封装产生的写法
- js写法一
function testAjax1() {
$.ajax({
url:"anno/testRequestBody5",
type:"post",
data:'{"name": "嘿嘿", "password": "123"}',
contentType:"application/json;charset=utf-8",
dataType:"json",
success:function (data) {
alert(data);
}
})
}
- js写法二
function testAjax2() {
var name = "嘿嘿";
var password = "123";
var age = 21;
var id = 1;
$.ajax({
url:"anno/testRequestBody5",
type:"post",
data:'{"id": '+id+',"name": "'+name+'", "password": "'+password+'", "age": '+age+'}',
contentType:"application/json;charset=utf-8",
dataType:"json",
success:function (data) {
alert(data);
}
})
}
- js写法三
function testAjax3() {
var name = "嘿嘿";
var password = "123";
var json = {"name": name, "password": password};
$.ajax({
url:"anno/testRequestBody5",
type:"post",
data: JSON.stringify(json),
contentType:"application/json;charset=utf-8",
dataType:"json",
success:function (data) {
alert(data);
}
})
}
- 后台接收
@RequestMapping(value = "/testRequestBody5")
public void testRequestBody5(@RequestBody User user) {
System.out.println("testRequestBody5方法执行了");
System.out.println(user);
}
- 错误写法,此处json对象没有序列化成字符串,故封装时匹配不上,报错如下:
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Unrecognized token ‘name’: was expecting ‘null’, ‘true’, ‘false’ or NaN; nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token ‘name’: was expecting ‘null’, ‘true’, ‘false’ or NaN
function testAjax4() {
var name = "嘿嘿";
var password = "123";
var json = {"name": name, "password": password};
$.ajax({
url:"anno/testRequestBody5",
type:"post",
data:json,
contentType:"application/json;charset=utf-8",
dataType:"json",
success:function (data) {
alert(data);
}
})
}