乐趣区

SpringMVC源码剖析5消息转换器HttpMessageConverter与ResponseBody注解

转自 [SpringMVC 关于 json、xml 自动转换的原理研究[附带源码分析]](https://www.cnblogs.com/fangj…

本系列文章首发于我的个人博客:https://h2pl.github.io/

欢迎阅览我的 CSDN 专栏:Spring 源码解析 https://blog.csdn.net/column/…

部分代码会放在我的的 Github:https://github.com/h2pl/
<!– more –>

目录

  • 前言
  • 现象
  • 源码分析
  • 实例讲解
  • 关于配置
  • 总结
  • 参考资料

前言

SpringMVC 是目前主流的 Web MVC 框架之一。

如果有同学对它不熟悉,那么请参考它的入门 blog:http://www.cnblogs.com/fangjian0423/p/springMVC-introduction.html

现象

本文使用的 demo 基于 maven,是根据入门 blog 的例子继续写下去的。

我们先来看一看对应的现象。我们这里的配置文件 *-dispatcher.xml 中的关键配置如下(其他常规的配置文件不在讲解,可参考本文一开始提到的入门 blog):

(视图配置省略)

<mvc:resources location="/static/" mapping="/static/**"/>
<mvc:annotation-driven/>
<context:component-scan base-package="org.format.demo.controller"/>

pom 中需要有以下依赖(Spring 依赖及其他依赖不显示):

<dependency>
  <groupId>org.codehaus.jackson</groupId>
  jackson-core-asl
  <version>1.9.13</version>
</dependency>
<dependency>
  <groupId>org.codehaus.jackson</groupId>
  jackson-mapper-asl
  <version>1.9.13</version>
</dependency>

这个依赖是 json 序列化的依赖。

ok。我们在 Controller 中添加一个 method:

<pre>@RequestMapping(“/xmlOrJson”)
@ResponseBody public Map<String, Object> xmlOrJson() {

Map<String, Object> map = new HashMap<String, Object>();
map.put("list", employeeService.list()); return map;

}</pre>

直接访问地址:

我们看到,短短几行配置。使用 @ResponseBody 注解之后,Controller 返回的对象 自动被转换成对应的 json 数据,在这里不得不感叹 SpringMVC 的强大。

我们好像也没看到具体的配置,唯一看到的就是 *-dispatcher.xml 中的一句配置:<mvc:annotation-driven/>。其实就是这个配置,导致了 java 对象自动转换成 json 对象的现象。

那么 spring 到底是如何实现 java 对象到 json 对象的自动转换的呢?为什么转换成了 json 数据,如果想转换成 xml 数据,那该怎么办?

源码分析

本文使用的 spring 版本是 4.0.2。

在讲解 <mvc:annotation-driven/> 这个配置之前,我们先了解下 Spring 的消息转换机制。@ResponseBody 这个注解就是使用消息转换机制,最终通过 json 的转换器转换成 json 数据的。

HttpMessageConverter 接口就是 Spring 提供的 http 消息转换接口。有关这方面的知识大家可以参考 ” 参考资料 ” 中的第二条链接,里面讲的很清楚。

下面开始分析 <mvc:annotation-driven/> 这句配置:

这句代码在 spring 中的解析类是:

在 AnnotationDrivenBeanDefinitionParser 源码的 152 行 parse 方法中:

分别实例化了 RequestMappingHandlerMapping,ConfigurableWebBindingInitializer,RequestMappingHandlerAdapter 等诸多类。

其中 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter 这两个类比较重要。

RequestMappingHandlerMapping 处理请求映射的,处理 @RequestMapping 跟请求地址之间的关系。

RequestMappingHandlerAdapter 是请求处理的适配器,也就是请求之后处理具体逻辑的执行,关系到哪个类的哪个方法以及转换器等工作,这个类是我们讲的重点,其中它的属性 messageConverters 是本文要讲的重点。

私有方法:getMessageConverters

从代码中我们可以,RequestMappingHandlerAdapter 设置 messageConverters 的逻辑:

1. 如果 <mvc:annotation-driven> 节点有子节点 message-converters,那么它的转换器属性 messageConverters 也由这些子节点组成。

message-converters 的子节点配置如下:

<mvc:annotation-driven>
  <mvc:message-converters>
    <bean class="org.example.MyHttpMessageConverter"/>
    <bean class="org.example.MyOtherHttpMessageConverter"/>
  </mvc:message-converters>
</mvc:annotation-driven>

2.message-converters 子节点不存在或它的属性 register-defaults 为 true 的话,加入其他的转换器:ByteArrayHttpMessageConverter、StringHttpMessageConverter、ResourceHttpMessageConverter 等。

我们看到这么一段:

这些 boolean 属性是哪里来的呢,它们是 AnnotationDrivenBeanDefinitionParser 的静态变量。

 其中 ClassUtils 中的 isPresent 方法如下:

看到这里,读者应该明白了为什么本文一开始在 pom 文件中需要加入对应的 jackson 依赖,为了让 json 转换器 jackson 成为默认转换器之一。

<mvc:annotation-driven> 的作用读者也明白了。

下面我们看如何通过消息转换器将 java 对象进行转换的。

RequestMappingHandlerAdapter 在进行 handle 的时候,会委托给 HandlerMethod(具体由子类 ServletInvocableHandlerMethod 处理)的 invokeAndHandle 方法进行处理,这个方法又转接给 HandlerMethodReturnValueHandlerComposite 处理。

HandlerMethodReturnValueHandlerComposite 维护了一个 HandlerMethodReturnValueHandler 列表。HandlerMethodReturnValueHandler 是一个对返回值进行处理的策略接口,这个接口非常重要。关于这个接口的细节,请参考楼主的另外一篇博客:http://www.cnblogs.com/fangjian0423/p/springMVC-request-param-analysis.html。然后找到对应的 HandlerMethodReturnValueHandler 对结果值进行处理。

最终找到 RequestResponseBodyMethodProcessor 这个 Handler(由于使用了 @ResponseBody 注解)。

RequestResponseBodyMethodProcessor 的 supportsReturnType 方法:

然后使用 handleReturnValue 方法进行处理:

我们看到,这里使用了转换器。

具体的转换方法:

至于为何是请求头部的 Accept 数据,读者可以进去 debug 这个 getAcceptableMediaTypes 方法看看。我就不罗嗦了~~~

 ok。至此,我们走遍了所有的流程。

现在,回过头来看。为什么一开始的 demo 输出了 json 数据?

我们来分析吧。

由于我们只配置了 <mvc:annotation-driven>,因此使用 spring 默认的那些转换器。

很明显,我们看到了 2 个 xml 和 1 个 json 转换器。要看能不能转换,得看 HttpMessageConverter 接口的 public boolean canWrite(Class<?> clazz, MediaType mediaType)方法是否返回 true 来决定的。

我们先分析 SourceHttpMessageConverter:

它的 canWrite 方法被父类 AbstractHttpMessageConverter 重写了。

发现 SUPPORTED_CLASSES 中没有 Map 类(本文 demo 返回的是 Map 类),因此不支持。

下面看 Jaxb2RootElementHttpMessageConverter:

这个类直接重写了 canWrite 方法。

需要有 XmlRootElement 注解。很明显,Map 类当然没有。

最终 MappingJackson2HttpMessageConverter 匹配,进行 json 转换。(为何匹配,请读者自行查看源码)

实例讲解

 我们分析了转换器的转换过程之后,下面就通过实例来验证我们的结论吧。

首先,我们先把 xml 转换器实现。

之前已经分析,默认的转换器中是支持 xml 的。下面我们加上注解试试吧。

由于 Map 是 jdk 源码中的部分,因此我们用 Employee 来做 demo。

因此,Controller 加上一个方法:

<pre>@RequestMapping(“/xmlOrJsonSimple”)
@ResponseBody public Employee xmlOrJsonSimple() { return employeeService.getById(1);
}</pre>

实体中加上 @XmlRootElement 注解

结果如下:

我们发现,解析成了 xml。

这里为什么解析成 xml,而不解析成 json 呢?

之前分析过,消息转换器是根据 class 和 mediaType 决定的。

我们使用 firebug 看到:

我们发现 Accept 有 xml,没有 json。因此解析成 xml 了。

我们再来验证,同一地址,HTTP 头部不同 Accept。看是否正确。

<pre>$.ajax({

url: "${request.contextPath}/employee/xmlOrJsonSimple",
success: function(res) {console.log(res);
},
headers: {"Accept": "application/xml"}

});</pre>

<pre>$.ajax({

url: "${request.contextPath}/employee/xmlOrJsonSimple",
success: function(res) {console.log(res);
},
headers: {"Accept": "application/json"}

});</pre>

验证成功。

关于配置

如果不想使用 <mvc:annotation-driven/> 中默认的 RequestMappingHandlerAdapter 的话,我们可以在重新定义这个 bean,spring 会覆盖掉默认的 RequestMappingHandlerAdapter。

为何会覆盖,请参考楼主的另外一篇博客:http://www.cnblogs.com/fangjian0423/p/spring-Ordered-interface.html

<pre> `<bean class=”org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter”>
<property name=”messageConverters”>

<list>
  <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>
  <bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
  <bean class="org.springframework.http.converter.ResourceHttpMessageConverter"/>
</list>

</property>
</bean>` </pre>

或者如果只想换 messageConverters 的话。

<mvc:annotation-driven>
  <mvc:message-converters>
    <bean class="org.example.MyHttpMessageConverter"/>
    <bean class="org.example.MyOtherHttpMessageConverter"/>
  </mvc:message-converters>
</mvc:annotation-driven>

如果还想用其他 converters 的话。

以上是 spring-mvc jar 包中的 converters。

这里我们使用转换 xml 的 MarshallingHttpMessageConverter。

这个 converter 里面使用了 marshaller 进行转换

我们这里使用 XStreamMarshaller。

json 没有转换器,返回 406.

至于 xml 格式的问题,大家自行解决吧。这里用的是 XStream~。

使用这种方式,pom 别忘记了加入 xstream 的依赖:

<dependency>
  <groupId>com.thoughtworks.xstream</groupId>
  xstream
  <version>1.4.7</version>
</dependency>

总结

 写了这么多,可能读者觉得有点罗嗦。毕竟这也是自己的一些心得,希望都能说出来与读者共享。

刚接触 SpringMVC 的时候,发现这种自动转换机制很牛逼,但是一直没有研究它的原理,目前,算是了了一个小小心愿吧,SpringMVC 还有很多内容,以后自己研究其他内容的时候还会与大家一起共享的。

文章难免会出现一些错误,希望读者们能指明出来。

参考资料

http://my.oschina.net/HeliosFly/blog/205343

http://my.oschina.net/lichhao/blog/172562

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html

详解 RequestBody 和 @ResponseBody 注解

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

Http 请求的抽象 还是回到请求 - 响应,也就是解析请求体,然后返回响应报文这个最基本的 Http 请求过程中来。我们知道,在 servlet 标准中,可以用 javax.servlet.ServletRequest 接口中的以下方法:

public ServletInputStream getInputStream() throws IOException; 

来得到一个 ServletInputStream。这个 ServletInputStream 中,可以读取到一个原始请求报文的所有内容。同样的,在 javax.servlet.ServletResponse 接口中,可以用以下方法:

public ServletOutputStream getOutputStream() throws IOException;

来得到一个 ServletOutputStream,这个 ServletOutputSteam,继承自 java 中的 OutputStream,可以让你输出 Http 的响应报文内容。

让我们尝试着像 SpringMVC 的设计者一样来思考一下。我们知道,Http 请求和响应报文本质上都是一串字符串,当请求报文来到 java 世界,它会被封装成为一个 ServletInputStream 的输入流,供我们读取报文。响应报文则是通过一个 ServletOutputStream 的输出流,来输出响应报文。

我们从流中,只能读取到原始的字符串报文,同样,我们往输出流中,也只能写原始的字符。而在 java 世界中,处理业务逻辑,都是以一个个有业务意义的 对象 为处理维度的,那么在报文到达 SpringMVC 和从 SpringMVC 出去,都存在一个字符串到 java 对象的阻抗问题。这一过程,不可能由开发者手工转换。我们知道,在 Struts2 中,采用了 OGNL 来应对这个问题,而在 SpringMVC 中,它是 HttpMessageConverter 机制。我们先来看两个接口。

HttpInputMessage 这个类是 SpringMVC 内部对一次 Http 请求报文的抽象,在 HttpMessageConverter 的 read()方法中,有一个 HttpInputMessage 的形参,它正是 SpringMVC 的消息转换器所作用的受体“请求消息”的内部抽象,消息转换器从“请求消息”中按照规则提取消息,转换为方法形参中声明的对象。

package org.springframework.http;

import java.io.IOException;
import java.io.InputStream;

public interface HttpInputMessage extends HttpMessage {InputStream getBody() throws IOException;

}

HttpOutputMessage 这个类是 SpringMVC 内部对一次 Http 响应报文的抽象,在 HttpMessageConverter 的 write()方法中,有一个 HttpOutputMessage 的形参,它正是 SpringMVC 的消息转换器所作用的受体“响应消息”的内部抽象,消息转换器将“响应消息”按照一定的规则写到响应报文中。

package org.springframework.http;

import java.io.IOException;
import java.io.OutputStream;

public interface HttpOutputMessage extends HttpMessage {OutputStream getBody() throws IOException;

}

HttpMessageConverter 对消息转换器最高层次的接口抽象,描述了一个消息转换器的一般特征,我们可以从这个接口中定义的方法,来领悟 Spring3.x 的设计者对这一机制的思考过程。

package org.springframework.http.converter;

import java.io.IOException;
import java.util.List;

import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;

public interface HttpMessageConverter<T> {boolean canRead(Class<?> clazz, MediaType mediaType);

    boolean canWrite(Class<?> clazz, MediaType mediaType);

    List<MediaType> getSupportedMediaTypes();

    T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException;

    void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException;

}

HttpMessageConverter 接口的定义出现了成对的 canRead(),read()和 canWrite(),write()方法,MediaType 是对请求的 Media Type 属性的封装。举个例子,当我们声明了下面这个处理方法。

@RequestMapping(value="/string", method=RequestMethod.POST)
public @ResponseBody String readString(@RequestBody String string) {return "Read string'" + string + "'";}

在 SpringMVC 进入 readString 方法前,会根据 @RequestBody 注解选择适当的 HttpMessageConverter 实现类来将请求参数解析到 string 变量中,具体来说是使用了 StringHttpMessageConverter 类,它的 canRead()方法返回 true,然后它的 read()方法会从请求中读出请求参数,绑定到 readString()方法的 string 变量中。

当 SpringMVC 执行 readString 方法后,由于返回值标识了 @ResponseBody,SpringMVC 将使用 StringHttpMessageConverter 的 write()方法,将结果作为 String 值写入响应报文,当然,此时 canWrite()方法返回 true。

我们可以用下面的图,简单描述一下这个过程。

RequestResponseBodyMethodProcessor 将上述过程集中描述的一个类是 org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor,这个类同时实现了 HandlerMethodArgumentResolver 和 HandlerMethodReturnValueHandler 两个接口。前者是将请求报文绑定到处理方法形参的策略接口,后者则是对处理方法返回值进行处理的策略接口。两个接口的源码如下:

package org.springframework.web.method.support;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;

public interface HandlerMethodArgumentResolver {boolean supportsParameter(MethodParameter parameter);

    Object resolveArgument(MethodParameter parameter,
                           ModelAndViewContainer mavContainer,
                           NativeWebRequest webRequest,
                           WebDataBinderFactory binderFactory) throws Exception;

}

package org.springframework.web.method.support;

import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;

public interface HandlerMethodReturnValueHandler {boolean supportsReturnType(MethodParameter returnType);

    void handleReturnValue(Object returnValue,
                           MethodParameter returnType,
                           ModelAndViewContainer mavContainer,
                           NativeWebRequest webRequest) throws Exception;

}

RequestResponseBodyMethodProcessor 这个类,同时充当了方法参数解析和返回值处理两种角色。我们从它的源码中,可以找到上面两个接口的方法实现。

对 HandlerMethodArgumentResolver 接口的实现:

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

public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {Object argument = readWithMessageConverters(webRequest, parameter, parameter.getGenericParameterType());

    String name = Conventions.getVariableNameForParameter(parameter);
    WebDataBinder binder = binderFactory.createBinder(webRequest, argument, name);

    if (argument != null) {validate(binder, parameter);
    }

    mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());

    return argument;
}

对 HandlerMethodReturnValueHandler 接口的实现

public boolean supportsReturnType(MethodParameter returnType) {return returnType.getMethodAnnotation(ResponseBody.class) != null;
}

    public void handleReturnValue(Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
        throws IOException, HttpMediaTypeNotAcceptableException {mavContainer.setRequestHandled(true);
    if (returnValue != null) {writeWithMessageConverters(returnValue, returnType, webRequest);
    }
}

看完上面的代码,整个 HttpMessageConverter 消息转换的脉络已经非常清晰。因为两个接口的实现,分别是以是否有 @RequestBody 和 @ResponseBody 为条件,然后分别调用 HttpMessageConverter 来进行消息的读写。

如果你想问,怎么样跟踪到 RequestResponseBodyMethodProcessor 中,请你按照前面几篇博文的思路,然后到这里 spring-mvc-showcase 下载源码回来,对其中 HttpMessageConverter 相关的例子进行 debug,只要你肯下功夫,相信你一定会有属于自己的收获的。

思考 张小龙在谈微信的本质时候说:“微信只是个平台,消息在其中流转”。在我们对 SpringMVC 源码分析的过程中,我们可以从 HttpMessageConverter 机制中领悟到类似的道理。在 SpringMVC 的设计者眼中,一次请求报文和一次响应报文,分别被抽象为一个请求消息 HttpInputMessage 和一个响应消息 HttpOutputMessage。

处理请求时,由合适的消息转换器将请求报文绑定为方法中的形参对象,在这里,同一个对象就有可能出现多种不同的消息形式,比如 json 和 xml。同样,当响应请求时,方法的返回值也同样可能被返回为不同的消息形式,比如 json 和 xml。

在 SpringMVC 中,针对不同的消息形式,我们有不同的 HttpMessageConverter 实现类来处理各种消息形式。但是,只要这些消息所蕴含的“有效信息”是一致的,那么各种不同的消息转换器,都会生成同样的转换结果。至于各种消息间解析细节的不同,就被屏蔽在不同的 HttpMessageConverter 实现类中了。

退出移动版