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

1. Обзор

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

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

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

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

2. HttpMessageConverter

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

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

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

Для проверки последней версии kryo вы можете использовать a Смотри сюда .

3.2. Kryo in Spring ОТДЫХ

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

application/х-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 в список. В этом примере мы предполагаем, что существует класс FooProtos.Foo , сгенерированный компилятором protoc на основе следующего файла proto :

package baeldung;
option java__package = "org.baeldung.web.dto";
option java__outer__classname = "FooProtos";
message Foo {
    required int64 id = 1;
    required string name = 2;
}

Spring поставляется с некоторой встроенной поддержкой протокола 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 __s, поэтому никаких изменений в контроллере для их различения не требуется).

Есть два способа устранить неоднозначность того, какой метод вызывается. Первый подход заключается в использовании разных 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 для всех форматов данных.

Наконец, если вы хотите углубиться в использование буферов протокола с Spring REST API, посмотрите ссылку:/spring-rest-api-with-protocol-buffers[справочная статья]

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

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

Будут использованы только те, которые вы предоставите.

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

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

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

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