SpringBoot-Web 开发 -4- 数据响应与内容协商

SpringBoot-Web 开发 -4- 数据响应与内容协商

ContentNegotiationManager 相关

跟 SpringMVC 不同,SpringBoot 自动配置里在注册内容协商策略的时候,默认注册不再是 ServletPathExtensionContentNegotiationStrategyHeaderContentNegotiationStrategy,而仅注册 HeaderContentNegotiationStrategy,这是因为 ContentNegotiationConfigurer#favorPathExtension 属性默认为 false。

SpringBoot 的具体配置请看《SpringBoot-Web 相关自动配置类源码解析 -WebMvcAutoConfiguration.md》的 WebMvcAutoConfigurationAdapter 小节下的 configureContentNegotiation 小节,和 重写 DelegatingWebMvcConfiguration 的 mvcContentNegotiationManager 小节。

ContentNegotiationConfigurer#favorPathExtension 属性默认为 false 导致不注册 ServletPathExtensionContentNegotiationStrategy 的相关源码请看《SpringMVC-ContentNegotiation 内容协商.md》的 ContentNegotiationManagerFactoryBean 小节中记录的用于配置生产的 contentNegotiationManager 的字段

SpringMVC 默认注册的内容协商策略有两个 ServletPathExtensionContentNegotiationStrategyHeaderContentNegotiationStrategy

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 小节

自定义内容协商的要素:

实践

首先添加自定义消息类型,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)之间转化的 HttpMessageConverterXiaShuoMessageConverter(且还未支持读操作)。并注册到 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,返回