SpringMVC流程分析(七): SpringMVC中消息转换的秘密

本系列文章皆在分析SpringMVC的核心组件和工作原理,让你从SpringMVC浩如烟海的代码中跳出来,以一种全局的视角来重新审视SpringMVC的工作原理.

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜

前言

相信对于使用SpringMVC的开发者而言,对于注解 @RequestBody@ResponseBody 两个注解肯定不会陌生。当在控制层使用该注解后,SpringMVC在处理返回内容时,便会分别完成请求报文到 Java 对象Java 对象到响应报文的转换。

而这背后的转换的逻辑便是依赖本文所介绍的 HttpMessageConverter 消息转换机制来实现的。

配置HttpMessageConverter

在开始分析HttpMessageConverter之前,我们先来看一段使用与HttpMessageConverter相关的配置信息。

xml 复制代码
<!--    返回值配置json-->
<mvc:annotation-driven>
    <mvc:message-converters register-defaults="true">
        <!-- 配置HttpMessageConverter相关信息--->
        <bean id="fastJsonHttpMessageConverter" 
        class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
            <property name="supportedMediaTypes">
                <list>
                     <!--协定返回内容格式信息 -->
                    <value>text/html;charset=UTF-8</value>
                    <value>application/json;charset=UTF-8</value>
                </list>
            </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

相信熟悉SpringMVC的开发者一定会非常熟悉上述代码的含义,上述xml配置文件的逻辑为SpringMVC内部指定一个HttpMeesageConverter的实现类,并协定请求的编码格式和返回内容的数据格式。

那这背后SpringMVC又为我们做了哪些操作呢?别着急,相信读了后续文章你会对HttpMeesageConverter有一个更加深刻的认识。

预备知识

在开始阅读本文之前,我们先来理解一下Servlet下体系下的几个关键概念。在处理 HTTP 请求的过程中,通常需要解析请求体内容,并将返回结果设置到响应体。如果你有过Servlet的开发经历,你一定会知道在 Servlet 标准中,对于类javax.servlet.ServletRequestjavax.servlet.ServletResponse 其内部分别会定义如下方法:

java 复制代码
public ServletInputStream getInputStream() throws IOException;

public ServletOutputStream getOutputStream() throws IOException;

通过上面两个方法可以获取到请求体和响应体。这是因为ServletInputStreamServletOutputStream分别继承 java 中的 InputStreamOutputStream 流对象,所以可以通过它们获取请求报文和设置响应报文。 进一步,在 Sping MVC 中,会将 Servlet 提供的请求和响应进行一层抽象 封装,便于操作读取和写入,再通过 HttpMessageConverter 消息转换机制来解析请求报文或者设置响应报文。

接下来,我们便看看HttpMessageConverter身上到底蕴藏着那些我们所不知道的秘密。

寻找HttpMessageConverter的处理入口

正如 "配置HttpMessageConverter" 一节中提到的,当我们将HttpMessageConverter相关配置文件加入到SpringMVC的配置文件后,SpringMVC会根据配置内容返回指定类型的数据信息。

此时你可能会想,通常我们定义控制层返回的内容信息时,通常会返回一个java对象,此时怎么配置这段信息后我的返回值类型就从java对象变为Json信息呢?

SpringMVC内部究竟对返回值做了哪些处理呢?又该从何处着手分析呢?

我们在上一篇SpringMVC流程分析(六):为处理器进行合适的"适配"中提到,对于RequestMappingHandlerAdapterhandler背后的逻辑无非就是调用被@ReqeustMapping标注的方法。此外,在处理过程中还会依赖一些额外的组件信息来完成参数解析,返回值处理等额外的操作

(注:默认分析通过@RequestMapping构建的处理器,所以对于适配器信息我们重点关注RequestMappingHandlerAdapter)

看到RequestMappingHandlerAdapter在执行hanlder方法时会包含对返回值处理等额外的操作 这些信息,不知你是不是会有这样一种猜想。即如果我们要分析SpringMVC内部是如何将返回的java对象转为Json格式的,我们应该关注RequestMappingHandlerAdapterhandler方法相关逻辑。因为其在执行方法完毕后会完成对返回值进行额外处理的操作。

事实上,在RequestMappingHandlerAdapter中对于返回值进行额外处理的调用链如下所示,其中handleReturnValue的作用就在于对返回值进行处理。

此处我们直接给出了RequestMappingHandlerAdapter方法中handlerInternal的调用链信息。为了方便理解,在如图所示的调用链中我们省略了一些其他方法的调用,因为我们的主要目的在于引入handleReturnValue方法。

有关handlerInternal方法的调用逻辑远比此复杂的多,感兴趣的可以自己翻看相关源码,在此我们便不再赘述。

继续沿着上述调用链你会进入到RequestResponseBodyMethodProcessorhandleReturnValue 方法中,而其该方法逻辑如下所示: writeWithMessageConverters方法

RequestResponseBodyMethodProcessor # handleReturnValue

java 复制代码
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, 
                              ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    // <1> 设置已处理
    mavContainer.setRequestHandled(true);
    // <2> 创建请求和响应
    ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
    ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
    // <3> 使用 HttpMessageConverter 对对象进行转换,并写入到响应
    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
j

上述代码在<2>处,会将请求封装成 ServletServerHttpRequestServletServerHttpResponse 两种类型,其中:

  • ServletServerHttpRequest:实现了 ServerHttpRequest接口;
  • ServletServerHttpResponse实现 ServerHttpResponse接口

进一步,我们关注<3>处的writeWithMessageConverters方法,因为对于java对象信息进行转换在此完成。

RequestResponseBodyMethodProcessor # writeWithMessageConverters

事实上,对于writeWithMessageConverters的调用又会进入到RequestResponseBodyMethodProcessor父类 AbstractMessageConverterMethodProcessorwriteWithMessageConverters方法中

(注:此处调用链可能看着有些复杂,但别乱,无非就是继承那些小事~~)

AbstractMessageConverterMethodProcessor # writeWithMessageConverters

java 复制代码
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
        ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    // <1> 获得 body、valueType、targetType
    Object body; Class<?> valueType; Type targetType;
    // <3> 选择使用的 MediaType
    MediaType selectedMediaType = null;

    // <4> 如果匹配到,则进行写入逻辑
    if (selectedMediaType != null) {
        // <4.1> 移除 quality 。例如,application/json;q=0.8 移除后为 application/json
        selectedMediaType = selectedMediaType.removeQualityValue();
        // <4.2> 遍历 messageConverters 数组
        for (HttpMessageConverter<?> converter : this.messageConverters) {
            // <4.3> 判断 HttpMessageConverter 是否支持转换目标类型
            GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter
                    ? (GenericHttpMessageConverter<?>) converter : null);
            if (genericConverter != null ?
                    ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType)
                    : converter.canWrite(valueType, selectedMediaType)) {
                
                // <5.2> body 非空,则进行写入
                if (body != null) {
                    if (genericConverter != null) {
                    // 信息写入
                        genericConverter.write(body, targetType, selectedMediaType, outputMessage);
                    } else {
                        ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
                    }
                }
                // <5.4> return 返回,结束整个逻辑
                return;
            }
        }
    }
    // ... 上面省略了大量代码
}

我们选取其中几个重要的方法进行介绍:

  1. <4.2> 处,遍历所有的 HttpMessageConverter 实现类

  2. <4.3> 处,调用当前 HttpMessageConverter 实现类的 canWrite 方法,判断是否支持写入

  3. <5.2> 处,调用该 HttpMessageConverter 实现类的 ** write ** 方法,进行写入

至此,相信你应该能明白一个大概了,其实SpringMVC中对返回值所谓的类型转换,本背后的逻辑原来全部依赖于HttpMessageConverterwrite 方法。我们前面那么多的铺垫,也就为了能引出此处的调用逻辑。

HttpMessageConverter 接口

HttpMessageConverter是对消息转换器最高层次的接口抽象,描述了一个消息转换器的一般特征。其内部相关法法如下:

java 复制代码
public interface HttpMessageConverter<T> {
    
    /** 能否读取 */
	boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    
    /** 能够写入 */
	boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
    
    /** 获取支持的 MediaType */
	List<MediaType> getSupportedMediaTypes();
    
    /** 读取请求体 */
	T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException;
    
    /** 设置响应体 */
	void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException;
}

HttpMessageConverter结构关系

进一步,HttpMessageConverter 接口体系的结构如下:

在如图所示的UML关系中,可以看到对于HttpMessageConverter的实现类可以分为两大分支:

  1. MappingJackson2HttpMessageConveter用于Json格式化数据的处理
  2. Jaxb2RootElementHttpMessageConverter用于Xml格式处理

(注:在后续分析中,我们以MappingJackson2HttpMessageConveter这一脉络进行重点讨论,以分析SpringMVC内部如何将返回值信息处理为Json格式类型的数据)

使用示例

在开始分析MappingJackson2HttpMessageConveter之前,我们先通过一个例子来直观的感受MappingJackson2HttpMessageConveter的作用。假设现在我们有如下代码信息:

java 复制代码
@RestController
public class UserController {
    @Autowired
    UserService userService;

    @GetMapping(value = "/query")
    public ResultInfo<List<User>> queryUser(@RequestBody String name) {
        try {
            return Result.success().data(userService.queryUserByName(name));
        } catch (Exception e) {
            return Result.fail(e);
        }
    }
}

当前端请发起一个请求路径为 GET /queryHTTP 后,由于对方法添加了@RequestBody 注解,所以从请求体读请求报文的,可以设置请求体中的数据为 yiHang ,即此时我们的业务逻辑为获取到到名字为 yiHang 的所有用户的信息。

Spring MVC 处理该请求时,会先进入到 RequestResponseBodyMethodProcessor 这个类获取方法入参,其中会通过 StringHttpMessageConverter 从请求体中读取 yiHang 数据,作为调用方法的入参。

Spring MVC 获取到方法的返回结果后,又会进入到RequestResponseBodyMethodProcessor这个类,往响应体中写数据,而这背后的逻辑都是通过 MappingJackson2HttpMessageConverterList<User> 返回结果写入响应。

总结下来,整个过程如下所示:

"顶级基类":AbstractHttpMessageConverter

AbstractHttpMessageConverter ,实现 HttpMessageConverter 接口,提供通用的骨架方法。

在之前分析writeWithMessageConverters时的<5.2>处有这样一句代码

java 复制代码
((HttpMessageConverter) converter).write(body, 
                                selectedMediaType, 
                                outputMessage);

其逻辑为调用HttpMessageConverterwrite方法,进而向前端写回响应内容,既然AbstractHttpMessageConverter会实现HttpMessageConverter接口,所以为了搞清楚HttpMessageConverter如何将数据内容信息格式化为json数据类型,我们应该关注HttpMessageConverter实现类的write方法。进一步,在AbstractHttpMessageConverterwrite方法如下:

AbstractHttpMessageConverter

java 复制代码
public final void write(final T t, 
                    @Nullable MediaType contentType, 
                    HttpOutputMessage outputMessage)
                      throws IOException, HttpMessageNotWritableException {
      // ....  省略其他无关代码
      
      // 抽象方法,交给子类实现,用以返回不同类型的格式数据信息
      writeInternal(t, outputMessage);
      outputMessage.getBody().flush();
   
}

可以看到,对于AbstractHttpMessageConverter方法而言,其在实现write方法时,会将写出内容的处理逻辑交给 writeInternal进行实现。进一步,其子类 AbstractJackson2HttpMessageConverter而言,其内部的writeInternal方法逻辑如下:

AbstractJackson2HttpMessageConverter

java 复制代码
@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
        throws IOException, HttpMessageNotWritableException {

    // <1> 获取编码方式
    // <1.1> 获取 Content-Type,例如 `application/json;charset=UTF-8`
    // appliation/json;charset=utf-8
    MediaType contentType = outputMessage.getHeaders().getContentType();
    // <1.2> 从 Content-Type 获取编码方式,默认 UTF8
    JsonEncoding encoding = getJsonEncoding(contentType);
    // <2> 构建一个 Json 生成器 `generator`,指定`输出流(响应)`和编码
    // 例如:UTF8JsonGenerator 对象(jackson-core 包)
    JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
    try {
        // <3> 设置前缀,默认没有
        writePrefix(generator, object);

        // <4> 获得方法的返回结果对象 `value`,返回结果类型 `javaType`
        Object value = object;
        Class<?> serializationView = null;
        FilterProvider filters = null;
        JavaType javaType = null;

        // <4.1> 如果返回结果对象是 MappingJacksonValue 类型,没使用过
        if (object instanceof MappingJacksonValue) {
            MappingJacksonValue container = (MappingJacksonValue) object;
            value = container.getValue();
            serializationView = container.getSerializationView();
            filters = container.getFilters();
        }
        // <4.2> 获取方法的返回结果的类型 `javaType`
        if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
            javaType = getJavaType(type, null);
        }

        // <5> 创建 ObjectWriter 对象 `objectWriter`,没有特殊配置通过 `this.objectMapper.writer()` 生成
        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);
        }
        // <6> 获取序列化配置
        SerializationConfig config = objectWriter.getConfig();
        if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
                config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
            objectWriter = objectWriter.with(this.ssePrettyPrinter);
        }
        // <7> **【重点】**通过 `objectWriter` 将返回结果进行序列化,设置到 `generator` 中
        // 多为fastjson中的序列化处理方式
        objectWriter.writeValue(generator, value);

        // <8> 设置后缀,默认没有
        writeSuffix(generator, object);
        // <9> 让 `generator` 刷出数据,以 Json 格式输出
        // 也就是会往响应中刷出 Json 格式的返回结果
        generator.flush();
    }
   
  }
    

对于上述代码我们重点关注的几个地方有如下几处:

<5> 创建 ObjectWriter 对象 objectWriter ,没有特殊配置通过 this.objectMapper.writer() 生成

<6>获取序列化配置

<7> 【重点】 通过 objectWriter 将返回结果进行序列化,设置到 generator

<8> 调用 writeSuffix(JsonGenerator generator, Object object) 方法,设置后缀。

事实上,上述代码可以归结为如下的调用链信息:

具体实现:MappingJackson2HttpMessageConverter

MappingJackson2HttpMessageConverter 继承 AbstractJackson2HttpMessageConverter 抽象类,JSON 格式的消息读取或者写入,也就是我们熟悉的 @RequestBody@ResponseBody 注解对应的 HttpMessageConverter 消息转换器

java 复制代码
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {

	@Nullable
	private String jsonPrefix;

	public MappingJackson2HttpMessageConverter() {
		this(Jackson2ObjectMapperBuilder.json().build());
	}
    
	public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
		super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
	}

	public void setJsonPrefix(String jsonPrefix) {
		this.jsonPrefix = jsonPrefix;
	}

	public void setPrefixJson(boolean prefixJson) {
		this.jsonPrefix = (prefixJson ? ")]}', " : null);
	}

	@Override
	protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
		if (this.jsonPrefix != null) {
			generator.writeRaw(this.jsonPrefix);
		}
	}
}

可以看到仅是添加了一个 jsonPrefix 属性,即Json 的前缀。除此之外,基本没有其他逻辑,这是因为相关处理逻辑都在父类中进行了定义和处理。

总结

Spring MVCHttpMessageConverter 机制中可以领悟到类似的道理,一次请求报文和一次响应报文,分别被抽象为一个请求消息 HttpInputMessage 和一个响应消息 HttpOutputMessage

在处理请求时,选取合适的 HttpMessageConverter 消息转换器将请求报文绑定为方法中的形参对象,同一个对象就有可能出现多种不同的消息形式,比如 json xml,同样,当响应请求时,方法的返回值也同样可能被返回为不同的消息形式,比如 json xml

而在 Spring MVC 中,针对不同的消息形式,有不同的 HttpMessageConverter 实现类来处理各种消息形式。但是,只要这些消息所蕴含的"有效信息"是一致的,那么各种不同的消息转换器,都会生成同样的转换结果。至于各种消息间解析细节的不同,就被屏蔽在不同的 HttpMessageConverter 实现类中了。

相关推荐
2202_754421545 分钟前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
蓝染-惣右介7 分钟前
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
java·数据库·tomcat·mybatis
小林想被监督学习8 分钟前
idea怎么打开两个窗口,运行两个项目
java·ide·intellij-idea
HoneyMoose10 分钟前
IDEA 2024.3 版本更新主要功能介绍
java·ide·intellij-idea
我只会发热12 分钟前
Java SE 与 Java EE:基础与进阶的探索之旅
java·开发语言·java-ee
是老余13 分钟前
本地可运行,jar包运行错误【解决实例】:通过IDEA的maven package打包多模块项目
java·maven·intellij-idea·jar
crazy_wsp13 分钟前
IDEA怎么定位java类所用maven依赖版本及引用位置
java·maven·intellij-idea
.Ayang16 分钟前
tomcat 后台部署 war 包 getshell
java·计算机网络·安全·web安全·网络安全·tomcat·网络攻击模型
bjzhang7521 分钟前
SpringBoot开发——Maven多模块工程最佳实践及详细示例
spring boot·maven·maven多模块工程
一直学习永不止步21 分钟前
LeetCode题练习与总结:最长回文串--409
java·数据结构·算法·leetcode·字符串·贪心·哈希表