【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---HttpMessageConverter的匹配规则(选择原理)(中)

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
简介: 【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---HttpMessageConverter的匹配规则(选择原理)(中)

Request请求read请求参数、请求body时消息转换器的匹配(本文重点)


相应的,处理请求@RequestBody的处理器选择,也发生在RequestResponseBodyMethodProcessor

此处以这个处理器为例进行讲解:


    @ResponseBody
    @RequestMapping(value = "/hello/post", method = RequestMethod.POST)
    public Person hello(@RequestBody Person person) {
        return person;
    }


若我们用postman发如下请求:‘


image.png


服务端日志也能收到如下的警告信息:


image.png


我们收到的竟然是一个报错,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;
    }
}


这样请求的时候都能记录上如下日志:


image.png


效率非常高,也不需要对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);
    }
}


这样我们看到我们的转换器如下:


image.png

可议看到我们的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);
    }


看看效果:


image.png


终效果也是没有问题的,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,是甩锅给客户端的行为。


阿里作为国内第一大开源阵营,其代码设计、质量,以及开源奉献精神还是要进一步提升啊,要严谨啊

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
3月前
|
XML Java 数据格式
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
这篇文章是Spring5框架的实战教程,主要介绍了如何在Spring的IOC容器中通过XML配置方式使用外部属性文件来管理Bean,特别是数据库连接池的配置。文章详细讲解了创建属性文件、引入属性文件到Spring配置、以及如何使用属性占位符来引用属性文件中的值。
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
|
11天前
|
JavaScript 前端开发 Java
SpringBoot_web开发-webjars&静态资源映射规则
https://www.91chuli.com/ 举例:jquery前端框架
12 0
|
1月前
|
前端开发 Java API
JAVA Web 服务及底层框架原理
【10月更文挑战第1天】Java Web 服务是基于 Java 编程语言用于开发分布式网络应用程序的一种技术。它通常运行在 Web 服务器上,并通过 HTTP 协议与客户端进行通信。
22 1
|
1月前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
51 2
|
2月前
|
安全 关系型数据库 Shell
Web安全-浅析CSV注入漏洞的原理及利用
Web安全-浅析CSV注入漏洞的原理及利用
118 3
|
23天前
|
存储 安全 前端开发
在前端开发中需要考虑的常见web安全问题和攻击原理以及防范措施
在前端开发中需要考虑的常见web安全问题和攻击原理以及防范措施
110 0
|
2月前
|
中间件 API 开发者
深入理解Python Web框架:中间件的工作原理与应用策略
在Python Web开发中,中间件位于请求处理的关键位置,提供强大的扩展能力。本文通过问答形式,探讨中间件的工作原理、应用场景及实践策略,并以Flask和Django为例展示具体实现。中间件可以在请求到达视图前或响应返回后执行代码,实现日志记录、权限验证等功能。Flask通过装饰器模拟中间件行为,而Django则提供官方中间件系统,允许在不同阶段扩展功能。合理制定中间件策略能显著提升应用的灵活性和可扩展性。
39 4
|
1月前
|
前端开发 Java
【案例+源码】详解MVC框架模式及其应用
【案例+源码】详解MVC框架模式及其应用
40 0
|
3月前
|
XML Java 数据格式
Spring5入门到实战------6、IOC容器-Bean管理XML方式(自动装配)
这篇文章是Spring5框架的入门教程,详细讲解了IOC容器中Bean的自动装配机制,包括手动装配、`byName`和`byType`两种自动装配方式,并通过XML配置文件和Java代码示例展示了如何在Spring中实现自动装配。
Spring5入门到实战------6、IOC容器-Bean管理XML方式(自动装配)
|
3月前
|
XML Java 数据格式
Spring5入门到实战------8、IOC容器-Bean管理注解方式
这篇文章详细介绍了Spring5框架中使用注解进行Bean管理的方法,包括创建Bean的注解、自动装配和属性注入的注解,以及如何用配置类替代XML配置文件实现完全注解开发。
Spring5入门到实战------8、IOC容器-Bean管理注解方式
下一篇
无影云桌面