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)异常场景处理** 未匹配到消息处理器的场景,抛出异常。 ----以上为所有内容----

相关推荐
qq_三哥啊2 小时前
【IDEA】设置Debug调试时调试器不进入特定类(Spring框架、Mybatis框架)
spring·intellij-idea·mybatis
别惹CC2 小时前
Spring AI 进阶之路01:三步将 AI 整合进 Spring Boot
人工智能·spring boot·spring
寒士obj2 小时前
Spring事物
java·spring
IT毕设实战小研11 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
甄超锋12 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
Java小白程序员15 小时前
Spring Framework:Java 开发的基石与 Spring 生态的起点
java·数据库·spring
甄超锋16 小时前
Java Maven更换国内源
java·开发语言·spring boot·spring·spring cloud·tomcat·maven
还是鼠鼠17 小时前
tlias智能学习辅助系统--Maven 高级-私服介绍与资源上传下载
java·spring boot·后端·spring·maven
还是大剑师兰特18 小时前
Spring面试题及详细答案 125道(1-15) -- 核心概念与基础1
spring·大剑师·spring面试题·spring教程
python_13621 小时前
web请求和响应
java·spring·github