背景
SpringMVC系列的第五篇介绍消息转换器,本文讨论的消息转换指代调用Controller接口后,对结果进行转换处理的过程。
内容包括介绍自定义消息转换器、SpringMVC常见的消息转换器、Spring消息转换器工作原理等三部分。
本文以 SpringMVC系列-2 HTTP请求调用链 和 SpringMVC系列-4 参数解析器 为基础,对相同内容不再重述。
1.自定义消息转换器
自定义消息转换器,需要实现HttpMessageConverter接口,该接口定义如下:
java
复制代码
public interface HttpMessageConverter<T> {
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
List<MediaType> getSupportedMediaTypes();
// ⚠️:read相关逻辑不是本文关注的部分
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
}
有三个比较重要的方法:
(1) getSupportedMediaTypes
方法返回该解析器支持的MIME媒体类型;
(2) canWrite
方法判断该解析器能否将目标类型的对象转化为指定的MIME媒体类型;
(3) write
方法将目标对象转化为mediaType的二进制流并写入到outputMessage流对象中。
自定义消息转换器:
java
复制代码
public class UserInfoHttpMessageConverter implements HttpMessageConverter<UserInfo> {
@Override
public boolean canWrite(Class clazz, MediaType mediaType) {
return clazz == UserInfo.class;
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.singletonList(MediaType.APPLICATION_JSON);
}
@Override
public void write(UserInfo userInfo, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
String result = userInfo.getId() + "_" + userInfo.getName() + "_" + LocalDateTime.now();
outputMessage.getBody().write(result.getBytes(StandardCharsets.UTF_8));
}
//...read Ignore
}
该自定义转换器表示可以将UserInfo类型的消息以"application/json"
媒体格式写出。
将自定义的消息转换器注册到SpringMVC:
java
复制代码
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(0, new UserInfoHttpMessageConverter());
}
}
注意 :这里通过SpringMVC的配置类WebMvcConfigurer进行注册,注册原理在本文第三章中说明。
用例涉及的Controller接口和基础类:
java
复制代码
@RestController
@RequestMapping("/api/user")
public class UserInfoController {
@GetMapping("/query")
public UserInfo query() {
return new UserInfo().setName("test_sy").setId(28);
}
}
@Data
@Accessors(chain = true)
public class UserInfo {
private Integer id;
private String name;
}
使用postman调用结果如下所示:
2.消息转换器
SpringBoot版本为2.3.2.RELEASE
2.1 框架内置的消息解析器
框架内置的消息解析器支持的MIME类型分布如下所示:
ByteArrayHttpMessageConverter :用于处理字节数组(byte array)的转换。
python
复制代码
ByteArrayHttpMessageConverter
application/octet-stream
*/*
StringHttpMessageConverter :用于处理字符串的转换。
python
复制代码
StringHttpMessageConverter
text/plain
*/*
StringHttpMessageConverter
text/plain
*/*
ResourceHttpMessageConverter :用于处理Spring Resource的实现类的转换。Spring Resource是一个抽象类,它封装了对各种资源(如文件、数据库连接等)的操作。
python
复制代码
ResourceHttpMessageConverter
*/*
ResourceRegionHttpMessageConverter :这个类是ResourceHttpMessageConverter的子类,它用于处理Resource的某个特定区域(如文件的某个部分)。
python
复制代码
ResourceRegionHttpMessageConverter
*/*
SourceHttpMessageConverter :用于处理javax.xml.transform.Source的转换。javax.xml.transform.Source是用于XML转换的接口。
python
复制代码
SourceHttpMessageConverter
application/xml
text/xml
application/*+xml
AllEncompassingFormHttpMessageConverter :用于处理表单提交请求,能解析复杂的form表单,包括文件上传等。
python
复制代码
AllEncompassingFormHttpMessageConverter
application/x-www-form-urlencoded
multipart/form-data
multipart/mixed
MappingJackson2HttpMessageConverter :用于处理JSON序列化和反序列化。
python
复制代码
MappingJackson2HttpMessageConverter
application/json
application/*+json
MappingJackson2HttpMessageConverter
application/json
application/*+json
Jaxb2RootElementHttpMessageConverter :这个类使用JAXB(Java Architecture for XML Binding)进行XML序列化和反序列化。
python
复制代码
Jaxb2RootElementHttpMessageConverter
application/xml
text/xml
application/*+xml
上述内置转换器中包括2个StringHttpMessageConverter和2个MappingJackson2HttpMessageConverter。转换器的顺序决定了其优先级,因此第二个StringHttpMessageConverter和MappingJackson2HttpMessageConverter处于失效状态:
1\] `HttpMessageConvertersAutoConfiguration`自动装配类引入的StringHttpMessageConverter替代了默认的StringHttpMessageConverter(SpringMVC框架自带),区别是前者默认字符集为**UTF_8** ,后者为**ISO_8859_1**。
\[2\] `JacksonHttpMessageConvertersConfiguration`自动装配类引入的MappingJackson2HttpMessageConverter替代了默认的MappingJackson2HttpMessageConverter。区别是使用其内部实现序列化和反序列化的`ObjectMapper`对象来自全局Bean对象(来自`JacksonAutoConfiguration`自动装配类引入的ObjectMapper)。因此在配置文件中对**spring.jackson**属性的配置可以体现在MappingJackson2HttpMessageConverter转换器上。
### 2.2 MappingJackson2HttpMessageConverter转换器
**(1) 匹配方法**
由于MappingJackson2HttpMessageConverter是GenericHttpMessageConverter接口的实现类,匹配时根据`canWrite(Type, Class>, MediaType)`方法进行:
```java
@Override
public boolean canWrite(@Nullable Type type, Class> clazz, @Nullable MediaType mediaType) {
return canWrite(clazz, mediaType);
}
```
上述方法实现时吞掉了Type类型的参数, 调用重载的`canWrite(Class>, MediaType)`方法:
```java
@Override
public boolean canWrite(Class> clazz, @Nullable MediaType mediaType) {
if (!canWrite(mediaType)) {
return false;
}
if (mediaType != null && mediaType.getCharset() != null) {
Charset charset = mediaType.getCharset();
if (!ENCODINGS.containsKey(charset.name())) {
return false;
}
}
AtomicReference causeRef = new AtomicReference<>();
if (this.objectMapper.canSerialize(clazz, causeRef)) {
return true;
}
logWarningIfNecessary(clazz, causeRef.get());
return false;
}
```
该方法可以分为三个部分:
\[1\] 调用`canWrite(MediaType)`判断媒体类型是否支持:
```java
protected boolean canWrite(@Nullable MediaType mediaType) {
if (mediaType == null || MediaType.ALL.equalsTypeAndSubtype(mediaType)) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.isCompatibleWith(mediaType)) {
return true;
}
}
return false;
}
```
如果mediaType为空或者`*/*`或者与消息解析器支持的类型匹配则返回true;框架预置MappingJackson2HttpMessageConverter时,支持的MediaType已确定,为`application/json`和`application/*+json`
\[2\] 判断编码类型是否符合, 支持的编码格式有`UTF-8,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,US-ASCII`
```java
if (mediaType != null && mediaType.getCharset() != null) {
Charset charset = mediaType.getCharset();
if (!ENCODINGS.containsKey(charset.name())) {
return false;
}
}
```
MediaType对象的Charset为空时,默认支持;
\[3\] 调用ObjectMapper的`canSerialize`方法判断是否可被序列化;
```java
AtomicReference causeRef = new AtomicReference<>();
if (this.objectMapper.canSerialize(clazz, causeRef)) {
return true;
}
```
**(2) 写方法**
```java
@Override
public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
// 添加Content-type: application/json
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}));
} else {
writeInternal(t, type, outputMessage);
outputMessage.getBody().flush();
}
}
```
`write`方法包含两个逻辑步骤:添加默认头域和写操作,写操作的实际执行方法在`writeInternal`中:
```java
@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
MediaType contentType = outputMessage.getHeaders().getContentType();
// 默认为UTF_8类型
JsonEncoding encoding = getJsonEncoding(contentType);
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
writePrefix(generator, object);
Object value = object;
Class> serializationView = null;
FilterProvider filters = null;
JavaType javaType = null;
if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = getJavaType(type, null);
}
ObjectWriter objectWriter = (serializationView != null ?
this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
if (filters != null) {
objectWriter = objectWriter.with(filters);
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.forType(javaType);
}
SerializationConfig config = objectWriter.getConfig();
if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
objectWriter = objectWriter.with(this.ssePrettyPrinter);
}
objectWriter.writeValue(generator, value);
writeSuffix(generator, object);
generator.flush();
}
```
上述方法可以分为三个步骤:添加前缀(如果有,内置的对象无前缀)、写内容、添加后缀(如果有,内置的对象无前缀),操作完全基于objectMapper对象;关于ObjectMappr的API用法不是本文的重点,不进行赘述。
## 3.工作原理
框架内置的消息转换器为处理HTTP请求和响应提供了强大的支持,基本可以满足项目的需要。这些转换器在容器启动时进行实例化和设置,后被保存在RequestMappingHandlerAdapter对象的messageConverters属性中。
当HTTP请求到达后,RequestMappingHandlerAdapter会构造一个ServletInvocableHandlerMethod对象,
且该对象拥有来自RequestMappingHandlerAdapter的消息转换器。
ServletInvocableHandlerMethod与HttpMessageConveter的关系图如下所示:
当HTTP请求被DispatcherServlet接受时,调用链会进入`RequestMappingHandlerAdapter`的`invokeHandlerMethod`方法,构造`ServletInvocableHandlerMethod`对象并调用`invokeAndHandle`方法:
```java
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 反射调用Controller接口获取返回结果
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//...
//将ModelAndViewContainer对象设置为请求未处理状态
mavContainer.setRequestHandled(false);
//处理结果
this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
```
**说明:**
从Controller接口获取返回结果后,将结果处理工作委托给了`returnValueHandlers`属性,该属性是`HandlerMethodReturnValueHandlerComposite`组合类型,内部维持了一个`List returnValueHandlers`列表;因此`handleReturnValue`实际会根据匹配关系分派到指定的HandlerMethodReturnValueHandler中。
框架内置的HandlerMethodReturnValueHandler和匹配关系如下:
> ModelAndView及其子类-\>**ModelAndViewMethodReturnValueHandler**
>
> Model及其子类-\>**ModelMethodProcessor**
>
> View及其子类-\>**ViewMethodReturnValueHandler**
>
> ResponseEntity及其子类或(ResponseEntity包裹)-\>**ResponseBodyEmitterReturnValueHandler**
>
> StreamingResponseBody及其子类或(ResponseEntity包裹)-\>**StreamingResponseBodyReturnValueHandler**
>
> HttpEntity,ResponseEntity-\>**HttpEntityMethodProcessor**
>
> HttpHeaders及其子类-\>**HttpHeadersReturnValueHandler**
>
> Callable及其子类-\>**CallableMethodReturnValueHandler**
>
> DeferredResult、ListenableFuture、CompletionStage及其子类-\>**DeferredResultMethodReturnValueHandler**
>
> WebAsyncTask及其子类-\>**AsyncTaskMethodReturnValueHandler**
>
> ModelAttribute注解-\>**ModelAttributeMethodProcessor**
>
> 方法或类被ResponseBody注解-\>**RequestResponseBodyMethodProcessor**
>
> void,CharSequence及其子类-\>**ViewNameMethodReturnValueHandler**
>
> Map及其子类-\>**MapMethodProcessor**
本文重点关注RequestResponseBodyMethodProcessor, 该结果处理器的匹配规则如下:
```dart
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}
```
即方法或者类被@ResponseBody注解的Controller接口使用RequestResponseBodyMethodProcessor。
当请求进入RequestResponseBodyMethodProcessor的`handleReturnValue`方法后:
```java
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
// 将该HTTP请求标记为已处理
mavContainer.setRequestHandled(true);
// 从webRequest获取HttpServletRequest的代理类
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
// 从webRequest获取HttpServletResponse的代理类
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
```
核心逻辑在`writeWithMessageConverters`方法:
```java
protected void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
// ...
}
```
该方法较长,主要步骤如下:
**(1)获取返回对象类型,并使用Object对象接收返回对象**
```java
Object body;
Class> valueType;
Type targetType;
if (value instanceof CharSequence) {
// 字符类型,则直接进行转换
body = value.toString();
valueType = String.class;
targetType = String.class;
} else {
body = value;
valueType = getReturnValueType(body, returnType);
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}
```
注意:valueType为对象实际类型,不包括泛型信息;targetType包含泛型信息。
如:
```java
public Map getMap() {
return new HashMap<>();
}
```
**valueType** 为j`ava.util.HashMap`;而 **targetType** 表示`java.util.Map`
**(2)InputStreamResource和Resource资源类型的特殊处理(Ignore);**
**(3)协商媒体类型,确定媒体类型**
```java
HttpServletRequest request = inputMessage.getServletRequest();
// 获取HTTP请求头中接收的媒体类型,代表客户端要求的MIME类型[标注1]
List acceptableTypes = getAcceptableMediaTypes(request);
// 从所有的消息转换器中取媒体类型交集,代表服务器可以处理的媒体类型
List producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);
}
// 从服务器支持的媒体类型中筛选出客户端要求的MIME类型
List mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (mediaTypesToUse.isEmpty()) {
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
if (logger.isDebugEnabled()) {
logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
return;
}
// 排序,按照品质因子进行[标注2]
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
} else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
```
上述逻辑有两个地方需要补充说明一下:
\[1\] getAcceptableMediaTypes方法从HttpServletRequest对象中获取客户端允许的MIME类型,由于框架内置的媒体协商器是HeaderContentNegotiationStrategy,即从请求头中的ACCEPT字段获取MIME类型;
\[2\] Accept代表客户端允许的媒体类型,客户端可以同时支持多种类型的资源,且可通过品质因数进行排序,如下所示:
`Accept: text/html;q=0.1,application/xhtml+xml;q=0.2,application/xml;q=0.3,application/json;q=0`
**Note** : 不接受application/json类型,按照期望排序可接收`text/html`、`application/xhtml+xml`、`application/xml`;类型
即q值越大,表示期望值越高。另外,出**Accept** 外,**Accept-Charset** (字符集)、**Accept-Encoding** (压缩算法)、**Accept-Language**(国际化)在HTTP媒体协商过程也可携带品质因子.
**(4)选择消息解析器,进行消息处理**
```java
// 删除选中的MIME的品质因子(即q值)
selectedMediaType = selectedMediaType.removeQualityValue();
// 遍历HttpMessageConverter,寻找第一个匹配的消息解析器处理body对象(待返回结果)
for (HttpMessageConverter> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?(GenericHttpMessageConverter>) converter : null);
// [标注1]
if (genericConverter != null ?((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :converter.canWrite(valueType, selectedMediaType)) {
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,(Class extends HttpMessageConverter>>) converter.getClass(),inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
// [标注2]
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
} else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
return;
}
}
```
代码按照遍历+匹配+处理的思路铺开,逻辑比较清晰。有两个地方需要补充说明一下:
**\[1\]** 按照消息解析器是HttpMessageConverter还是GenericHttpMessageConverter,会使用不同的canWrite进行判断,后者多一个参数;write也有区别。
**\[2\]** `addContentDispositionHeader`用于为文件请求添加Content-Disposition头域,用于指示文件的名称和下载方式。取值范围有**inline** 和**attachment** ,**inline** 表示文件直接浏览器中显示文本**attachment**表示文件下载到本地。
**(5)异常场景处理**
未匹配到消息处理器的场景,抛出异常。
----以上为所有内容----