Spring怎么读取请求体的数据
在Spring框架下我们写接口时,不用从原始的request中解析参数,而是可以直接用java对象接收即可,常见的形式如下:
@PostMapping(value = "/name")
@ResponseBody
public String name(@RequestBody String name) {
System.out.println(name);
return "ok";
}
@PostMapping(value = "/user")
@ResponseBody
public String user(@RequestBody UserModel userModel) {
System.out.println(userModel);
return "ok";
}
其中 @ResponseBody表示返回字符串数据,而不是返回页面。@RequestBody 表示使用RequestResponseBodyMethodProcessor从请求体中解析参数。请求体中的数据格式有多种,如json、xml、text、二进制流。对于同一种请求体格式,我们在写Controller层接口时,参数类型可以有多种。如json、xml、text格式的数据,在Controller都可以用String格式接收。同样的,同一种请求数据格式,在Controller层可以用多种数据类型接收,如json格式的请求数据,既可以用String接收,又可以用实体类接收。
Spring是怎么实现的呢?是通过 HandlerMethodArgumentResolver 和 HttpMessageConverter完成的。
- HandlerMethodArgumentResolver判断是否支持Controller接口上的参数转换,不仅要考虑参数类型,也要考虑参数注解。下面两个接口参数类型都一样,但是他们使用的 HandlerMethodArgumentResolver 却不一样,因为参数上的注解不同,导致Spring选用不同的 HandlerMethodArgumentResolver ,因为要从请求的不同位置读取数据。
@PostMapping(value = "/name1")
@ResponseBody
public String name1(@RequestBody String name) {
System.out.println(name);
return "ok";
}
@PostMapping(value = "/name2")
@ResponseBody
public String name2(@RequestParam String name) {
System.out.println(name);
return "ok";
}
HandlerMethodArgumentResolver 的supportsParameter(MethodParameter parameter);方法返回true表示这个 HandlerMethodArgumentResolver支持这种参数解析。上面的两个方法的 HandlerMethodArgumentResolver分别是RequestResponseBodyMethodProcessor和RequestParamMethodArgumentResolver,他们的supportsParameter方法如下
// RequestResponseBodyMethodProcessor
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}
// RequestParamMethodArgumentResolver
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
return (requestParam != null && StringUtils.hasText(requestParam.name()));
}
else {
return true;
}
}
else {
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return false;
}
parameter = parameter.nestedIfOptional();
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
}
else if (this.useDefaultResolution) {
return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
}
else {
return false;
}
}
}
对于一些不带注解的参数,Spring也会默认帮我们解析,如HttpServletRequest ,这也是因为有相应的参数解析器ServletRequestMethodArgumentResolver
@GetMapping("/name3")
@ResponseBody
public String name3(HttpServletRequest request) {
System.out.println(request.getParameter("name"));
return "ok";
}
// ServletRequestMethodArgumentResolver
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
(Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations()) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
- HttpMessageConverter是完成请求体数据和目标数据类型转化的过程。如果 HttpMessageConverter的canRead(Class<?> clazz, @Nullable MediaType mediaType);方法返回true,表示这个**HttpMessageConverter**支持将mediaType的请求体数据转成Class<?>类型的Controller参数。我们看两个常用的HttpMessageConverter。
StringHttpMessageConverter
@Override
// 判断能否将mediaType格式的请求提数据转换成clazz类型的参数
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
return supports(clazz) && canRead(mediaType);
}
// StringHttpMessageConverter只能尝试转成String类型的参数
@Override
public boolean supports(Class<?> clazz) {
return String.class == clazz;
}
// 判断StringHttpMessageConverter是否支持这种mediaType格式
protected boolean canRead(@Nullable MediaType mediaType) {
if (mediaType == null) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.includes(mediaType)) {
return true;
}
}
return false;
}
// StringHttpMessageConverter默认支持所有格式
public StringHttpMessageConverter(Charset defaultCharset) {
super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);
}
注意,再SpringMVC中默认添加的StringHttpMessageConverter字符集是ISO_8859_1,中文参数会乱码。需要自己注册一个,SpringBoot自动注册的就是UTF-8格式的,所以可以正确处理中文。
// SpringMVC注册的ISO_8859_1字符集的StringHttpMessageConverter
public RequestMappingHandlerAdapter() {
this.messageConverters = new ArrayList<>(4);
this.messageConverters.add(new ByteArrayHttpMessageConverter());
// 使用ISO_8859_1字符集
this.messageConverters.add(new StringHttpMessageConverter());
if (!shouldIgnoreXml) {
try {
this.messageConverters.add(new SourceHttpMessageConverter<>());
}
catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
}
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}
// SpringMVC项目中手动注册UTF_8字符集的StringHttpMessageConverter
@Bean
public StringHttpMessageConverter stringHttpMessageConverter() {
return new StringHttpMessageConverter(StandardCharsets.UTF_8);
}
// SpringBoot自动注册UTF_8字符集的StringHttpMessageConverter
// org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration.StringHttpMessageConverterConfiguration
// 如果没有在配置文件中配置server.servlet.encoding,则默认就是UTF_8字符集
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(StringHttpMessageConverter.class)
protected static class StringHttpMessageConverterConfiguration {
@Bean
@ConditionalOnMissingBean
public StringHttpMessageConverter stringHttpMessageConverter(Environment environment) {
Encoding encoding = Binder.get(environment).bindOrCreate("server.servlet.encoding", Encoding.class);
StringHttpMessageConverter converter = new StringHttpMessageConverter(encoding.getCharset());
converter.setWriteAcceptCharset(false);
return converter;
}
}
MappingJackson2HttpMessageConverter
@Override
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
return canRead(clazz, null, mediaType);
}
@Override
public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {
if (!canRead(mediaType)) {
return false;
}
JavaType javaType = getJavaType(type, contextClass);
// 根据参数类型和mediaType选择合适的ObjectMapper
ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), mediaType);
if (objectMapper == null) {
return false;
}
AtomicReference<Throwable> causeRef = new AtomicReference<>();
// 判断选出的ObjectMapper 是否可以反序列化目标参数类型
if (objectMapper.canDeserialize(javaType, causeRef)) {
return true;
}
logWarningIfNecessary(javaType, causeRef.get());
return false;
}
现在我们知道Spring是怎么默默完成从请求体数据向接口参数类型的转化了:
- 根据参数类型和注解确认使用哪个HandlerMethodArgumentResolver
- 判断这个HandlerMethodArgumentResolver里是否有合适的HttpMessageConverter
- 一般用于接收请求体数据的参数类型是String和实体类(包括集合数据),前者是用StringHttpMessageConverter,后者使用MappingJackson2HttpMessageConverter。StringHttpMessageConverter是直接读取请求体的inputStream,再转成String。MappingJackson2HttpMessageConverter是借助ObjectMapper 的各种JsonDeserializer完成数据转化(即反序列化),可以看到JsonDeserializer的实现类很多,常见的反序列化场景都能满足。但是对枚举类的反序列化支持不够友好,只能根据枚举的ordinal值进行反序列化,而我们一般会自定义code值,我们更希望根据自定义code值反序列化成对应的枚举对象。
了解以上背景后我们可以做两个扩展
- 自定义参数解析器,根据自定义的@User注解,从请求头中解析出用户信息
- 自定义反序列化器,支持数字到枚举的反序列化
可以看到,Spring参数解析器、消息转换器是从两个维度定义参数解析。参数解析器确定从哪里读取数据,消息转换器是真正执行消息转换的工作。我们也可以根据需要,从这两个维度进行自定义。