文档章节

SpringMVC4.x源码分析(六):消息转换器自动转换json、xml原理分析

祖大俊
 祖大俊
发布于 2018/06/19 21:12
字数 1717
阅读 262
收藏 0

先来一个简单的例子,首先在pom.xml内加入处理json、xml的jackson依赖包。

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.9.5</version>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.5</version>
</dependency>

写一个简单的Controllor。

@RequestMapping(path = {"/jsonOrXml"})
@ResponseBody
public User jsonOrXml() {
  return new User("张三", 10, new Date());
}

启动服务进行测试。

(Made In Firefox)

一个简单的@ResponseBody注解,页面就可以获得json数据。

接下来给User对象加上一个@XmlRootElement注解,该注解为JDK自带的JAXB规范注解。

@XmlRootElement
public class User {
    private String name;
    private Integer age;
    private Date date;
//...
}

(Made In Firefox)

一个简单的@ResponseBody+@XmlRootElement注解,页面就获得了xml数据。

接下来,我们就从源码开始分析其内部机理。

HttpMessageConverter

之前的文章已经介绍过,处理Controllor方法参数使用的是argumentResolvers,处理返回值使用的是returnValueHandlers,而argumentResolvers和returnValueHandlers会用到HttpMessageConverter消息转换器,用以处理@ResponseBody和@RequestBody所标注的方法或参数。需要注意的是,HttpMessageConverter,只有部分argumentResolvers和returnValueHandlers会用到。

消息转换器集合,我们使用messageConverters来表示,它作为集合属性,存储在RequestMappingHandlerAdapter内。

AnnotationDrivenBeanDefinitionParser.parse()源码:

RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
handlerAdapterDef.setSource(source);
handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
handlerAdapterDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
handlerAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef);
handlerAdapterDef.getPropertyValues().add("messageConverters", messageConverters);

AnnotationDrivenBeanDefinitionParser.getMessageConverters()源码:

private ManagedList<?> getMessageConverters(Element element, Object source, ParserContext parserContext) {
    // 查找<mvc:annotation-driven>标签的子元素message-converters标签
	Element convertersElement = DomUtils.getChildElementByTagName(element, "message-converters");
	ManagedList<? super Object> messageConverters = new ManagedList<Object>();
	if (convertersElement != null) {
		messageConverters.setSource(source);
		for (Element beanElement : DomUtils.getChildElementsByTagName(convertersElement, "bean", "ref")) {
			Object object = parserContext.getDelegate().parsePropertySubElement(beanElement, null);
			messageConverters.add(object);
		}
	}
    // 查找<message-converters>标签的属性register-defaults的值,默认true,以决定是否加入默认消息转换器
	if (convertersElement == null || Boolean.valueOf(convertersElement.getAttribute("register-defaults"))) {
		messageConverters.setSource(source);
		messageConverters.add(createConverterDefinition(ByteArrayHttpMessageConverter.class, source));

		RootBeanDefinition stringConverterDef = createConverterDefinition(StringHttpMessageConverter.class, source);
		stringConverterDef.getPropertyValues().add("writeAcceptCharset", false);
		messageConverters.add(stringConverterDef);

		messageConverters.add(createConverterDefinition(ResourceHttpMessageConverter.class, source));
		messageConverters.add(createConverterDefinition(SourceHttpMessageConverter.class, source));
		messageConverters.add(createConverterDefinition(AllEncompassingFormHttpMessageConverter.class, source));

		if (romePresent) {
			messageConverters.add(createConverterDefinition(AtomFeedHttpMessageConverter.class, source));
			messageConverters.add(createConverterDefinition(RssChannelHttpMessageConverter.class, source));
		}
        // Class.forName()检测是否引入了相关jar包
		if (jackson2XmlPresent) {
			Class<?> type = MappingJackson2XmlHttpMessageConverter.class;
			RootBeanDefinition jacksonConverterDef = createConverterDefinition(type, source);
			GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source);
			jacksonFactoryDef.getPropertyValues().add("createXmlMapper", true);
			jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef);
			messageConverters.add(jacksonConverterDef);
		}
		else if (jaxb2Present) {
			messageConverters.add(createConverterDefinition(Jaxb2RootElementHttpMessageConverter.class, source));
		}
        // Class.forName()检测是否引入了相关jar包
		if (jackson2Present) {
			Class<?> type = MappingJackson2HttpMessageConverter.class;
			RootBeanDefinition jacksonConverterDef = createConverterDefinition(type, source);
			GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source);
			jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef);
			messageConverters.add(jacksonConverterDef);
		}
		else if (gsonPresent) {
			messageConverters.add(createConverterDefinition(GsonHttpMessageConverter.class, source));
		}
	}
	return messageConverters;
}

通过以上源码的分析,可以得出3个结论:

1、自定义RequestMappingHandlerAdapter,配置messageConverters属性,可覆盖默认的消息转换器

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
    <list>
	<bean class="xxxConverter"/>
	<bean class="xxxConverter"/>
    </list>
</property>
</bean>

2、配置<mvc:annotation-driven>的子元素<message-converters>标签,可以增加自定义的消息转换器,也可以覆盖默认的消息转换器。

<mvc:annotation-driven>
  <mvc:message-converters>
    <bean class="xxxConvertor"/>
    <bean class="xxxConvertor"/>
  </mvc:message-converters>
</mvc:annotation-driven>

3、<mvc:message-converters register-defaults="true|false">可以控制,是否加入默认的消息转换器

补:

RequestMappingHandlerAdapter和RequestMappingHandlerMapping等组件,都是可以通过自定义bean的配置,来覆盖<mvc:annotation-driven>所引入的默认组件,当多种配置共存时,可以通过配置顺序或者通过order属性来明确指定优先级。

HttpMessageConverter的接口定义:

public interface HttpMessageConverter<T> {
    // 判断能否转换request请求数据
	boolean canRead(Class<?> clazz, MediaType mediaType);

    // 判断能否转换response响应数据
	boolean canWrite(Class<?> clazz, MediaType mediaType);

    // 消息转换器所支持的MIME类型,即Content-Type的值集
	List<MediaType> getSupportedMediaTypes();

    // 转换request请求数据
	T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException;

    // 响应response数据
	void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException;
}

参数说明:

Class<?> clazz:Controllor方法参数的Type类型。

MediaType mediaType:request请求的Content-Type值。

@ResponseBody和@RequestBody的使用

接下来,写一个简单的例子:

@RequestMapping(path = {"/jsonOrXml"})
@ResponseBody
public User jsonOrXml(@RequestBody User user) {
  return user;
}

@ResponseBody:表示可以响应json或xml数据

@RequestBody:表示可以接收json或xml数据

因此,我们需要模拟request请求,以POST方式,发送一个json或xml数据请求,使用工具Postman。

RequestResponseBodyMethodProcessor

(Made In IntelliJ IDEA)

RequestResponseBodyMethodProcessor同时实现了HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler接口:

@Override
public boolean supportsParameter(MethodParameter parameter) {
	return parameter.hasParameterAnnotation(RequestBody.class);
}

@Override
public boolean supportsReturnType(MethodParameter returnType) {
	return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
			returnType.hasMethodAnnotation(ResponseBody.class));
}

AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters()源码:

protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
		Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

	//...

	try {
		inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage);

		for (HttpMessageConverter<?> converter : this.messageConverters) {
			Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
			if (converter instanceof GenericHttpMessageConverter) {
				GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;      
                // 消息转换器判断能否转换请求数据
				if (genericConverter.canRead(targetType, contextClass, contentType)) {
					if (inputMessage.getBody() != null) {
						inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
                        // 消息转换器读取数据
						body = genericConverter.read(targetType, contextClass, inputMessage);
						body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
					}
					else {
						body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
					}
					break;
				}
			}
			//...

	return body;
}

读取json请求数据

MappingJackson2HttpMessageConverter刚好支持读取application/json和application/*+json的MIME类型

于是MappingJackson2HttpMessageConverter,就将request请求的json数据{"name":"张三","age":20},转换为User{name='张三', age=20, date=null}对象。

MappingJackson2HttpMessageConverter内部使用的,就是pom.xml中jackson包中的类完成json to object转换的。

响应json数据

响应json数据,也是通过MappingJackson2HttpMessageConverter的write()方法完成的。

AbstractJackson2HttpMessageConverter.writeInternal()

protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
		throws IOException, HttpMessageNotWritableException {

	MediaType contentType = outputMessage.getHeaders().getContentType();
	JsonEncoding encoding = getJsonEncoding(contentType);
    // outputMessage就是response对象
	JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
	try {
		writePrefix(generator, object);

		// 写user对象
		objectWriter.writeValue(generator, value);

		writeSuffix(generator, object);
		generator.flush();

	}
	//...
	}
}

可以看到JsonGenerator里引用的输出流对象就是response对象。

response所write的对象,就是User对象。

读取xml请求数据

MappingJackson2HttpMessageConverter可以接收json数据,响应json或xml数据。

以此类推,MappingJackson2XmlHttpMessageConverter则可以接收xml数据,响应json或xml数据,要使用MappingJackson2XmlHttpMessageConverter,需要在pom.xml内添加jar包依赖:

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

<dependency>
  <groupId>com.fasterxml.woodstox</groupId>
  <artifactId>woodstox-core</artifactId>
  <version>5.1.0</version>
</dependency>

当添加上jackson-dataformat-xml包依赖后,MappingJackson2XmlHttpMessageConverter就会被自动加入消息转换器列表。

使用Postman模拟发送xml数据:

响应json或xml数据

HTTP请求头加入Accept=application/xml即可响应xml数据。

HTTP请求头加入Accept=application/json即可响应json数据。

问题:添加jackson-dataformat-xml依赖包后,原来返回json数据的方法,现在变成返回xml数据

原因:

1、未加入jackson-dataformat-xml包之前,只有一个MappingJackson2HttpMessageConverter转换器。

request请求信息:

Content-Type=application/json

Accept=空,为空表示任意*/*,此时响应的selectedMediaType就取转换器所支持的MediaType类型application/json。

2、加入jackson-dataformat-xml包之后,有MappingJackson2HttpMessageConverter和MappingJackson2XmlHttpMessageConverter转换器。

request请求信息:

Content-Type=application/json

Accept=空,为空表示任意*/*,此时响应的selectedMediaType就取转换器所支持的MediaType类型application/xml和application/json,由于application/xml排序顺序在application/json的前面,因此选择了application/xml,于是原来返回json的方法,就变成了返回xml数据。

解决办法

第一种、明确指定接收响应的Accept的类型值。

第二种、不要同时使用MappingJackson2HttpMessageConverter和MappingJackson2XmlHttpMessageConverter转换器。

第三种、通过SpringMVC配置指定默认的Accept值

<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"/> 
<bean id="contentNegotiationManager" 
      class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean"> 
    <property name="defaultContentType" value="application/json" /> 
</bean> 

推荐第三种解决办法。

总结,本文至少涉及到以下知识点:

1、@ResponseBody、@RequestBody

2、HttpMessageConverter

3、HandlerMethodArgumentResolver、HandlerMethodReturnValueHandler

4、Postman模拟raw请求发送json、xml数据

5、接收json或xml数据,响应json或xml数据

原文出处:http://my.oschina.net/zudajun

© 著作权归作者所有

祖大俊
粉丝 802
博文 32
码字总数 52477
作品 0
昌平
私信 提问
Spring Boot:定制HTTP消息转换器

这篇文章主要介绍在Spring Boot中使用消息转换器。 在构建RESTful数据服务过程中,我们定义了controller、repositories,并用一些注解修饰它们,但是到现在为止我们还没执行过对象的转换——...

阿杜
2017/12/09
0
0
Java Web技术经验总结(六)

synchronized的作用和原理:link 使用经验:synchronized是一种互斥锁。在Java开发中,当某个变量需要在多个线程之间共享时,需要分析具体的场景:如果多个线程对该共享变量的读和写之间没有...

杜琪
2016/06/16
0
0
SpringMVC源码剖析(五)-消息转换器HttpMessageConverter

概述 在SpringMVC中,可以使用@RequestBody和@ResponseBody两个注解,分别完成请求报文到对象和对象到响应报文的转换,底层这种灵活的消息转换机制,就是Spring3.x中新引入的HttpMessageConv...

相见欢
2013/10/28
75.6K
20
Axios源码深度剖析 - AJAX新王者

axios源码分析 - XHR篇 文章源码托管在github上,欢迎fork指正! axios 是一个基于 Promise 的http请求库,可以用在浏览器和node.js中,目前在github上有 42K 的star数 备注: 每一小节都会从...

wanghairong-i
2018/05/28
0
0
SpringMVC源码(六)-@RequestBody和@ResponseBody

在SpringMVC的使用时,往往会用到@RequestBody和@ResponseBody两个注解,尤其是处理ajax请求必然要使用@ResponseBody注解。这两个注解对应着Controller方法的参数解析和返回值处理,开始时都...

青离
2017/10/18
2K
1

没有更多内容

加载失败,请刷新页面

加载更多

[mycat]PartitionByString分片报错

java.lang.RuntimeException: error,check your partitionScope definition.at io.mycat.route.util.PartitionUtil.<init>(PartitionUtil.java:69) PartitionUtil.java 注意:其中count,l......

Danni3
18分钟前
6
0
OSChina 周三乱弹 —— 魂淡!不是这种粪发涂墙

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @小小编辑推荐歌曲《10/10》- Rex Orange County 《10/10》- Rex Orange County 手机党少年们想听歌,请使劲儿戳(这里) @奋斗的小牛 :上午...

小小编辑
30分钟前
576
7
Arduino教程:认识Arduino控制板

@toc 1.1 课程说明 认识Arduino控制板的各个部分, 1.2 器材 名称 数量 规格 Arduino uno控制板 1 R3 1.3 UNO电路: UNO参数 名称 参数说明 工作电压: 5V 输入电压: 接上USB时无须外部供电...

acktomas
36分钟前
6
0
WeUI框架

WeUI框架 WeUI是一套小程序的UI框架,所谓UI框架就是一套界面设计方案,有了组件,我们可以用它来拼接出一个内容丰富的小程序,而有了UI框架,我们就可以让我们的小程序变得更加美观。 体验W...

达达前端小酒馆
39分钟前
3
0
Rainbond 5.1.8发布,应用网关支持多IP网络接入

2019年10月23日,Rainbond发布5.1.8版本,本次版本更新带来了应用网关对多IP的支持, 第三方组件对域名实例的支持 等新功能和修复若干BUG。 Rainbond:支撑企业应用的开发、架构、交付和运维的...

好雨云帮
41分钟前
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部