Request请求read请求参数、请求body时消息转换器的匹配(本文重点)
相应的,处理请求@RequestBody
的处理器选择,也发生在RequestResponseBodyMethodProcessor
里
此处以这个处理器为例进行讲解:
@ResponseBody @RequestMapping(value = "/hello/post", method = RequestMethod.POST) public Person hello(@RequestBody Person person) { return person; }
若我们用postman发如下请求:‘
服务端日志也能收到如下的警告信息:
我们收到的竟然是一个报错,what竟然不支持我的类型???
其实在这之前,有小伙伴问过这啥情况?这就是为什么我要解释上面的Http基础原理的原因了。
如图其实根本原因是Postman给我们发送请求的时候,默认给我们发送了一个content-type,有点自作主张了,所以导致的这问题。(Chome是不会这样自作主张的~~~)
解决方案其实非常简单:我们自己指定一个Content-Type:application/json就木问题了~
至于根本原因,请看下面的源码分析便一目了然。
RequestResponseBodyMethodProcessor匹配入参的消息转换器
我们可以先看看RequestResponseBodyMethodProcessor这个处理器:
// @since 3.1 public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestBody.class); } ... @Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 入参是支持使用Optional包装一层的~~~ parameter = parameter.nestedIfOptional(); // 这个方法就特别重要了,实现就在下面,现在强烈要求吧目光先投入到下面这个方法实现上~~~~ Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); // 拿到入参的形参的名字 比如此处为person String name = Conventions.getVariableNameForParameter(parameter); // 下面就是进行参数绑定、数据适配、转换的逻辑了 这个在Spring MVC处理请求参数这一章会详细讲解 // 数据校验@Validated也是在此处生效的 if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); if (arg != null) { validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } if (mavContainer != null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } } return adaptArgumentIfNecessary(arg, parameter); } // 之前讲过writeWithMessageConverters,这个相当于读的时候进行一个消息转换器的匹配,实现逻辑大体一致~~~ @Override protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); Assert.state(servletRequest != null, "No HttpServletRequest"); ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest); // 核心是这个方法,它的实现逻辑在父类AbstractMessageConverterMethodArgumentResolver上~~~继续转移目光吧~~~ Object arg = readWithMessageConverters(inputMessage, parameter, paramType); // body体为空,而且不是@RequestBody(required = false),那就抛错呗 请求的body是必须的 这个很好理解 if (arg == null && checkRequired(parameter)) { throw new HttpMessageNotReadableException("Required request body is missing: " + parameter.getExecutable().toGenericString(), inputMessage); } return arg; } }
上面已经分析到,读取的核心匹配逻辑,其实在父类AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters,因此我们继续看父类的实现源码:
// @since 3.1 可议看出这个实现是处理请求request的处理器~~~ public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver { ... @SuppressWarnings("unchecked") @Nullable protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { MediaType contentType; boolean noContentType = false; // 如果已经制定了contentType,那就没什么好说的 以你的为准 try { contentType = inputMessage.getHeaders().getContentType(); } catch (InvalidMediaTypeException ex) { throw new HttpMediaTypeNotSupportedException(ex.getMessage()); } // Content-Type默认值:application/octet-stream if (contentType == null) { noContentType = true; contentType = MediaType.APPLICATION_OCTET_STREAM; } // 这几句代码就是解析出入参的类型~ Class<?> contextClass = parameter.getContainingClass(); Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null); if (targetClass == null) { ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter); targetClass = (Class<T>) resolvableType.resolve(); } HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null); Object body = NO_VALUE; EmptyBodyCheckingHttpInputMessage message; try { message = new EmptyBodyCheckingHttpInputMessage(inputMessage); // 这里就是匹配消息转换器的的逻辑~~~ 还是一样的 优先以GenericHttpMessageConverter这种类型的转换器为准 for (HttpMessageConverter<?> converter : this.messageConverters) { Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass(); GenericHttpMessageConverter<?> genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null); if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) : (targetClass != null && converter.canRead(targetClass, contentType))) { // 因为它是一个EmptyBodyCheckingHttpInputMessage,所以它有这个判断方法 body != null表示有body内容 // 注意此处的判断,若有body的时候,会执行我们配置的`RequestBodyAdvice` 进行事前、时候进行处理~~~ 后面我会示范 if (message.hasBody()) { HttpInputMessage msgToUse = getAdvice().beforeBodyRead(message, parameter, targetType, converterType); // 执行消息转换器的read方法,从inputStream里面读出一个body出来~ body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse)); // 读出body后的事后工作~~~ body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType); } // 如果没有body体,就交给handleEmptyBody来处理~~~~ 第一个参数永远为null // 这个处理器可以让我们给默认值,比如body为null时,我们返回一个默认的body之类的吧 else { body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType); } break; } } } catch (IOException ex) { throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage); } // 到此处,如果body还没有被赋值过~~~~ 此处千万不能直接报错,因为很多请求它可以不用body体的 // 比如post请求,body里有数据。但是Content-type确实text/html 就会走这里(因为匹配不到消息转换器) if (body == NO_VALUE) { if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) || (noContentType && !message.hasBody())) { return null; } // 比如你是post方法,并且还指定了ContentType 你有body但是却没有找到支持你这种contentType的消息转换器,那肯定就报错了~~~ // 这个异常会被InvocableHandlerMethod catch住,虽然不会终止程序。但是会打印一个warn日志~~~ // 并不是所有的类型都能read的,从上篇博文可议看出,消息转换器它支持的可以read的类型还是有限的 throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes); } MediaType selectedContentType = contentType; Object theBody = body; LogFormatUtils.traceDebug(logger, traceOn -> { String formatted = LogFormatUtils.formatValue(theBody, !traceOn); return "Read \"" + selectedContentType + "\" to [" + formatted + "]"; }); return body; } ... }
借助RequestBodyAdvice实现对请求参数进行干预
RequestBodyAdvice
它能对入参body封装的前后、空进行处理。比如下面我们就可以很好的实现日志打打印:
public class LogRequestBodyAdvice implements RequestBodyAdvice { @Override public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { return true; } @Override public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException { return httpInputMessage; } @Override public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { Method method=methodParameter.getMethod(); log.info("{}.{}:{}",method.getDeclaringClass().getSimpleName(),method.getName(),JSON.toJSONString(o)); return o; } @Override public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { Method method=methodParameter.getMethod(); log.info("{}.{}",method.getDeclaringClass().getSimpleName(),method.getName()); return o; } }
这样请求的时候都能记录上如下日志:
效率非常高,也不需要对request的body进行多次读取了。
使用@ControllerAdvice注解+ResponseBodyAdvice+ResponseBodyAdvice,可以对请求的输入输出进行处理,避免了在controller中对业务代码侵入。FastJson有提供一个JSONPResponseBodyAdvice可以直接使用的
它还有一个很不错的应用场景:就是对请求、返回数据进行加密、解密
基于RequestBodyAdvice和ResponseBodyAdvice来实现spring中参数的加密和解密
自定义消息转换器HttpMessageConverter【并让其生效】
虽然前面说了,Spring MVC已经为我们准备了好多个消息转换器了,能应付99.99%的使用场景了。
但是我们经常会遇到说不喜欢用它自带的Jackson来序列化,而想换成我们想要的国产的FastJson转换器。怎么弄呢???那么接下来就来实现这一波
其实FastJson还是非常友好的,早在@since 1.2.10版本就已经为我们写好了FastJsonHttpMessageConverter,我们直接使用即可。
Spring MVC内置支持了jackson,gson。但却没有内置支持我们国产的FastJson,看来我们还得加油啊~~(因此我们要想它生效,还得自己动动手~)
我们这样配置:
@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { // 请不要复写这个方法,否则你最终只有你配置的转换器,Spring MVC默认的并不会加载了~~ //@Override //public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { // FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); // fastJsonHttpMessageConverter.setCharset(StandardCharsets.UTF_8); // converters.add(fastJsonHttpMessageConverter); //} @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); fastJsonHttpMessageConverter.setCharset(StandardCharsets.UTF_8); converters.add(fastJsonHttpMessageConverter); } }
这样我们看到我们的转换器如下:
可议看到我们的FastJsonHttpMessageConverter
已经被配置进去了。
此处我们把Handler写成这样:
@ResponseBody @RequestMapping(value = "/hello/post", method = RequestMethod.POST) public Object hello(@RequestBody(required = false) Person person) { return person; }
关于read: 关于writer:
总结一句:我们发现不管是读还是写,最终生效的都还是MappingJackson2HttpMessageConverter,我们自定义的FastJson转换器并没有生效。相信这个原因大家都知道了:FastJson转换器排在Jackson转换器的后面,所以处理json不会生效
那怎么破呢???
找到原因,那就只要把 自定义的消息转换器**【FastJsonJsonpHttpMessageConverter】添加到 MappingJackson2HttpMessageConverter 前面就可以**
如果你是SpringBoot环境,你可以直接使用HttpMessageConverters很方便的把你自定义的转换器提到最高优先级,但是此处我们介绍一下Spring中的处理方式:
我们只需要这么配置就行了:
@Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); fastJsonHttpMessageConverter.setCharset(StandardCharsets.UTF_8); // 把FastJsonHttpMessageConverter放到Jackson的前面去 // 这个list是个ArrayList 所以我们要么就放在首位(不建议),而converters.indexOf() 因为人家是new的 所以肯定是找不到此对象的位置的 所以采用遍历的方式吧 int jacksonIndex = 0; for (int i = 0; i < converters.size(); i++) { if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) { jacksonIndex = i; break; } } // 添加在前面 converters.add(jacksonIndex, fastJsonHttpMessageConverter); }
看看效果:
终效果也是没有问题的,json数据的转换工作都会被我们的FastJson接管了,完美~
我看到有文章说可以通过HttpMessageConverters这种方式配置自定义的消息转换器,那是不眼睛的。因为HttpMessageConverters它属于SpringBoot的类,而不是属于Spring Framework的,所以别被误导了~
FastJsonHttpMessageConverter的坑和正确使用姿势
绝大多数情况下,直接使用FastJsonHttpMessageConverter是没有问题的。但是如果是这样的:
@ResponseBody @RequestMapping(value = "/hello/post", method = RequestMethod.POST) public Object hello(@RequestBody(required = false) MultiValueMap person) { return person; }
请求报错如下:
JSON parse error: unsupport type interface org.springframework.util.MultiValueMap; nested exception is com.alibaba.fastjson.JSONException: unsupport type interface org.springframework.util.MultiValueMap]
我们发现报错竟然找到了FastJson
的头上。说明了什么:责任链模式下,fastjson接了这个活,最终发现自己干不了,然后还抛出异常,这是明显的甩锅行为嘛~
这个根本原因在这,看它的源码:
// 会发现FastJson这个转换器它接受所有类型它都表示可以处理~~~~ @Override protected boolean supports(Class<?> clazz) { return true; }
显然它违反了“手伸得太长的”基本原则,是需要付一定的责任的~~~~
为了避免误伤,其实我们配置它的时候应该限制它的作用范围,如下:
@Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); fastJsonHttpMessageConverter.setCharset(StandardCharsets.UTF_8); // 这步是很重要的,让它只处理application/json这种MediaType的就没有问题了~~~~ List<MediaType> fastMediaTypes = new ArrayList<>(); fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8); fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes); // 把FastJsonHttpMessageConverter放到Jackson的前面去 // 这个list是个ArrayList 所以我们要么就放在首位(不建议),而converters.indexOf() 因为人家是new的 所以肯定是找不到此对象的位置的 所以采用遍历的方式吧 int jacksonIndex = 0; for (int i = 0; i < converters.size(); i++) { if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) { jacksonIndex = i; break; } } // 添加在前面 converters.add(jacksonIndex, fastJsonHttpMessageConverter); }
这样它就不会造成问题了,因为我们只让它处理MediaType.APPLICATION_JSON_UTF8这种类型,而这就是它最擅长的。
吐槽一下FastJsonHttpMessageConverter
作为非Spring MVC自带的组件,默认设置 */* 这种MediaType,是非常不好的。说好听点叫误伤,说难听点是存在存在挂羊头卖狗肉、名实不副的行为。
在Rest现在成为主流的今天,这是一个很不好的做法。自己能力不是最大,却大包大揽承担最大责任,处理不了还返回 HTTP 400,是甩锅给客户端的行为。
阿里作为国内第一大开源阵营,其代码设计、质量,以及开源奉献精神还是要进一步提升啊,要严谨啊