【小家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,是甩锅给客户端的行为。


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

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
2月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
60 2
|
3月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
2月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
171 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
3月前
|
前端开发 安全 Java
技术进阶:使用Spring MVC构建适应未来的响应式Web应用
【9月更文挑战第2天】随着移动设备的普及,响应式设计至关重要。Spring MVC作为强大的Java Web框架,助力开发者创建适应多屏的应用。本文推荐使用Thymeleaf整合视图,通过简洁的HTML代码提高前端灵活性;采用`@ResponseBody`与`Callable`实现异步处理,优化应用响应速度;运用`@ControllerAdvice`统一异常管理,保持代码整洁;借助Jackson简化JSON处理;利用Spring Security增强安全性;并强调测试的重要性。遵循这些实践,将大幅提升开发效率和应用质量。
72 7
|
3月前
|
负载均衡 网络协议 应用服务中间件
web群集--rocky9.2源码部署nginx1.24的详细过程
Nginx 是一款由 Igor Sysoev 开发的开源高性能 HTTP 服务器和反向代理服务器,自 2004 年发布以来,以其高效、稳定和灵活的特点迅速成为许多网站和应用的首选。本文详细介绍了 Nginx 的核心概念、工作原理及常见使用场景,涵盖高并发处理、反向代理、负载均衡、低内存占用等特点,并提供了安装配置教程,适合开发者参考学习。
|
3月前
|
前端开发 测试技术 开发者
MVC模式在现代Web开发中有哪些优势和局限性?
MVC模式在现代Web开发中有哪些优势和局限性?
|
4月前
|
开发者 前端开发 Java
架构模式的诗与远方:如何在MVC的田野上,用Struts 2编织Web开发的新篇章
【8月更文挑战第31天】架构模式是软件开发的核心概念,MVC(Model-View-Controller)通过清晰的分层和职责分离,成为广泛采用的模式。随着业务需求的复杂化,Struts 2框架应运而生,继承MVC优点并引入更多功能。本文探讨从MVC到Struts 2的演进,强调架构模式的重要性。MVC将应用程序分为模型、视图和控制器三部分,提高模块化和可维护性。
50 0
|
4月前
|
Java 开发者 前端开发
Struts 2、Spring MVC、Play Framework 上演巅峰之战,Web 开发的未来何去何从?
【8月更文挑战第31天】在Web应用开发中,Struts 2框架因强大功能和灵活配置备受青睐,但开发者常遇配置错误、类型转换失败、标签属性设置不当及异常处理等问题。本文通过实例解析常见难题与解决方案,如配置文件中遗漏`result`元素致页面跳转失败、日期格式不匹配需自定义转换器、`&lt;s:checkbox&gt;`标签缺少`label`属性致显示不全及Action中未捕获异常影响用户体验等,助您有效应对挑战。
94 0
|
4月前
|
Java 前端开发 Apache
Apache Wicket与Spring MVC等Java Web框架大PK,究竟谁才是你的最佳拍档?点击揭秘!
【8月更文挑战第31天】在Java Web开发领域,众多框架各具特色。Apache Wicket以组件化开发和易用性脱颖而出,提高了代码的可维护性和可读性。相比之下,Spring MVC拥有强大的生态系统,但学习曲线较陡;JSF与Java EE紧密集成,但在性能和灵活性上略逊一筹;Struts2虽成熟,但在RESTful API支持上不足。选择框架时还需考虑社区支持和文档完善程度。希望本文能帮助开发者找到最适合自己的框架。
52 0
|
4月前
|
存储 前端开发 数据库
神秘编程世界惊现强大架构!Web2py 的 MVC 究竟隐藏着怎样的神奇魔力?带你探索实际应用之谜!
【8月更文挑战第31天】在现代 Web 开发中,MVC(Model-View-Controller)架构被广泛应用,将应用程序分为模型、视图和控制器三个部分,有助于提高代码的可维护性、可扩展性和可测试性。Web2py 是一个采用 MVC 架构的 Python Web 框架,其中模型处理数据和业务逻辑,视图负责呈现数据给用户,控制器则协调模型和视图之间的交互。
42 0