SpringBoot-Web 开发 -4- 数据响应与内容协商
SpringBoot-Web 开发 -4- 数据响应与内容协商
ContentNegotiationManager 相关
跟 SpringMVC 不同,SpringBoot 自动配置里在注册内容协商策略的时候,默认注册不再是 ServletPathExtensionContentNegotiationStrategy
和 HeaderContentNegotiationStrategy
,而仅注册 HeaderContentNegotiationStrategy
,这是因为 ContentNegotiationConfigurer#favorPathExtension
属性默认为 false。
SpringBoot 的具体配置请看《SpringBoot-Web 相关自动配置类源码解析 -WebMvcAutoConfiguration.md》的
WebMvcAutoConfigurationAdapter
小节下的configureContentNegotiation
小节,和重写 DelegatingWebMvcConfiguration 的 mvcContentNegotiationManager
小节。
ContentNegotiationConfigurer#favorPathExtension
属性默认为 false 导致不注册ServletPathExtensionContentNegotiationStrategy
的相关源码请看《SpringMVC-ContentNegotiation 内容协商.md》的ContentNegotiationManagerFactoryBean
小节中记录的用于配置生产的contentNegotiationManager
的字段
SpringMVC 默认注册的内容协商策略有两个
ServletPathExtensionContentNegotiationStrategy
和HeaderContentNegotiationStrategy
![]()
SpringBoot 中可直接在配置文件中配置内容协商,但是只有四个参数可配置
spring:
mvc:
contentnegotiation:
favor-parameter: false
parameter-name: format
favor-path-extension: false
media-types:
json: application/json
xml: application/xml
具体的源码在《SpringBoot-Web 相关自动配置类源码解析 -WebMvcAutoConfiguration.md》的
WebMvcAutoConfigurationAdapter
小节下configureContentNegotiation
小节
如果需要进行进一步的更细粒度的配置,则只能进行通过 半自动Web配置
,在实现了 WebMvcConfigurer
的 Web 配置类中重写 configureContentNegotiation
方法,请看《SpringMVC- 第十篇:基于注解配置 SpringMVC.md》的 configureContentNegotiation
小节和《SpringMVC-ContentNegotiation 内容协商.md》的 通过Java配置
小节。
HttpMessageConverter 相关
对 json 和 xml 的支持
当我们添加 web 场景启动器的时候,就已经添加了 json 相关的依赖
<!-- 引入Web相关的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
在 spring-boot-starter-web
的依赖中
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
因此,只要引入了 web 相关启动器,应用就已经可以处理 JSON 数据。
而 SpringBoot 默认没有添加支持 xml 的 HttpMessageConverter,因此需要手动导入。(父 pom 中就已经有了版本控制)
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
对 HttpMessageConverter 的管理
SpringBoot 中对 HttpMessageConverter
的管理跟 SpringMVC 框架中确实不一样。专门增加了 HttpMessageConverters
类型的 bean 来管理所有的 HttpMessageConverter
,而且还专门额外创建了 HttpMessageConvertersAutoConfiguration
自动配置类来配置 HttpMessageConverters
。现在我们只需要注册 HttpMessageConverter
类型的 bean 即可添加自定义的 HttpMessageConverter
。
请看《SpringBoot-HttpMessageConverters 自动配置类源码解析 -HttpMessageConvertersAutoConfiguration.md》
一个很明显的问题是,如果添加了对 json 和 xml 的 HttpMessageConverter
的支持,json 和 xml 对应的 HttpMessageConverter
会添加两遍,但是 SpringMVC 中是不会的,
SpringMVC 中的情况:

SpringBoot 中的情况:

这是正常的,原因请看具体原因看《SpringBoot-HttpMessageConverters 自动配置类源码解析 -HttpMessageConvertersAutoConfiguration.md》的 HttpMessageConvertersAutoConfiguration
小节,
自定义 MediaType+ 自定义 MessageConverter
SpringMVC 版本的请看《SpringMVC-ContentNegotiation 内容协商.md》的
自定义MediaType+自定义MessageConverter
小节
自定义内容协商的要素:
-
自定义文件拓展名与自定义
MediaType
的映射可直接在配置文件中注册文件拓展名与自定义
MediaType
的映射,具体的源码在《SpringBoot-Web 相关自动配置类源码解析 -WebMvcAutoConfiguration.md》的WebMvcAutoConfigurationAdapter
小节下configureContentNegotiation
小节。或者使用 SpringMVC 的配置方式:
在实现
WebMvcConfigurer
的 Web 配置类中,重写configureContentNegotiation
方法,通过ContentNegotiationConfigurer
来添加(注册)自定义文件拓展名和MediaType
的映射关系具体请参考《SpringMVC- 第十篇:基于注解配置 SpringMVC.md》的
configureContentNegotiation
小节 -
自定义 MessageConverter 处理特定控制器方法的返回值类型到自定义 MediaType 的转化,即自定义
MessageConverter
,可直接注册
HttpMessageConverter
类型的 bean 即可添加自定义的HttpMessageConverter
。具体原理请看《SpringBoot-HttpMessageConverters 自动配置类源码解析 -HttpMessageConvertersAutoConfiguration.md》或者使用 SpringMVC 的配置方式:
参考《SpringMVC- 第十篇:基于注解配置 SpringMVC.md》的
configureMessageConverters & extendMessageConverters
小节
实践
首先添加自定义消息类型,XiaShuoMessage
@Data
public class XiaShuoMessage {
private String name;
private int age;
private String info;
}
配置文件中添加内容协商的相关配置
spring:
mvc:
contentnegotiation:
favor-parameter: true
parameter-name: _f
media-types:
xs: application/xs
其中,为了支持请求路径扩展名(
ServletPathExtensionContentNegotiationStrategy
)和请求参数(ParameterContentNegotiationStrategy
)的内容协商,我们最好添加自定义的MediaType
到文件拓展名的映射,如果只是使用基于Accept
请求头进行内容协商,则不需要添加MediaType
到文件拓展名的映射。
然后添加只能处理 XiaShuoMessage
类型的实例到 application/xs
(自定义 MediaType
)之间转化的 HttpMessageConverter
:XiaShuoMessageConverter
(且还未支持读操作)。并注册到 IOC 容器中:
/**
* 参考 StringHttpMessageConverter
* 仅支持 `XiaShuoMessage`类型的实例到`application/xs`(自定义`MediaType`)之间转化
* 且还未支持读操作
*/
@Component
public class XiaShuoMessageConverter extends AbstractHttpMessageConverter<XiaShuoMessage> {
// 默认编码
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
public XiaShuoMessageConverter() {
// 仅支持 "application/xs" 类型的MediaType
super(DEFAULT_CHARSET, MediaType.valueOf("application/xs"));
}
@Override
protected boolean supports(Class<?> clazz) {
// 只支持处理 XiaShuoMessage 类型的实例
return XiaShuoMessage.class.isAssignableFrom(clazz);
}
@Override
protected XiaShuoMessage readInternal(Class<? extends XiaShuoMessage> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
// 读操作有待实现
return null;
}
@Override
protected void writeInternal(XiaShuoMessage xiaShuoMessage, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
String content = myProtocolContent(xiaShuoMessage);
// 输出到响应的消息体
StreamUtils.copy(content, DEFAULT_CHARSET, outputMessage.getBody());
}
/**
* 自定义消息输出内容格式
*
* @param xiaShuoMessage
* @return
*/
private String myProtocolContent(XiaShuoMessage xiaShuoMessage) {
// 当然你可以在这里选择其他的拼接方式,比如把所有的属性用英文逗号拼接,等等。
return xiaShuoMessage.toString();
}
@Override
protected void addDefaultHeaders(HttpHeaders headers, XiaShuoMessage s, @Nullable MediaType type) throws IOException {
// 不重写 addDefaultHeaders 指定响应的 ContentType 消息头的话,默认的addDefaultHeaders会自动设置 ContentType 为自定义 MediaType :application/xs;charset=UTF-8,
// 浏览器会处理不了,会变成下载,所以这里需要设置ContentType 消息头,设置成 MediaType.TEXT_PLAIN 是为了方便展示
// 指定类型的时候指定编码
headers.setContentType(new MediaType(MediaType.TEXT_PLAIN, DEFAULT_CHARSET));
super.addDefaultHeaders(headers, s, type);
}
@Override
protected Long getContentLength(XiaShuoMessage message, @Nullable MediaType contentType) {
String str = myProtocolContent(message);
return (long) str.getBytes(DEFAULT_CHARSET).length;
}
}
配置结束,真的很方便。
创建控制器方法来测试内容协商
@RestController
@RequestMapping("/ContentNegotiation")
public class ContentNegotiationController {
/**
* 能处理 XiaShuoMessage 类型返回值且将其转化为 application/xs 类型的 MediaType 的只有 XiaShuoMessageConverter
*/
@RequestMapping("/XiaShuo")
public XiaShuoMessage getXiaShuoMessage() {
XiaShuoMessage xiaShuoMessage = new XiaShuoMessage();
xiaShuoMessage.setName("xiashuo");
xiaShuoMessage.setAge(20);
xiaShuoMessage.setInfo("这是我的自定义的协议");
return xiaShuoMessage;
}
}
访问 http://localhost:8081/SpringBoot-WebSimple/ContentNegotiation/XiaShuo?_f=xs
,返回
访问 http://localhost:8081/SpringBoot-WebSimple/ContentNegotiation/XiaShuo?_f=json
,返回