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
Kryo 是一個高性能、快速的序列化/反序列化庫,主要用於 Java 和 C++。它旨在提供比 Java 默認的 Serialization 機制(如 Java Serialization)更快的性能,尤其是在處理大型對象時。
Kryo 的核心優勢在於其基於字典的序列化方式。它使用一個字典來映射 Java 對象到其序列化的表示形式,從而避免了在序列化過程中進行冗餘的類型檢查和轉換。
主要特點:
- 高性能: Kryo 的序列化/反序列化速度通常比 Java 默認的 Serialization 機制快得多。
- 基於字典: 通過字典映射對象,減少了運行時類型檢查的開銷。
- 支持多種數據類型: Kryo 支持 Java 對象、基本數據類型、XML、JSON 等多種數據類型。
- 可定製性: Kryo 允許用户自定義序列化/反序列化策略,以滿足特定需求。
示例 (Java):
// 這是一個簡單的 Kryo 示例,用於序列化和反序列化一個 Person 對象。
// 注意:這裏只是一個示例,實際使用時需要根據具體情況進行調整。
import org.apache.kruger.kryo.serializers.JsonSerializer;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
// 序列化示例
Person person = new Person("John Doe", 30);
Kryo kryo = new Kryo();
kryo.register(Person.class);
byte[] serializedData = kryo.toBytes(person);
// 反序列化示例
Person deserializedPerson = kryo.fromBytes(serializedData);
System.out.println("Name: " + deserializedPerson.getName());
System.out.println("Age: " + deserializedPerson.getAge());
3.1. Kryo 概述與 Maven
Kryo 是一種二進制編碼格式,相比文本格式,它提供了更好的序列化和反序列化速度,以及更小的傳輸數據量。
雖然理論上它可以用於不同類型的系統之間的數據傳輸,但它主要設計用於與 Java 組件一起使用。
我們通過以下 Maven 依賴項添加了必要的 Kryo 庫:
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.0</version>
</dependency>要檢查 kryo 的最新版本,您可以在此處查看。
3.2. Kryo 在 Spring REST 中
為了利用 Kryo 作為數據傳輸格式,我們創建了一個自定義的 <em >HttpMessageConverter</em> 並實現了必要的序列化和反序列化邏輯。 此外,我們定義了自定義的 HTTP 頭部用於 Kryo:<em >application/x-kryo</em>。 下面是一個完整的簡化版工作示例,我們用於演示目的:
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 會根據 Accept 標頭中的值以及轉換器中聲明的媒體類型自動選擇合適的轉換器。
例如,要添加對 JSON 和 Kryo 的支持,請註冊 KryoHttpMessageConverter 和 MappingJackson2HttpMessageConverter:
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
messageConverters.add(new MappingJackson2HttpMessageConverter());
messageConverters.add(new KryoHttpMessageConverter());
super.configureMessageConverters(messageConverters);
}
現在,我們假設我們還想將 Google Protocol Buffer 添加到列表中。對於這個例子,我們假設有一個名為 FooProtos.Foo 的類,該類是使用 protoc 編譯器根據以下 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;
}<p>春天內置了對 Protocol Buffer 的一些支持。 只需要將 <em >ProtobufHttpMessageConverter</em> 添加到支持轉換器列表中,就能使其正常工作。</p>
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
messageConverters.add(new MappingJackson2HttpMessageConverter());
messageConverters.add(new KryoHttpMessageConverter());
messageConverters.add(new ProtobufHttpMessageConverter());
}然而,我們必須定義一個單獨的控制器方法,該方法返回 FooProtos.Foo 實例(protobuf 和 Kryo 都能處理 Foo,因此控制器無需進行區分)。
有以下兩種方法可以解決關於哪個方法被調用的歧義。第一種方法是使用不同的 URL 來區分 protobuf 和其他格式。例如,對於 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) { … }
請注意,對於 protobuf,我們使用 value = “/fooprotos/{id}”,而對於其他格式,我們使用 value = “/foos/{id}”。
更優的方案是使用相同的 URL,但明確在請求映射中指定生成的 protobuf 數據格式:
@RequestMapping(
method = RequestMethod.GET,
value = "/foos/{id}",
produces = { "application/x-protobuf" })
@ResponseBody
public FooProtos.Foo findProtoById(@PathVariable long id) { … }
請注意,通過在 produces 註解屬性中指定媒體類型,我們向底層 Spring 機制提供提示,以便根據客户端提供的 Accept 請求頭中的值,選擇合適的映射方法,從而消除對 “foos/{id}” URL 中需要調用的方法存在的歧義。
這種第二種方法使我們能夠為客户端提供一致且統一的 REST API,適用於所有數據格式。
如果您想更深入地瞭解使用 Protocol Buffers 與 Spring REST API 的結合,請參閲參考文章。
5. 註冊額外的消息轉換器
需要特別注意的是,當你覆蓋 <em >configureMessageConverters</em> 方法時,你將丟失所有默認的消息轉換器。 只有你提供的轉換器才會使用。
雖然在某些情況下,這正是你想要的,但在許多情況下,你可能只想添加新的轉換器,同時仍然保留默認的轉換器,這些轉換器可以處理標準數據格式,如 JSON。 要實現這一點,請覆蓋 <em >extendMessageConverters</em> 方法:
@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 作為示例進行了説明。
我們還展示瞭如何添加對多種格式的支持,以便不同的客户端能夠使用不同的格式。