SpringMVC系列-5 消息转换器

背景

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的关系图如下所示: ![在这里插入图片描述](https://file.jishuzhan.net/article/1717508287450058753/3acd2ed4647027e09d8b093e318755ef.webp)当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>) 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)异常场景处理** 未匹配到消息处理器的场景,抛出异常。 ----以上为所有内容----

相关推荐
hello_ejb32 小时前
聊聊Spring AI 1.0.0-SNAPSHOT的变更
java·人工智能·spring
程序员buddha2 小时前
【Spring Boot】Spring Boot + Thymeleaf搭建mvc项目
spring boot·后端·mvc
okok__TXF4 小时前
spring详解-循环依赖的解决
java·后端·spring
神马都会亿点点的毛毛张6 小时前
【SpringBoot教程】SpringBoot自定义注解与AOP实现切面日志
java·spring boot·后端·spring·spring aop·aspectj
Java~~11 小时前
山东大学软件学院项目实训-基于大模型的模拟面试系统-个人主页头像上传
java·vue.js·spring
佩奇的技术笔记12 小时前
Java学习手册:Spring 生态其他组件介绍
java·spring
极客智谷12 小时前
Spring AI系列——大模型驱动的自然语言SQL引擎:Java技术实现详解
java·人工智能·spring
zfj32113 小时前
spring-boot-maven-plugin 将spring打包成单个jar的工作原理
spring·maven·jar·springboot·classloader·打包
神秘的t14 小时前
Spring Web MVC————入门(1)
java·spring·mvc
YUELEI1181 天前
spring cloud 与 cloud alibaba 版本对照表
后端·spring·spring cloud