Перейти к основному содержимому

Двоичные форматы данных в Spring REST API

· 6 мин. чтения

1. Обзор

Хотя JSON и XML являются широко популярными форматами передачи данных, когда речь идет о REST API, они не единственные доступные варианты.

Существует много других форматов с различной степенью сериализации и размером сериализованных данных.

В этой статье мы рассмотрим, как настроить механизм Spring REST для использования двоичных форматов данных, что мы проиллюстрируем с помощью Kryo.

Кроме того, мы показываем, как поддерживать несколько форматов данных, добавляя поддержку буферов протокола Google.

2. HttpMessageConverter

Интерфейс HttpMessageConverter — это общедоступный API Spring для преобразования форматов данных REST.

Есть разные способы указать нужные преобразователи. Здесь мы реализуем WebMvcConfigurer и явно предоставляем преобразователи, которые мы хотим использовать, в переопределенном методе configureMessageConverters :

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

3. Крио

3.1. Обзор Kryo и Maven

Kryo — это формат двоичного кодирования, обеспечивающий хорошую скорость сериализации и десериализации и меньший размер передаваемых данных по сравнению с текстовыми форматами.

Хотя теоретически его можно использовать для передачи данных между различными типами систем, в первую очередь он предназначен для работы с компонентами Java.

Добавляем необходимые библиотеки Kryo со следующей зависимостью Maven:

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

Чтобы проверить последнюю версию крио , вы можете посмотреть здесь .

3.2. Крио весной ОТДЫХ

Чтобы использовать Kryo в качестве формата передачи данных, мы создаем собственный HttpMessageConverter и реализуем необходимую логику сериализации и десериализации. Кроме того, мы определяем наш собственный HTTP-заголовок для Kryo: 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. Поддержка нескольких форматов данных

Часто требуется обеспечить поддержку нескольких форматов данных для одной и той же службы. Клиенты указывают желаемые форматы данных в HTTP-заголовке Accept , и вызывается соответствующий преобразователь сообщений для сериализации данных.

Обычно вам просто нужно зарегистрировать другой конвертер, чтобы все заработало из коробки. 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 foreach;
option java_package = "com.foreach.web.dto";
option java_outer_classname = "FooProtos";
message Foo {
required int64 id = 1;
required string name = 2;
}

Spring поставляется с некоторой встроенной поддержкой буфера протокола. Все, что нам нужно, чтобы это заработало, это включить 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 и других форматов. Например, для 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 мы используем значение = «/fooprotos/{id}» , а для других форматов значение = «/foos/{id}» .

Второй и лучший подход — использовать тот же URL-адрес, но явно указать формат создаваемых данных в сопоставлении запроса для protobuf:

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

Обратите внимание, что, указав тип носителя в атрибуте аннотации, мы даем подсказку базовому механизму Spring о том, какое сопоставление необходимо использовать на основе значения в заголовке Accept , предоставленного клиентами, поэтому нет никакой двусмысленности в отношении того, какой метод должен быть использован. вызываться для URL- адреса «foos/{id}» .

Второй подход позволяет нам предоставлять клиентам единый и согласованный REST API для всех форматов данных.

Наконец, если вы заинтересованы в более подробном изучении использования Protocol Buffers с Spring REST API, ознакомьтесь со справочной статьей .

5. Регистрация дополнительных конвертеров сообщений

Очень важно отметить, что вы теряете все конвертеры сообщений по умолчанию, когда переопределяете метод configureMessageConverters . Будут использованы только те, которые вы предоставите.

Хотя иногда это именно то, что вам нужно, во многих случаях вы просто хотите добавить новые конвертеры, сохраняя при этом стандартные конвертеры, которые уже заботятся о стандартных форматах данных, таких как JSON. Для этого переопределите метод extendMessageConverters :

@Configuration
@EnableWebMvc
@ComponentScan({ "com.foreach.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.

Мы также показали, как добавить поддержку нескольких форматов, чтобы разные клиенты могли использовать разные форматы.

Реализация этих форматов двоичных данных в учебном пособии Spring REST API , конечно же, находится на Github . Это проект на основе Maven, поэтому его легко импортировать и запускать как есть.