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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 【小家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,是甩锅给客户端的行为。


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

相关实践学习
通过日志服务实现云资源OSS的安全审计
本实验介绍如何通过日志服务实现云资源OSS的安全审计。
相关文章
|
2月前
|
域名解析 网络协议 API
【Azure Container App】配置容器应用的缩放规则 Managed Identity 连接中国区 Azure Service Bus 问题
本文介绍了在 Azure Container Apps 中配置基于自定义 Azure Service Bus 的自动缩放规则时,因未指定云环境导致的域名解析错误问题。解决方案是在扩展规则中添加 `cloud=AzureChinaCloud` 参数,以适配中国区 Azure 环境。内容涵盖问题描述、原因分析、解决方法及配置示例,适用于使用 KEDA 实现事件驱动自动缩放的场景。
|
6月前
|
前端开发 Java 测试技术
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
本文介绍了 `@RequestParam` 注解的使用方法及其与 `@PathVariable` 的区别。`@RequestParam` 用于从请求中获取参数值(如 GET 请求的 URL 参数或 POST 请求的表单数据),而 `@PathVariable` 用于从 URL 模板中提取参数。文章通过示例代码详细说明了 `@RequestParam` 的常用属性,如 `required` 和 `defaultValue`,并展示了如何用实体类封装大量表单参数以简化处理流程。最后,结合 Postman 测试工具验证了接口的功能。
305 0
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
|
23天前
|
设计模式 Java 开发者
如何快速上手【Spring AOP】?从动态代理到源码剖析(下篇)
Spring AOP的实现本质上依赖于代理模式这一经典设计模式。代理模式通过引入代理对象作为目标对象的中间层,实现了对目标对象访问的控制与增强,其核心价值在于解耦核心业务逻辑与横切关注点。在框架设计中,这种模式广泛用于实现功能扩展(如远程调用、延迟加载)、行为拦截(如权限校验、异常处理)等场景,为系统提供了更高的灵活性和可维护性。
|
6月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestBody
`@RequestBody` 是 Spring 框架中的注解,用于将 HTTP 请求体中的 JSON 数据自动映射为 Java 对象。例如,前端通过 POST 请求发送包含 `username` 和 `password` 的 JSON 数据,后端可通过带有 `@RequestBody` 注解的方法参数接收并处理。此注解适用于传递复杂对象的场景,简化了数据解析过程。与表单提交不同,它主要用于接收 JSON 格式的实体数据。
462 0
|
2月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
170 0
|
2月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
106 0
|
2月前
|
JSON 前端开发 Java
第05课:Spring Boot中的MVC支持
第05课:Spring Boot中的MVC支持
134 0
|
5月前
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
373 70
|
4月前
|
存储 应用服务中间件 nginx
在使用Nginx之后,如何在web应用中获取用户IP以及相关原理
但总的来说,通过理解网络通信的基础知识,了解http协议以及nginx的工作方式,我们已经能在大多数情况下准确地获取用户的真实IP地址了,在调试问题或者记录日志时会起到很大的帮助。
246 37