«1. Обзор

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

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

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

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

2. HttpMessageConverter

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

Есть разные способы указать нужные преобразователи. Здесь мы реализуем 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.

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

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

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

3.2. Kryo в Spring REST

Чтобы использовать 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 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 работают с Foos, поэтому никаких изменений в контроллере не требуется, чтобы различить их).

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

@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());
    }
}

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

6. Заключение

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

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