目录

一、前言

二、SpringBoot 内容协商介绍

2.1 什么是内容协商

2.2 内容协商机制深入理解

2.2.1 内容协商产生的场景

2.3 内容协商实现的常用方式

2.3.1 前置准备

2.3.2 通过HTTP请求头

2.3.2.1 操作示例

2.3.3 通过请求参数

三、SpringBoot 消息转换器介绍

3.1 HttpMessageConvertor介绍

3.1.1 常用的HttpMessageConvertor

3.2 如何确定使用哪个消息转换器

3.2.1 针对请求时的判断

3.2.2 针对响应时的判断

3.3 SpringMVC框架默认的消息转换器

3.3.1 源码跟踪

四、自定义消息转换器

4.1 自定义yaml消息转换器

4.1.1 引入如下的依赖

4.1.2 自定义yaml媒体类型

4.1.3 自定义HttpMessageConverter

4.1.4 配置消息转换器

4.1.5 测试与效果验证

五、写在文末


一、前言

在微服务开发中,客户端与服务端数据格式的协商和转换是一个经常接触的场景,不同的业务场景下,对于数据格式的要求也不同,比如有的客户端需要服务器响应XML格式数据,有的需要响应Json格式数据,这就是HTTP消息内容协商机制的源头,如何满足复杂多变的HTTP消息转换需求呢,本篇将详细分享如何在SpringBoot框架中完成自定义消息转换器的定制开发与使用。

二、SpringBoot 内容协商介绍

2.1 什么是内容协商

内容协商(Content Negotiation)是指服务器根据客户端请求来决定响应的内容类型(MIME 类型)。这使得应用程序可以根据客户端的需求返回不同格式的数据,如 JSON、XML 或 HTML 等。Spring Boot 通过 HttpMessageConverters 和 @RequestMapping 注解等机制来支持内容协商。

2.2 内容协商机制深入理解

内容协商机制是指服务器根据客户端的请求来决定返回资源的最佳表现形式

  • 白话描述:客户端需要什么格式的数据,服务端就返回什么格式的数据。

比如:

  • 客户端需要json,就响应json;
  • 客户端需要xml,就响应xml;
  • 客户端需要yaml,就响应yaml;

于是,你可能会有疑问,客户端接收数据时统一采用一种格式,例如Json不就行了,为什么还有那么多的格式要求呢?因为在实际开发中并不是这样的,比如在下面的场景:

  • 遗留的老的系统中的某些业务,处理数据时仍然使用的是xml格式;
  • 对于处理速度有要求的这种系统,明确要求使用json格式的数据;
  • 对于安全要求比较高的系统,一般要求使用xml格式的数据;
  • 某些业务场景下明确指定了某个类型的数据格式...

基于上面的场景,在当下流行的微服务开发模式下,不同的客户端可能需要后端返回不同格式的数据,于是,对于后端来说,就需要尽可能的适配和满足这种多样化的需求场景。

2.2.1 内容协商产生的场景

内容协商的产生具有一定的背景,下面列举了产生内容协商的一些因素

  • 多客户端支持
  • 浏览器用户可能希望看到 HTML 页面。
  • 移动应用开发者可能更倾向于使用 JSON 数据来解析和展示信息。
  • 某些旧系统或特定工具可能依赖于 XML 格式的响应。
  • 提升用户体验
  • 不同的客户端有不同的偏好和要求。允许客户端指定他们想要的内容类型可以提高交互效率,减少不必要的数据处理步骤,并确保最终呈现给用户的界面是最优化的。例如,某些设备可能更适合处理压缩过的二进制格式,而不是文本格式的数据。
  • 遵照RESTful 原则
  • 遵循 REST 架构风格的应用程序通常会根据资源的状态来确定响应的内容类型,而不是依赖于 URL 的变化。这意味着同一个 URI 可以根据请求的不同部分(如 HTTP 方法、查询参数或头部信息)返回不同类型的内容。内容协商是实现这一设计理念的关键机制之一。

2.3 内容协商实现的常用方式

通常来说,通过HTTP请求头(比如Accept)获取请求参数(如Format),来指定客户端偏好接收的内容类型(JSON或XML等),服务器会根据这些信息选择合适的格式进行响应。下面介绍2种比较常用的方式。

2.3.1 前置准备

为了后续的操作演示,请提前在工程中导入下面几个基础依赖

<properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/>
    </parent>

    <dependencies>

        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-yaml</artifactId>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

    </dependencies>

</project>

2.3.2 通过HTTP请求头

SpringBoot框架中,如果开发人员不做任何配置的情况下,优先使用这种方式。

  • 服务器会根据客户端发送请求时提交在请求头中的信息,比如:”Accept:application/json“或"Accept:text/html"来决定最终响应什么格式数据;
2.3.2.1 操作示例

添加一个接口

@RestController
public class UserController {

    //localhost:8081/getUser
    @GetMapping("/getUser")
    public Object getUser(){
        return new User("mike",18);
    }

}

正常调用,请求头不加任何参数默认得到的是json结构

【微服务】SpringBoot 自定义消息转换器使用详解_springboot消息转换器

如果在请求头指定响应的数据格式,如下,在Accept中指定是json

curl -H "Accept: application/json" localhost:8081/getUser

【微服务】SpringBoot 自定义消息转换器使用详解_spring消息转换器详解_02

如果此时我们指定返回xml格式的数据,此时发现并不好使

curl -H "Accept: application/xml" localhost:8081/getUser

如果需要支持该怎么办呢?需要做下面的2步:

1)添加依赖jackson-dataformat-xml

  • 可以将Java对象转为xml格式的数据
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

2)为实体类增加注解

在当前的User类上面添加注解 @JacksonXmlRootElement用于转换为xml

package com.congge.entity;

import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@JacksonXmlRootElement
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    private String name;
    private int age;

}

3)请求测试调用

指定为json

【微服务】SpringBoot 自定义消息转换器使用详解_消息转换器使用详解_03

指定为xml

【微服务】SpringBoot 自定义消息转换器使用详解_springmvc消息转换器_04

总结:

  • 客户端请求的时候,通过在请求协议的请求头上面增加一个Accept字段,服务端接收到这个值之后,会根据这个参数值动态返回客户端要求的格式的数据;

2.3.3 通过请求参数

也可以在请求url中拼接指定的请求参数的方式实现,默认的请求参数名为format,格式如下:

http://xxx?format=json

仍然以上面的接口为例,测试一下这种方式的使用

curl http://localhost:8081/getUser?format=json

效果如下:

【微服务】SpringBoot 自定义消息转换器使用详解_springboot消息转换器_05

但是如果指定format为xml,发现并不生效

【微服务】SpringBoot 自定义消息转换器使用详解_spring自定义消息转换器_06

原因是springboot中在内容协商的处理上,优先使用Accept这种方式,所以如果你要使用format这种方式,还需在配置文件中增加下面的配置信息;

#使用format的方式完成内容协商,如果没有配置,默认采用Accept的方式实现
spring:
  mvc:
    contentnegotiation:
      favor-parameter: true
      #默认就叫format,也可以改为自定义的名称
      parameter-name: format

设置完成后再次重启服务测试,此时可以看到两种格式的数据都支持

【微服务】SpringBoot 自定义消息转换器使用详解_消息转换器使用详解_07

三、SpringBoot 消息转换器介绍

在上面通过案例操作演示介绍了什么是spring框架的内容协商机制,简单来说就是,客户端需要什么样格式的数据,服务端就响应什么格式数据,事实上真的就那么简单吗?这背后框架做了什么呢?是不是有什么组件在这个转换的过程中起作用了呢?接下来就要详细介绍springmvc框架中对于内容协商的重要技术组件,即HttpMessageConvertor。

3.1 HttpMessageConvertor介绍

HttpMessageConvertor是一个接口,被翻译为HTTP消息转换器,即对HTTP消息进行转换,什么是HTTP消息呢?HTTP消息本质上就是浏览器向服务端发送请求时提交的数据,或者是服务器向浏览器响应的数据。而HttpMessageConvertor接口就是负责完成请求/响应时数据格式转换用的。

  • 在springmvc框架中提供了很多种HttpMessageConvertor接口的实现类,不同的HTTP消息转换器具有不同的转换效果,使用的场景也有区别,有的是负责将Java对象转为JSON格式的数据,有的负责将Java对象转为XML格式的数据。

3.1.1 常用的HttpMessageConvertor

springmvc框架内置了一些常用的消息转换器,正是这些转换器完成了诸如上述json或xml格式的数据转换,下面介绍一些常用的框架内置的消息转换器:

  • FormHttpMessageConvertor
  • 常用于处理提交表单数据时候使用的转换器;
  • MappingJackson2HttpMessageConvertor
  • 客户端或浏览器提交JSON格式的数据转换为JAVA对象主要是由这个转换器处理,比如经常在POST请求接口上面添加的@RequestBody注解;
  • JaxbRootElementHttpMessageConvertor
  • 将JAVA对象转为XML格式的数据通常由这个消息转换器完成;
  • StringHttpMessageConvertor
  • 将String类型的的数据直接写入到响应中由这个转换器完成;

3.2 如何确定使用哪个消息转换器

有这么多的消息转换器,那么在具体使用的时候,框架是如何确定使用哪种类型的转换器的呢?

3.2.1 针对请求时的判断

请求时通常根据下面的条件来确定使用哪个消息转换器:

  • 请求的Content-Type头信息
  • SpringMVC会检查Content-Type头信息,以确定请求体的数据格式,比如:application/json,application/xml...
  • 方法参数类型
  • 控制器方法中接收请求体的参数类型,比如POST请求中有@RequestBody注解;

3.2.2 针对响应时的判断

响应时通常根据以下条件来确定使用哪个消息转换器:

  • 请求提交时,请求头上的Accept字段
  • Spring MVC 会检查客户端请求的 Accept 字段,以确定客户端期望的响应格式(例如 application/json、application/xml 等);
  • 方法返回值的类型
  • 控制器方法的返回值类型比如: @ResponseBody
  • @ResponseBody + 控制器方法的返回值是String,则使用StringHttpMessageConverter转换器。(将字符串直接写入响应体)
  • @ResponseBody + 控制器方法的返回值是Java对象,则使用MappingJackson2HttpMessageConverter转换器。(将java对象转换成json格式的字符串写入到响应体)

3.3 SpringMVC框架默认的消息转换器

SpringMVC框架自身已经内置了一些消息转换器,可以在启动的时候debug源码看到,主要包括下面6个

  • ByteArrayHttpMessageConverter
  • 用于将字节数组(byte[])与HTTP消息体之间进行转换。这通常用于处理二进制数据,如图片或文件。
  • StringHttpMessageConverter
  • 用于将字符串(String)与HTTP消息体之间进行转换。它支持多种字符集编码,能够处理纯文本内容。
  • ResourceHttpMessageConverter
  • 用于将Spring的Resource对象与HTTP消息体之间进行转换。Resource是Spring中表示资源的接口,可以读取文件等资源。这个转换器对于下载文件或发送静态资源有用。
  • ResourceRegionHttpMessageConverter
  • 用于处理资源的部分内容(即“Range”请求),特别是当客户端请求大文件的一部分时。这对于实现视频流媒体等功能很有用。
  • AllEncompassingFormHttpMessageConverter
  • 用于处理表单,是一个比较全面的form消息转换器。处理标准的application/x-www-form-urlencoded格式的数据,以及包含文件上传的multipart/form-data格式的数据。
  • MappingJackson2HttpMessageConverter
  • 使用Jackson库来序列化和反序列化JSON数据。可以将Java对象转换为JSON格式的字符串,反之亦然。

3.3.1 源码跟踪

入口类:WebMvcAutoConfiguration

  • WebMvcAutoConfiguration内部类EnableWebMvcConfiguration
  • EnableWebMvcConfiguration继承了DelegatingWebMvcConfiguration
  • DelegatingWebMvcConfiguration继承了WebMvcConfigurationSupport

【微服务】SpringBoot 自定义消息转换器使用详解_springboot消息转换器_08

DelegatingWebMvcConfiguration

【微服务】SpringBoot 自定义消息转换器使用详解_springboot消息转换器_09

继续进入到WebMvcConfigurationSupport

【微服务】SpringBoot 自定义消息转换器使用详解_spring自定义消息转换器_10

在这个类中,提供了一个方法 addDefaultHttpMessageConverters,在这个方法中,会将工程中的所有的消息转换器加进去。下面通过debug源码的方式跟进一下过程。

启动springboot工程后,进入该方法,此时messageConverters这个列表还是空的

【微服务】SpringBoot 自定义消息转换器使用详解_spring消息转换器详解_11

从源码不难看出,方法中会new出几个内置的转换器加入到这个集合中

【微服务】SpringBoot 自定义消息转换器使用详解_spring消息转换器详解_12

在上面的方法中,注意到会有一个判断的方法,比如:jackson2XmlPresent,它是如何判断的呢?其实在当前的类中,在静态代码块中维护了一个全局的布尔变量,工程在加载的时候,通过ClassUtils.isPresent方法,传入类的全路径,从而判断是否满足条件,满足,则在addDefaultHttpMessageConverters方法执行时候加入进去。

【微服务】SpringBoot 自定义消息转换器使用详解_springboot消息转换器_13

最后在这个方法执行完成的时候,列表中就添加了一些消息转换器

【微服务】SpringBoot 自定义消息转换器使用详解_spring自定义消息转换器_14

通过debug源码不难看出,在实际开发中,只要引入相关的依赖,让类路径存在某个类,则对应的消息转换器就会被加载。

四、自定义消息转换器

实际项目开发过程中,来自客户端的需求场景是很多的,当系统内置的转换器格式不能满足要求时,比如需要返回yaml格式的数据,或者其他定制化类型的数据时,此时就可以考虑自定义消息转换器。下面以yaml这种特殊格式的数据为例进行说明。

4.1 自定义yaml消息转换器

下面看具体的操作步骤。

4.1.1 引入如下的依赖

任何一个能够处理yaml格式数据的库都可以,这里选择使用jackson的库,因为它既可以处理json,xml,又可以处理yaml。

<dependency>
      <groupId>com.fasterxml.jackson.dataformat</groupId>
      <artifactId>jackson-dataformat-yaml</artifactId>
</dependency>

通过下面这段程序测试一下这个SDK的转换效果

import com.congge.entity.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;

public class JavaYamlTest {

    public static void main(String[] args) throws JsonProcessingException {
        // 创建YAML工厂类
        YAMLFactory yamlFactory = new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER); // 禁止使用文档头标记
        // 创建对象映射器
        ObjectMapper objectMapper = new ObjectMapper(yamlFactory);
        // 准备数据
        User user = new User("user01", 12);
        // 将数据转换成YAML格式
        String res = objectMapper.writeValueAsString(user);
        System.out.println(res);
    }

}

运行可以看到能够正常转换

【微服务】SpringBoot 自定义消息转换器使用详解_springmvc消息转换器_15

4.1.2 自定义yaml媒体类型

Springboot 默认支持xml和json两种媒体类型,如果要支持yaml格式的,需新增一个yaml媒体类型,在springboot的配置文件中进行如下配置:

spring:
  mvc:
    contentnegotiation:
      media-types:
        yaml: text/yaml

注意:

  • 以上types后面的yaml是媒体类型的名字,名字可以自己修改,如果媒体类型起名为xyz,那么发送请求时的路径应该是这样的:http://localhost:8081/getUser?format=xyz

4.1.3 自定义HttpMessageConverter

编写一个类,比如:YamlHttpMessageConverter继承AbstractHttpMessageConverter,需要继承AbstractHttpMessageConverter这个类,参考下面的代码:

package com.congge.config;

import com.congge.entity.User;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;

import java.io.IOException;
import java.nio.charset.Charset;

public class YamlHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

    private ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER));

    /**
     * 将自定义的消息转换器 和 配置文件中自定义的媒体类型 text/yaml 进行绑定
     */
    public YamlHttpMessageConverter() {
        super(new MediaType("text", "yaml", Charset.forName("UTF-8")));
    }

    /**
     * 用于指定消息转换器支持哪些类型的对象转换,比如这里指定User对象类型的数据进行转换
     * @param clazz
     * @return
     */
    @Override
    protected boolean supports(Class<?> clazz) {
        // 表示User类型的数据支持yaml,其他类型不支持
        return User.class.isAssignableFrom(clazz);
    }

    /**
     * 处理 @RequestBody(将提交的yaml格式数据转换为java对象)
     * @param clazz
     * @param inputMessage
     * @return
     * @throws IOException
     * @throws HttpMessageNotReadableException
     */
    @Override
    protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    /**
     * 处理 @ResponseBody(将java对象转换为yaml格式的数据)
     * @param o
     * @param outputMessage
     * @throws IOException
     * @throws HttpMessageNotWritableException
     */
    @Override
    protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        this.objectMapper.writeValue(outputMessage.getBody(), o);
        // 注意:spring框架会自动关闭输出流,无需程序员手动释放。
    }
}

补充说明:

  • 所有的消息转换器,包括自定义的,都需要实现HttpMessageConverter接口,或者继承AbstractHttpMessageConverter这个类,重写里面的核心方法。

4.1.4 配置消息转换器

重写WebMvcConfigurer接口的configureMessageConverters方法,将上面的自定义消息加入到全局的转换器列表中。

package com.congge.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class ConverterWebConfig implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new YamlHttpMessageConverter());
    }

}

4.1.5 测试与效果验证

启动工程,通过下面的curl命令再次测试,可以看到通过上面的自定义改造已经能够输出yaml格式的数据了

curl -H "Accept: text/yaml" localhost:8081/getUser

【微服务】SpringBoot 自定义消息转换器使用详解_spring消息转换器详解_16

针对其他类型格式的转换器,也可以参照上面的步骤进行编写即可

五、写在文末

本文详细介绍了SpringBoot消息转换器的知识,并通过案例操作演示了如何进行自定义消息转换器的定制开发和使用,希望对看到的同学有用哦,本篇到此结束,感谢观看。