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<Throwable> 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/jsonapplication/*+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<Throwable> 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接受时,调用链会进入RequestMappingHandlerAdapterinvokeHandlerMethod方法,构造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<HandlerMethodReturnValueHandler> 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 <T> 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<String, Integer> getMap() {
    return new HashMap<>();
}

valueType 为java.util.HashMap;而 targetType 表示java.util.Map<java.lang.String, java.lang.Integer>

(2)InputStreamResource和Resource资源类型的特殊处理(Ignore);

(3)协商媒体类型,确定媒体类型

java 复制代码
HttpServletRequest request = inputMessage.getServletRequest();
// 获取HTTP请求头中接收的媒体类型,代表客户端要求的MIME类型[标注1]
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
// 从所有的消息转换器中取媒体类型交集,代表服务器可以处理的媒体类型
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);

if (body != null && producibleTypes.isEmpty()) {
    throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);
}
// 从服务器支持的媒体类型中筛选出客户端要求的MIME类型
List<MediaType> 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/htmlapplication/xhtml+xmlapplication/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头域,用于指示文件的名称和下载方式。取值范围有inlineattachmentinline 表示文件直接浏览器中显示文本attachment表示文件下载到本地。

(5)异常场景处理

未匹配到消息处理器的场景,抛出异常。

----以上为所有内容----

相关推荐
程序员大金37 分钟前
基于SSM+Vue+MySQL的酒店管理系统
前端·vue.js·后端·mysql·spring·tomcat·mybatis
努力的布布41 分钟前
Spring源码-从源码层面讲解声明式事务的运行流程
java·spring
程序员大金1 小时前
基于SpringBoot的旅游管理系统
java·vue.js·spring boot·后端·mysql·spring·旅游
爱上语文1 小时前
Springboot三层架构
java·开发语言·spring boot·spring·架构
你知道“铁甲小宝”吗丶3 小时前
【第33章】Spring Cloud之SkyWalking服务链路追踪
java·spring boot·spring·spring cloud·skywalking
听封3 小时前
Thymeleaf 的创建
java·spring boot·spring·maven
huapiaoy4 小时前
Spring mvc
java·spring·mvc
你知道“铁甲小宝”吗丶5 小时前
【第34章】Spring Cloud之SkyWalking分布式日志
java·spring boot·spring·spring cloud·skywalking
吃汉堡吃到饱7 小时前
【Android】浅析MVC与MVP
android·mvc