栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

SpringBoot自定义MessageConverter及其源码分析

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

SpringBoot自定义MessageConverter及其源码分析

SpringBoot自定义MessageConverter及其源码分析

正在学习SpringBoot,在自定义MessageConverter时发现:为同一个返回值类型配置多个MessageConverter时,可能会发生响应数据格式错误,或406异常(客户端无法接收相应数据)。在此记录一下解决问题以及追踪源码的过程。此处的讨论场景为:基于请求参数的内容协商,详见需求描述。

一 需求描述
  1. 前提条件:浏览器访问路径为http://localhost:8080/showPerson,服务器端对应的Controller如下:
@RestController
public class ParamController {
    @GetMapping("/showPerson")
    public Person showPerson(Person person){
        person.setUserName("lisi");
        person.setAge(35);
        return person;
    }
}
  1. 需求:建立两个自定义的MessageConverter,并利用URL请求路径中携带的format参数来控制使用哪个converter。举例来说:

    当发送请求 http://localhost:8080/showPerson?format=something时,需要的响应格式为:lisi - something - 35
    当发送请求 http://localhost:8080/showPerson?format=anything时,需要的响应格式为:lisi - anything - 35

  • PS:用 POST方式请求或许更合理,这里为了省事儿直接用 GET了。
二 最初的实现
  1. 根据上面的需求,我们首先创建了如下的配置类:
@Configuration(proxyBeanMethods = false)
public class WebConfig  {

    //1、WebMvcConfigurer定制化SpringMVC的功能
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            @Override
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
				// 创建Map,用于收集MediaType信息
                Map mediaTypes = new HashMap<>();
                mediaTypes.put("json",MediaType.APPLICATION_JSON);
                mediaTypes.put("xml",MediaType.APPLICATION_XML);
                // 添加自定义的媒体类型
                mediaTypes.put("something", MediaType.parseMediaType("application/something"));
                mediaTypes.put("anything", MediaType.parseMediaType("application/anything"));
               
                // 指定支持解析哪些参数对应的哪些媒体类型
                ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
				// 添加
                HeaderContentNegotiationStrategy headeStrategy = new HeaderContentNegotiationStrategy();
				configurer.strategies(Arrays.asList(parameterStrategy,headeStrategy));
            }

            @Override
            public void extendMessageConverters(List> converters) {
                
                converters.add(new SomethingMessageConverter());
                converters.add(new AnythingMessageConverter());
            }
        };
    }
}
  1. 随后定义对应something和anything的两个MessageConverter(最初版本,有bug):
// 将Person转换为"lisi - something - 35"格式的converter
public class SomethingMessageConverter implements HttpMessageConverter {
    @Override
    public boolean canRead(Class clazz, MediaType mediaType) {
        // 我们不需要用MessageConverter处理参数,所以这里直接返回false
        return false;
    }

    @Override
    public boolean canWrite(Class clazz, MediaType mediaType) {
        // 根据clazz和Person的关系进行判断。
        return Person.class.isAssignableFrom(clazz);
    }

    @Override
    public List getSupportedMediaTypes() {
        return MediaType.parseMediaTypes("application/something");
    }

    @Override
    public Person read(Class clazz, HttpInputMessage inputMessage) 
    			throws IOException, HttpMessageNotReadableException {
        // 不需要此功能,直接返回null
        return null;
    }

	// 决定最终输出格式的write方法.
    @Override
    public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) 
    			throws IOException, HttpMessageNotWritableException {
        String returnData = person.getUserName() + " - something - " + person.getAge();
        OutputStream body = outputMessage.getBody();
        body.write(returnData.getBytes(StandardCharsets.UTF_8));
    }
}

// 将Person转换为"lisi - anything- 35"格式的converter
public class AnythingMessageConverter implements HttpMessageConverter {
    @Override
    public boolean canRead(Class clazz, MediaType mediaType) {
        return false;
    }

    @Override
    public boolean canWrite(Class clazz, MediaType mediaType) {
        // 根据clazz和Person的关系进行判断。
        return Person.class.isAssignableFrom(clazz);
    }

    @Override
    public List getSupportedMediaTypes() {
        return MediaType.parseMediaTypes("application/anything");
    }

    @Override
    public Person read(Class clazz, HttpInputMessage inputMessage)
    			throws IOException, HttpMessageNotReadableException {
        return null;
    }

    @Override
    public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) 
    			throws IOException, HttpMessageNotWritableException {
        String returnData = person.getUserName() + " - anything - " + person.getAge();
        OutputStream body = outputMessage.getBody();
        body.write(returnData.getBytes(StandardCharsets.UTF_8));
    }
}

  1. 跑起来,似乎没什么问题:
  2. 再试试发送anything的请求参数,bug如约而至:
    anything 请求返回的响应格式仍然是 something。 那么,接下来去追一下源码,看看错误是如何产生的。
三 源码追踪
  1. IDEA以debug模式启动,然后向服务器发送anything的请求。debug肯定是从老朋友DispatcherServlet开始,找到它,把断点打在doDispatch()方法内部的ha.handle()方法调用处。

  2. 按顺序step into 以下名称的方法:

    • handle() -> handleInternal() -> invokeHandlerMethod() -> invokeAndHandle() -> handleReturnValue() -> …略… -> writeWithMessageConverters() -> getAcceptableMediaTypes() ->

    • 按照上面的处理返回值的路线,一路追踪到这里(顺便补充SpringBoot版本为2.5.5):

      可以看见strategies中包含了自定义的媒体类型,显然,我们在WebConfig中自定义的策略是有效的。继续step into这个do while 循环中的方法。

    • 接下来该方法根据URL中的format参数(key: “anything”)来从请求域中取出对应的mediaType( request.getParameter(xxx) )并将其存储到acceptableTypes中(*补充说明:基于请求参数的内容协商需要yml的配置才能生效:spring.mvc.contentnegotiation.favor-parameter=true,设置之后,底层容器才会提供对应的参数解析策略ParameterContentNegotiationStrategy *):

      这里获取到的mediaType对应着anything,是正确的。到这里一切正常,acceptableTypes是我们预料的样子,那么问题大概率出在producibleTypes身上。(因为Spring的内容协商就是基于这两个集合的)

    • 继续step into,到messageConverters第一次出现在视野中:

      可以看到,我们自定义的两个Converter存在于其中。那么接下来是关键点1——调用每个Converter的canWrite方法,判断是否是符合需求的媒体类型,如果判断通过,则将对应的mediaType添加到result当中,注意这里传入的第二个参数为null:

      遍历完成后,返回到上一层调用栈,此时,返回的producibleTypes如图,包含了我们定义的两个媒体类型。

      现在我们得到了acceptableTypes和producibleType。下面就对这两个集合的元素进行遍历,逐个比较,来确定要使用的mediaType(媒体类型)。最终我们得到的类型是对应application/anything的媒体类型。这里没有问题。

    • 得到mediaType之后,一路向下走,直到下图中的循环体(关键点2),this.messageConverters再次登场:

      这里做的事情并不复杂,就是调用this.messageConverters里面的每一个converter的canWrite()方法,来判断是否是我们要的converter。而break label 183的存在,则告诉我们:循环在找到第一个符合要求的converter之后就终止了。
      我们之前遇到的bug是:本应该是anything的converter来处理返回值,但响应的却是something的converter的处理效果,这说明something的converter被程序判定为正确的converter了。所以我们一路放行到对应something的converter.canWrite()方法,看看里面执行的过程:

    • 进入canWrite()方法,实际上就是我们自定义的SomethingMessageConverter的canWrite()方法。那么这里的问题就一目了然了:在canWrite()中,只对一个参数(即clazz)进行了判断(如下图),所以这里返回true,通过了验证。那么如上面所说,break语句执行,我们定义的AnythingMessageConverter就根本没机会被遍历到。


      值得一提的是:messageConverters被遍历了两次,这两次遍历中,有些第一轮就被淘汰的元素其实没必要再进行第二次遍历,这应该是一个性能上的优化点。

四 解决问题
  • 上面已经分析出来问题所在:canWrite()只对clazz参数进行了判断,那么解决办法自然就是:加上对mediaType的判断,如图中代码所示。接下来重新测试:

    测试结果竟然为:406,客户端无法接收相应数据。笔者当时又debug了一会儿,这里省略过程,直接描述原因和解决办法:

    • 我们回到上文关键点1的地方,也就是第一次对messageConverters进行遍历的地方。这里向canWrite()传入的第二个参数是null,这导致了自定义的canWrite()均不能判断通过。这意味着,程序找到的producibleMediaTypes中将会缺少我们自定义的MessageConverter,那么在后续的“内容协商”(即将acceptableTypes和producibleMediaTypes进行逐一对照的过程)中,就会找不到对应的converter,所以最终导致找不到页面。

    • 那么此时,我们需要做的事情就很明显了:将canWrite的判断进行修改,来规避上面提到的,传入参数为null时造成的MessageConverter类型遗漏。修改后的代码如图:

  • 再次运行并测试,成功:


The End,感谢指正。


转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/356259.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号