大部分人的第一反应是通过SpringMVC拦截器(Interceptor
)中的postHandler
方法处理。实际这是行不通的,因为当程序运行到该方法,是在返回数据之后,渲染页面之前,所以这时候HttpServletResponse
中的输出流已经关闭了,自然无法在对返回数据进行处理。
其实这个问题用几行代码就可以搞定,因为SpringMVC提供了非常丰富的扩展支持,无论是之前提到的MethodArgumentResolver
和HandlerMethodReturnValueHandler
,还是接下来要提到的HttpMessageConverter
。
在SpringMVC的 Controller
层经常会用到@RequestBody
和@ResponseBody
,通过这两个注解,可以在Controller
中直接使用Java对象作为请求参数和返回内容,而完成这之间转换作用的便是HttpMessageConverter
。
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> {
/**
* Indicates whether the given class can be read by this converter.
* @param clazz the class to test for readability
* @param mediaType the media type to read, can be {@code null} if not specified.
* Typically the value of a {@code Content-Type} header.
* @return {@code true} if readable; {@code false} otherwise
*/
boolean canRead(Class<?> clazz, MediaType mediaType);
/**
* Indicates whether the given class can be written by this converter.
* @param clazz the class to test for writability
* @param mediaType the media type to write, can be {@code null} if not specified.
* Typically the value of an {@code Accept} header.
* @return {@code true} if writable; {@code false} otherwise
*/
boolean canWrite(Class<?> clazz, MediaType mediaType);
/**
* Return the list of {@link MediaType} objects supported by this converter.
* @return the list of supported media types
*/
List<MediaType> getSupportedMediaTypes();
/**
* Read an object of the given type form the given input message, and returns it.
* @param clazz the type of object to return. This type must have previously been passed to the
* {@link #canRead canRead} method of this interface, which must have returned {@code true}.
* @param inputMessage the HTTP input message to read from
* @return the converted object
* @throws IOException in case of I/O errors
* @throws HttpMessageNotReadableException in case of conversion errors
*/
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
/**
* Write an given object to the given output message.
* @param t the object to write to the output message. The type of this object must have previously been
* passed to the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}.
* @param contentType the content type to use when writing. May be {@code null} to indicate that the
* default content type of the converter must be used. If not {@code null}, this media type must have
* previously been passed to the {@link #canWrite canWrite} method of this interface, which must have
* returned {@code true}.
* @param outputMessage the message to write to
* @throws IOException in case of I/O errors
* @throws HttpMessageNotWritableException in case of conversion errors
*/
void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
HttpMessageConverter
接口提供了5个方法:
canRead
:判断该转换器是否能将请求内容转换成Java对象canWrite
:判断该转换器是否可以将Java对象转换成返回内容getSupportedMediaTypes
:获得该转换器支持的MediaType
类型read
:读取请求内容并转换成Java对象write
:将Java对象转换后写入返回内容
其中read
和write
方法的参数分别有有HttpInputMessage
和HttpOutputMessage
对象,这两个对象分别代表着一次Http通讯中的请求和响应部分,可以通过getBody
方法获得对应的输入流和输出流。
当前SpringMVC中已经默认提供了相当多的转换器,如上图,其中常用的有:
名称 | 作用 | 读支持MediaType | 写支持MediaType |
---|---|---|---|
ByteArrayHttpMessageConverter | 数据与字节数组的相互转换 | */* | application/octet-stream |
StringHttpMessageConverter | 数据与String类型的相互转换 | text/* | text/plain |
FormHttpMessageConverter | 表单与MultiValueMap的相互转换 | application/x-www-form-urlencoded | application/x-www-form-urlencoded |
SourceHttpMessageConverter | 数据与javax.xml.transform.Source的相互转换 | text/xml和application/xml | text/xml和application/xml |
MarshallingHttpMessageConverter | 使用Spring的Marshaller/Unmarshaller转换XML数据 | text/xml和application/xml | text/xml和application/xml |
MappingJackson2HttpMessageConverter | 使用Jackson的ObjectMapper转换Json数据 | application/json | application/json |
MappingJackson2XmlHttpMessageConverter | 使用Jackson的XmlMapper转换XML数据 | application/xml | application/xml |
BufferedImageHttpMessageConverter | 数据与java.awt.image.BufferedImage的相互转换 | Java I/O API支持的所有类型 | Java I/O API支持的所有类型 |
SpringMVC自己的HttpMessageConverter
还是定义在RequestMappingHandlerAdapter
里,并且被MethodArgumentResolver
和HandlerMethodReturnValueHandler
解析数据的时候所用到。
public RequestMappingHandlerAdapter() {
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316
this.messageConverters = new ArrayList<HttpMessageConverter<?>>(4);
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(stringHttpMessageConverter);
this.messageConverters.add(new SourceHttpMessageConverter<Source>());
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}
这里面四个HttpMessageConverter
,重点介绍下AllEncompassingFormHttpMessageConverter
,看源码。
public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConverter {
private static final boolean jaxb2Present =
ClassUtils.isPresent("javax.xml.bind.Binder", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
private static final boolean jackson2Present =
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader()) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
private static final boolean jackson2XmlPresent =
ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
private static final boolean gsonPresent =
ClassUtils.isPresent("com.google.gson.Gson", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
public AllEncompassingFormHttpMessageConverter() {
addPartConverter(new SourceHttpMessageConverter<Source>());
if (jaxb2Present && !jackson2XmlPresent) {
addPartConverter(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
addPartConverter(new MappingJackson2HttpMessageConverter());
} else if (gsonPresent) {
addPartConverter(new GsonHttpMessageConverter());
}
if (jackson2XmlPresent) {
addPartConverter(new MappingJackson2XmlHttpMessageConverter());
}
}
}
它的主要作用是,从类加载器去查找相关的类,只有这些转换器需要的类(jar包被你引入了)存在,那么转换器才会被加载进去,比如我们在项目中引入了
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
这样就预示着MappingJackson2HttpMessageConverter
被加入到了SpringMVC中,后面在使用@RequestBody
和@ResponseBody
,RequestResponseBodyMethodProcessor
起作用的时候,它就会寻找能够匹配的Converter
,进而找到MappingJackson2HttpMessageConverter
用来转换对象。
那如果我们引入了
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>${jackson.version}</version>
</dependency>
那也就预示着MappingJackson2XmlHttpMessageConverter
被加入转换器队列中了,那么你使用@ResponseBody
,响应类型为Content-Type: application/xml;charset=UTF-8
,估计是默认请求头Accept直接为application/xml
,具体源码我就不去翻了。
当用户发送请求后,@RequestBody
注解会读取请求body中的数据,通过获取请求头Header中的Content-Type
来确认请求头的数据格式,从而来为请求数据适配合适的转换器。例如contentType:applicatin/json
,那么转换器会适配MappingJackson2HttpMessageConverter
。响应时候的时候同理,@ResponseBody
注解会通过检测请求头Header中Accept
属性来适配对应响应的转换器。
我这里因为没找到合适的例子去实现自己的HttpMessageConverter
,但想到了另外一个可以拿来用的例子。
@RequestMapping(value="simple")
public String simple(){
return "abc中国";
}
这个方法我们访问的时候,应该是SpringMVC会去找一个abc中国.jsp
的页面,由于找不到,会报404
。
但当我们稍微修改如下:
@RequestMapping(value="simple")
@ResponseBody
public String simple(){
return "abc中国";
}
由于我们使用了@ResponseBody
,当我们再次发起请求的时候,请求头Header
中Accept
为text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
根据上个小节所介绍到的,它应该会找到StringHttpMessageConverter
,并完成转换,输出的直接是返回值,然而我们却发现结果中文乱码了,这是因为该类默认支持的编码格式是ISO-8859-1
。
package org.springframework.http.converter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.util.StreamUtils;
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1");
那我们应当如何使之支持UTF-8
呢,这也是接下来我们所需要介绍的,应当如何覆盖SpringMVC自己的转换器,或者新增自定义的转换器。
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/plain;charset=UTF-8</value>
<value>text/html;charset=UTF-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
如果我们不想使用系统默认的转换器,还可以去掉系统给你定义的,然后重新定义你所需要的转换器,如下:
<mvc:annotation-driven>
<mvc:message-converters register-defaults="false">
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/plain;charset=UTF-8</value>
<value>text/html;charset=UTF-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
我个人的意见是,如果没有特别的需要,不要重新定义自己的转换器,我们大可覆盖或者新增就行了。