1. 概述

JSON 和 XML 是 REST API 中广泛使用的数据传输格式,但它们并非唯一选择。市面上存在多种其他格式,它们在序列化速度和传输数据大小上各有优势。

本文将探讨如何配置 Spring REST 机制以支持二进制数据格式——我们以 Kryo 为例进行演示。此外,还会展示如何通过添加 Google Protocol Buffers 支持来实现多数据格式兼容。

2. HttpMessageConverter

HttpMessageConverter 接口本质上是 Spring 用于转换 REST 数据格式的公开 API。指定所需转换器有几种方式,这里我们实现 WebMvcConfigurer 并在重写的 configureMessageConverters 方法中显式提供目标转换器:

@Configuration
@EnableWebMvc
@ComponentScan({ "com.baeldung.web" })
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        //...
    }
}

3. Kryo

3.1. Kryo 概述与 Maven 依赖

Kryo 是一种二进制编码格式,相比文本格式具有以下优势:

  • 更快的序列化/反序列化速度
  • 更小的传输数据体积

虽然理论上可用于跨系统数据传输,但它主要设计用于 Java 组件间通信。通过以下 Maven 依赖添加 Kryo 库:

<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>4.0.0</version>
</dependency>

提示:最新版本可查阅 Maven 中央仓库

3.2. 在 Spring REST 中集成 Kryo

要使用 Kryo 作为数据传输格式,需执行以下步骤:

  1. 创建自定义 HttpMessageConverter
  2. 实现序列化/反序列化逻辑
  3. 定义自定义 HTTP 头 application/x-kryo

以下是完整简化示例:

public class KryoHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

    public static final MediaType KRYO = new MediaType("application", "x-kryo");

    private static final ThreadLocal<Kryo> kryoThreadLocal = new ThreadLocal<Kryo>() {
        @Override
        protected Kryo initialValue() {
            Kryo kryo = new Kryo();
            kryo.register(Foo.class, 1);
            return kryo;
        }
    };

    public KryoHttpMessageConverter() {
        super(KRYO);
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return Object.class.isAssignableFrom(clazz);
    }

    @Override
    protected Object readInternal(
      Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException {
        Input input = new Input(inputMessage.getBody());
        return kryoThreadLocal.get().readClassAndObject(input);
    }

    @Override
    protected void writeInternal(
      Object object, HttpOutputMessage outputMessage) throws IOException {
        Output output = new Output(outputMessage.getBody());
        kryoThreadLocal.get().writeClassAndObject(output, object);
        output.flush();
    }

    @Override
    protected MediaType getDefaultContentType(Object object) {
        return KRYO;
    }
}

踩坑提醒:使用 ThreadLocal 是因为创建 Kryo 实例开销较大,需要尽可能复用

控制器方法实现非常直接(注意无需特定协议的数据类型,使用普通 Foo DTO 即可):

@RequestMapping(method = RequestMethod.GET, value = "/foos/{id}")
@ResponseBody
public Foo findById(@PathVariable long id) {
    return fooRepository.findById(id);
}

验证集成的测试用例:

RestTemplate restTemplate = new RestTemplate();
restTemplate.setMessageConverters(Arrays.asList(new KryoHttpMessageConverter()));

HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(KryoHttpMessageConverter.KRYO));
HttpEntity<String> entity = new HttpEntity<String>(headers);

ResponseEntity<Foo> response = restTemplate.exchange("http://localhost:8080/spring-rest/foos/{id}",
  HttpMethod.GET, entity, Foo.class, "1");
Foo resource = response.getBody();

assertThat(resource, notNullValue());

4. 支持多数据格式

实际开发中常需为同一服务提供多种数据格式支持。客户端通过 Accept HTTP 头指定所需格式,Spring 会自动调用对应的转换器。

通常只需注册新转换器即可开箱即用。Spring 会根据 Accept 头值和转换器支持的媒体类型自动选择合适的转换器。

例如同时支持 JSON 和 Kryo:

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
    messageConverters.add(new MappingJackson2HttpMessageConverter());
    messageConverters.add(new KryoHttpMessageConverter());
    super.configureMessageConverters(messageConverters);
}

现在假设要添加 Google Protocol Buffers 支持。假设存在通过 protoc 编译器生成的 FooProtos.Foo 类,基于以下 proto 文件:

package baeldung;
option java_package = "com.baeldung.web.dto";
option java_outer_classname = "FooProtos";
message Foo {
    required int64 id = 1;
    required string name = 2;
}

Spring 内置 Protocol Buffer 支持,只需添加 ProtobufHttpMessageConverter

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
    messageConverters.add(new MappingJackson2HttpMessageConverter());
    messageConverters.add(new KryoHttpMessageConverter());
    messageConverters.add(new ProtobufHttpMessageConverter());
}

但需注意:必须定义单独的控制器方法返回 FooProtos.Foo 实例(JSON/Kryo 都处理 Foo 类型,无需区分)。

解决方法有两种:

方案一:使用不同 URL

// Protobuf 接口
@RequestMapping(method = RequestMethod.GET, value = "/fooprotos/{id}")
@ResponseBody
public FooProtos.Foo findProtoById(@PathVariable long id) { … }

// 其他格式接口
@RequestMapping(method = RequestMethod.GET, value = "/foos/{id}")
@ResponseBody
public Foo findById(@PathVariable long id) { … }

方案二:使用相同 URL + 显式指定格式(推荐)

@RequestMapping(
  method = RequestMethod.GET, 
  value = "/foos/{id}", 
  produces = { "application/x-protobuf" })
@ResponseBody
public FooProtos.Foo findProtoById(@PathVariable long id) { … }

关键点:通过 produces 属性指定媒体类型,Spring 会根据客户端 Accept 头自动选择匹配的方法。这种方式能提供统一一致的 REST API。

深入学习:参考 Protocol Buffers 与 Spring REST API 集成指南

5. 注册额外消息转换器

重要提醒:重写 configureMessageConverters 会丢失所有默认转换器,仅保留显式注册的转换器。

虽然有时这是预期行为,但通常只需添加新转换器同时保留默认转换器(它们已处理 JSON 等标准格式)。此时应重写 extendMessageConverters 方法:

@Configuration
@EnableWebMvc
@ComponentScan({ "com.baeldung.web" })
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        messageConverters.add(new ProtobufHttpMessageConverter());
        messageConverters.add(new KryoHttpMessageConverter());
    }
}

6. 总结

本文展示了在 Spring MVC 中使用任意数据传输格式的便捷性,并以 Kryo 为例进行了实践演示。同时还实现了多格式支持,使不同客户端能按需选择数据格式。

完整实现代码可在 GitHub 项目 中获取,这是一个基于 Maven 的项目,可直接导入运行。


原始标题:Binary Data Formats in a Spring REST API

» 下一篇: JMockit 101