最新消息:

Spring MVC接口总是默认返回XML的问题排查

Java ksharpdabu 91浏览 0评论

 

 

昨天同事用了我写的WSDL对接代码后,整个Spring MVC项目接口总是返回XML格式,而不是以前的JSON格式。我也是第一次遇到这个问题,觉得很好奇,所以帮忙排查了原因。

排查过程:

其中一个接口代码如下:

 

公司的spring mvc项目都是默认返回JSON格式的,因为是前后端分离的架构。但是再集成我的WSDL代码之后,所有标记了@ResponBod注解的接口默认返回的格式变成了XML格式。 所谓的默认是在http请求时,http请求头中的accept为空的时候。如果accept为“application/json” ,则只会返回json格式。
这种问题用排除法是最好的。我先把我的WSDL相关的java代码还原,重新编译后,发现一切又恢复正常了,默认又返回JSON格式了,所以是我的WSDL代码导致的这个问题。这里说下,我的WSDL代码中,只是引入了 jackson-dataformat-xml 这个依赖包来解析XML,实现Bean对象的序列化和反序列化,其他代码没有任何特别,也不涉及Spring配置的修改,pom文件中加入的依赖:

 

我猜测是因为pom中引入jackson-dataformat-xml 包导致的这个问题。
 

分析过程

通过在SpringMVC上打断点,跟踪HTTP请求的流程,得到以下分析:
对于SpringMVC来说,当一个Controller执行完,会返回一个对象,SpringMVC最终会根据返回的对象来找到对应的HandlerMethodReturnValueHandler实现类,然后调用实现类的handleReturnValue方法来将这个对象渲染成特定的格式,如json、xml等。
对于添加了@ResponseBody的接口,其会调用
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#handleReturnValue,
然后调用org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.web.context.request.NativeWebRequest),
最后调用org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse) ,其代码如下:

 

this.mediaTypes中的最后排序:
由于我们引入了jackson-dataformat-xml包,导致mediaTypes的第一个MediaType就是application/xml;charset=UTF-8
而 mediaType.isConcrete方法返回true(因为”application/xml;charset=UTF-8″的类型是具体的(不是 “*/*” 通配符类型)),这就决定了返回的MediaType格式是xml格式。所以,getAcceptableMediaTypes和getProducibleMediaTypes方法决定了mediaTypes,这是两个最关键的方法。我们接下来继续分析下这两个方法。

getAcceptableMediaTypes方法:

功能:根据ContentNegotiationManager配置的协商策略,从Request中获取客户端接受的MediaType。
spring提供了 “org.springframework.web.accept.ContentNegotiationManagerFactoryBean” 工厂bean来生成管理(content negotiation)内容协商策略的ContentNegotiationManager 。所谓的内容协商就是后台接口怎么知道前台想要什么格式的,是json,xml还是其他的,而SpringMVC默认的contentNegotiationManager的协商策略只有三种:
  • 使用Request的URL后缀(扩展名)来实现 (eg .xml/.json) , 默认开启
  • 根据Request中的URL参数来实现(eg ?format=json)  , 默认关闭
  • 根据Request的header中的 Accept属性来决定,默认开启
我们再查看ContentNegotiationManagerFactoryBean类的官方文档:

 

Factory to create a ContentNegotiationManager and configure it with ContentNegotiationStrategy instances.

This factory offers properties that in turn result in configuring the underlying strategies. The table below shows the property names, their default settings, as well as the strategies that they help to configure:

As of 5.0 you can set the exact strategies to use via setStrategies(List).

Note: if you must use URL-based content type resolution, the use of a query parameter is simpler and preferable to the use of a path extension since the latter can cause issues with URI variables, path parameters, and URI decoding. Consider setting setFavorPathExtension(boolean) to false or otherwise set the strategies to use explicitly via setStrategies(List).

这个ContentNegotiationManagerFactoryBean类负责生成 ContentNegotiationManager对象和设置默认的内容协商策略
org.springframework.web.accept.ContentNegotiationManagerFactoryBean#afterPropertiesSet:
作用:生成ContentNegotiationManager对象,并给它配置好默认的协商策略。同时将默认的mediaTypes赋值给所有协商策略,用于内容协商时计算

 

由于项目使用的默认配置,我们使用idea的find usage功能,查找下只有org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer这个类调用了ContentNegotiationManagerFactoryBean,而且也仅仅是一个帮助设置ContentNegotiationManager的相关属性。
再接着查找谁调用了ContentNegotiationConfigurer类, 我们看到 WebMvcConfigurationSupport最终会设置ContentNegotiationConfigurer对象的mediaTypes属性,如下:
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#mvcContentNegotiationManager:

 

mvcContentNegotiationManager方法会通过ContentNegotiationManagerFactoryBean工厂bean创建ContentNegotiationConfigurer对象,并且设置自己的MediaType属性,然后返回一个ContentNegotiationManager的bean对象,而ContentNegotiationConfigurer对象的MediaTypes就是决定request到底需要返回何种格式的关键属性之一。如果ContentNegotiationConfigurer的MediaTypes列表中没有xml类型,那么所有的协商策略都会失败(除非自定义了协商策略),此时SpringMVC就会认为Http请求头的accept是 “*/*”
“getDefaultMediaTypes()”会返回默认的MediaType,我们查看该方法的具体代码:
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getDefaultMediaTypes:

 

上面方法中的几个xxxPresent的boolean类型变量决定了默认mediaType有哪几种类型,其赋值代码如下:

 

上面的代码的意思是,从WebMvcConfigurationSupport的classLoader中查找这些接口类,来确定classpath是否存在这些接口类 ,如果存在,就将对象的MediaType加到默认列表中。
这里以XML为例子:

 

我们使用idea的ctrl+shift+N快捷键,分别查看classpath是否有这两个接口:
javax.xml.bind.Binder接口时jdk自带的
接着查看”com.fasterxml.jackson.dataformat.xml.XmlMapper”接口确实存在,存在于我们pom引入的jackson-dataformat-xml依赖包中:
由上面的截图可见,javax.xml.bind.Binder接口是jdk自带的,所以jaxb2Present一定为true,所以无论jackson2XmlPresent是否为真,都会将“application/xml”加入到默认的MediaType列表中。
我们接口url是home/sms/getsmsconfiglist ,  其URL后缀并没有指定返回的MediaType,而且我们的Http请求头中accept也是空的,所以SpringMVC调用this.contentNegotiationManager.resolveMediaTypes()方法时,没法根据SpringMVC默认的协商策略知道http客户端那边想要哪些MediaType,所以 getAcceptableMediaTypes最后返回就是MediaType.ALL (即”*/*”),即认为客户端可以接收所有类型的MediaType
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#getAcceptableMediaTypes

 

getProducibleMediaTypes方法

前面分析了getAcceptableMediaTypes方法,该方法让SpringMVC认为客户端接收所有的MediaType。但是,客户端虽然接收所有类型的MediaType,不代表SpringMVC能生成所有的MediaType,这是由getProducibleMediaTypes方法来决定的:

 

给allSupportedMediaTypes赋值的过程:

 

通过不断的 find usage, 最终可以找到WebMvcConfigurationSupport类的getMessageConverters方法里获取所有的MessageConverter:
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getMessageConverters

 

我们看具体的代码:
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addDefaultHttpMessageConverters:

 

上面方法中的几个xxxPresent的boolean类型变量决定了默认mediaType有哪几种类型,其赋值代码如下:

 

上面的代码的意思是,从WebMvcConfigurationSupport的classLoader中查找这些接口类,来确定classpath是否存在这些接口类 ,如果存在,就将对象的MediaType加到默认列表中。
这里以jackson2XmlPresent为例子:

 

我们使用idea的ctrl+shift+N快捷键,分别查看classpath是否有这接口:
查看”com.fasterxml.jackson.dataformat.xml.XmlMapper”接口确实存在,存在于我们pom引入的jackson-dataformat-xml依赖包中:
由上面的截图可见,如果classpath中存在com.fasterxml.jackson.dataformat.xml.XmlMapper接口的实现类,则会将MappingJackson2XmlHttpMessageConverter对象添加到MessageConverter列表中,我们看下MappingJackson2XmlHttpMessageConverter类的代码:

 

根据javadoc的说明,这个类使用Jackson 2.x 来实现xml的读取和写入,默认支持的MediaType为application/xml, text/xml, and application/*+xml 。到此为止,这里我们就算找到问题的关键了。
如果我们pom文件中不引入jackson-dataformat-xml依赖包,则springmvc项目的classpath就找不到com.fasterxml.jackson.dataformat.xml.XmlMapper接口的实现类,则jackson2XmlPresent变量就为false,此时SpringMVC就不会创建并添加MappingJackson2XmlHttpMessageConverter 到messageConverters列表中,   也就不支持生成   application/xml, text/xml, 和application/*+xml 这三种MediaType了
此时的writeWithMessageConverters方法中的mediaTypes列表的如下:

这时”application/json;”是排在第一个,且 isConcrete()返回true,所以最后返回的就会是json格式,而不再是xml格式了。

 

从上面的分析种,我们就能猜到以前SpringMVC项目的接口总是默认返回JSON格式的原因:肯定是因为classpath中有“com.fasterxml.jackson.databind.ObjectMapper (属于jackson-databind包)”和“com.fasterxml.jackson.core.JsonGenerator (属于jackson-core包)”这两个接口的实现类。我们查看公司项目的pom文件,确实发现了引用了jackson的两个包:

 

解决办法

方法一:
从pom(classpath)去掉jackson-dataformat-xml包,用xstream替代。
方法二:
对于Spring3.2之后的修改协商策略:
XML配置
设置默认的MediaType为“application/json”:

 

或者
Java代码配置

 

参考:
  • https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.html
  • https://www.baeldung.com/spring-httpmessageconverter-rest
  • https://www.baeldung.com/spring-mvc-content-negotiation-json-xml

 

 

 

转载请注明:大步's Blog » Spring MVC接口总是默认返回XML的问题排查

发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址