SpringBoot 支持 application/xml 及 406 异常分析

Posted 毕小宝

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot 支持 application/xml 及 406 异常分析相关的知识,希望对你有一定的参考价值。

背景

在 Spring 框架中,@ResponseBody 注解响应的对象会利用 HttpMessageConverter 类,根据 produces 属性指定的类型,找到对应的数据转换器完成转换。

最常见也是默认的响应类型是 application/json,还有很多其他的响应数据类型,都封装在 org.springframework.http.MediaType 中了,本文将介绍 application/xml 类型的响应支持及其引发的思考。

支持 application/xml

定义一个请求方法,设置响应类型为 XML:

    @ResponseBody
    @RequestMapping(value= "/hello/json")
    public MyData hello() {
        return new MyData("13020045218","10");
    }

    @ResponseBody
    @RequestMapping(value= "/hello/xml", produces = MediaType.APPLICATION_XML_VALUE)
    public MyData hello1() {
        return new MyData("13020045218","10");
    }

关键配置

依赖引入

<dependency>
      <groupId>com.fasterxml.jackson.dataformat</groupId>
      <artifactId>jackson-dataformat-xml</artifactId>
       <version>2.9.0</version>
 </dependency>

如果不添加上述依赖,而又在 Controller 的方法上指定了 XML 类型,服务端会抛出 406 异常:

异常类型为:

org.springframework.web.HttpMediaTypeNotAcceptableException:
 Could not find acceptable representation

加入依赖后,页面返回 XML 数据:

延伸思考两个知识点

框架用久了,难免会忘记它底层的封装过程,像本文这个简单的练习,源于 SpringBoot 中重新设置 MappingJackson2HttpMessageConverter 以解决特殊转换问题,同时针对 XML 类型中的特殊转换,需要重新设置 MappingJackson2XMLHttpMessageConverter

思考两个问题:

  1. 406 是什么原因导致的?
  2. 这个引用包中是怎么让 XML 转换器自动加入到 Spring 环境中的?

第一个问题容易猜测:请求指定响应数据格式为 application/xml 后,系统在返回时会调用org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#handleReturnValue,这个里面会使用 MessageConverter,查找 XML 类型的转换器。出现 406 错误,说明没有找到该转换器。

第二个问题,SpringBoot 自动装配 Jackson 转换器是通过 JacksonHttpMessageConvertersConfiguration 类完成的,它会注入 JSON 转换器 MappingJackson2HttpMessageConverter 和 XML 的转换器 ,它的源码如下:

@org.springframework.context.annotation.Configuration
class JacksonHttpMessageConvertersConfiguration {
    JacksonHttpMessageConvertersConfiguration() { /* compiled code */ }

    @org.springframework.context.annotation.Configuration
    @org.springframework.boot.autoconfigure.condition.ConditionalOnClass({com.fasterxml.jackson.dataformat.xml.XmlMapper.class})
    @org.springframework.boot.autoconfigure.condition.ConditionalOnBean({org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.class})
    protected static class MappingJackson2XmlHttpMessageConverterConfiguration {
        protected MappingJackson2XmlHttpMessageConverterConfiguration() { /* compiled code */ }

        @org.springframework.context.annotation.Bean
        @org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
        public org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder) { /* compiled code */ }
    }

    @org.springframework.context.annotation.Configuration
    @org.springframework.boot.autoconfigure.condition.ConditionalOnClass({com.fasterxml.jackson.databind.ObjectMapper.class})
    @org.springframework.boot.autoconfigure.condition.ConditionalOnBean({com.fasterxml.jackson.databind.ObjectMapper.class})
    @org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(name = {"spring.http.converters.preferred-json-mapper"}, havingValue = "jackson", matchIfMissing = true)
    protected static class MappingJackson2HttpMessageConverterConfiguration {
        protected MappingJackson2HttpMessageConverterConfiguration() { /* compiled code */ }

        @org.springframework.context.annotation.Bean
        @org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean(value = {org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class}, ignoredType = {"org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter", "org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter"})
        public org.springframework.http.converter.json.MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(com.fasterxml.jackson.databind.ObjectMapper objectMapper) { /* compiled code */ }
    }
}

注意第一部分, XML 转换器的自动注入条件是:@org.springframework.boot.autoconfigure.condition.ConditionalOnClass({com.fasterxml.jackson.dataformat.xml.XmlMapper.class}) 这就是引入依赖自动触发注入的根源了。

这个条件注入又引发了第三个问题,不引入 jar 包,这个配置就存在编译错误,怎么解决这个编译错误的呢?

搜索到一篇《注解@ConditionalOnClass(X.class),X不存在时的探究》,原理类似。

猜测同理, spring-boot 引用了这个包,但是 optional=true ,所以依赖不会传递到我们项目中,如果不引入相关 jar 又想用 @ConditionOnClass 的话,可以使用 name 属性:

@ConditionalOnClass(name = "com.fasterxml.jackson.dataformat.xml.XmlMapper")

启示录

以上就是 Jackson 转换器引发的思考:

  1. 如何更改 MappingJackson2HttpMessageConverter 默认配置?
  2. @ConditionalOnClass 未引入对应 jar 包时,编译错误是怎么回事?
  3. SpringBoot 是如何自动装配 Jackson 的两个转换器的?

看完本文,你明白怎么回事儿了吗?

以上是关于SpringBoot 支持 application/xml 及 406 异常分析的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot 支持 application/xml 及 406 异常分析

如何配置springboot的jsp支持

SpringBoot application.yml logback.xml,多环境配置,支持 java -jar --spring.profiles.active

SpringBoot application.yml logback.xml,多环境配置,支持 java -jar --spring.profiles.active

SpringBoot配置切换

SpringBoot -- 事件(Application Event)