Spring--视图内容协商(三)
Spring--视图内容协商(三)
本文是学习了小马哥在慕课网的课程的《Spring Boot 2.0深度实践之核心技术篇》的内容结合自己的需要和理解做的笔记。
上两篇文章,简单介绍了一下Spring的视图内容协商。接下来我们针对 REST 内容协商做一下介绍。
没想到截图效果怎么不好,实在是失落。
大纲
- 理解REST请求媒体类型
- REST内容协商流程
- REST内容协商源码分析
理解REST请求媒体类型
理解解析请求的媒体类型
在我们使用Spring开发的时候,相信 @RequestMapping
这个注解再也熟悉不过了,相信使用Restful API 接口形式开发的小伙伴们都避免不了设置API的媒体类型 比如 Accept
和 Content-Type
,在前一篇我们也说过 Spring 通过 ContentNegotiationManager
的 ContentNegotiationStrategy
解析请求中的媒体类型。
以 HeaderContentNegotiationStrategy
为例
- 如果解析成功,则返回合法的
MediaType
集合。 - 否则,返回
MediaType.ALL
默认的媒体类型,也就是*/*
在这里我们可以看一下HeaderContentNegotiationStrategy
源码,具体解释已经在注释中给出
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request)
throws HttpMediaTypeNotAcceptableException {
//获取 Accept的 媒体类型的字符串数组
String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
//如果为空 则返回 全部类型 也就是 */*
if (headerValueArray == null) {
return MEDIA_TYPE_ALL_LIST;
}
List<String> headerValues = Arrays.asList(headerValueArray);
try {
//转换为Spring 内置媒体类型集合
List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
//最佳媒体类型排序
MediaType.sortBySpecificityAndQuality(mediaTypes);
return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotAcceptableException(
"Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
}
}
当然 在 ContentNegotiationManager
中也会有判断
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
//循环遍历协商处理策略
for (ContentNegotiationStrategy strategy : this.strategies) {
//获取媒体类型 比如 HeaderContentNegotiationStrategy
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
//如果媒体类型是 默认的所有 即 */* 则跳过
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
continue;
}
return mediaTypes;
}
//如果都不满足 则最后返回 默认所有 即*/*
return MEDIA_TYPE_ALL_LIST;
}
对比上面的源码我们可以看到 Spring 做了很多层校验 来判断媒体类型是否为空。
理解可生成的媒体类型
使用@RequestMapping.produces()
属性 来指定MediaType 类型集合 影响 浏览器响应头 Content-Type 媒体类型映射。
- 如果
@RequestMapping.produces()
存在,返回指定 MediaType 列表。 - 否则,返回已注册的
HttpMessageConverter
列表中支持的 MediaType 列表。 - 如果该列表与请求的媒体类型兼容,执行第一个兼容
HttpMessageConverter
的实现,默认
@RequestMapping#produces
内容到响应头 Content-Type
否则,抛出HttpMediaTypeNotAcceptableException
, HTTP Status Code : 415
这段源码会在稍后的源码分析中做详解。
理解可消费的媒体类型
使用@RequestMapping#consumes
属性,来设置兼容的MediaType 类型集合 过滤 请求头 Content-Type 媒体类型映射。
- 如果请求头 Content-Type 媒体类型兼容
@RequestMapping.consumes()
属性,执行该HandlerMethod
- 否则
HandlerMethod
不会被调用。
这段源码会在稍后的源码分析中做详解。
REST内容协商流程
在讲述REST协商流程之前,我们先来了解一下 对于REST内容协商非常关键的两个解析器
- 处理方法参数解析器(
HandlerMethodArgumentResolver
)- 用于 HTTP 请求中解析
HandlerMethod
参数内容
- 用于 HTTP 请求中解析
- 处理方法返回值解析器(
HandlerMethodReturnValueHandler
)- 用于
HandlerMethod
返回值解析为 HTTP 响应内容
- 用于
流程图
对于协商流程,在这里贴一张图,流程大体就可以理解了,在结合下面的源码分析,相信很快就可以明白Spring的视图协商流程。
针对 上述第10步 转化HTTP消息 的详细流程图
调试前的代码准备
在这里需要增加一个简单的User
对象以及对应请求的Controller
User.java
/**
* 用户对象
*/
public class User {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
UserRestController.java
@RestController
public class UserRestController {
//最终相应回浏览器的Content-Type 是 produces 的内容
@PostMapping(value = "/user",
consumes = "application/json;charset=UTF-8",
produces = "application/json;charset=GBK")
public User user(@RequestBody User user) {
return user;
}
}
添加完这两个类之后,让我们启动一下Spring-boot项目。
我们可以对比上面的总体流程图来对源码进行解读。我们使用PostMan来发送一个简单的请求。
首先我们先打开DispatcherServlet#doDispatch
方法。我们来关注下面两个方法

首先我们先来看一下 步骤 2和3的对应方法

接下来让我们进入到 步骤 4的对应方法中

接下来就是我们的重头戏就是调用HandlerMethod
,在这个阶段就包括了剩下的步骤,也就是步骤5~10。

我们一步一步的往方法里进。

我们接着进入到 RequestMappingHanlderAdapter
中的 handleInternal
方法。



我们接着进入到 invokeAndHandle
方法


我们详细看一下如何解析方法参数的。

这里我们重点看一下 argumentResolvers.supportsParameter(parameter)
这段代码 主要是判断 参数处理器是否支持解析传入的参数。

判断完某个解析器(RequestResponseBodyMethodProcessor
)是否支持解析,接下来就是具体解析的操作了。

进入到 resolveArgument
方法


在这里我们已经获取到了 入参的值,我们回到最初调用的方法然后反射调用方法。



至此 我们步骤8之前的已经讲解完了。下面我们讲解一下返回值解析。我们重新回到org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle
方法中。

进入handleReturnValue
方法。

我们来看一下 selectHandler()
这个方法

选择好了返回值解析器 (RequestResponseBodyMethodProcesser
),下面我们就看看如何解析返回值的。
细心的小伙伴可以发现 这个解析器即是 方法参数解析器 又是 返回值解析器。这里就不多做解释了 这是因为他即继承AbstractMessageConverterMethodArgumentResolver
抽象类又实现了 HandlerMethodReturnValueHandler
接口。我们再进入到里面一层的handleReturnValue


接下来的内容就是详细流程图中的内容了我们从第7步开始看,我们通过媒体类型来匹配HttpMessageConverter。


选择好转换器之后我们就可以进行转换了,我们看一下MappingJackson2HttpMessageConverter
是如何转换然后返回相应的。




最后,在PostMan中显示返回结果


通过响应头 我们也可以看到 @RequestMapping.produces()
的作用。
总结
虽然协商逻辑以及流程比较繁琐,但是在我们使用Spring的时候,这些功能给了我们很大的便利,至于 最后Jackson是如何序列化的,这里就不详细说明了。不属于本次内容的范畴。 别的不多说,继续努力吧。