Статьи

Использование буферов протокола Google с REST-сервисами на основе Spring MVC


Автор Джош Лонг в весеннем блоге 

На этой неделе я нахожусь в Сан-Паулу, Бразилия, на QCon SP. У меня была интересная дискуссия с кем-то, кто любит REST-стек Spring, но задавался вопросом, есть ли что-то более эффективное, чем обычный JSON. Действительно, есть! Меня часто спрашивают о поддержке Spring высокоскоростного двоичного кодирования сообщений. Давно поддерживаемая RPC-кодировка в Spring с помощью Hessian, Burlap и т. Д., А в Spring Framework 4.1 появилась поддержка  буферов протокола Google,  которые также можно использовать со службами REST.

С веб-сайта буфера протокола Google:

Буферы протокола — это независимый от языка, платформенно-независимый, расширяемый механизм Google для сериализации структурированных данных — думайте XML, но меньше, быстрее и проще. Вы определяете, как вы хотите, чтобы ваши данные были структурированы один раз, а затем вы можете использовать специальный сгенерированный исходный код, чтобы легко записывать и считывать ваши структурированные данные в различные потоки данных и из них, используя различные языки…

Google широко использует Protocol Buffers в своей собственной внутренней сервис-ориентированной архитектуре.

.proto Документ описывает типы (_messages_) должны быть закодированы и содержит язык описания , которые должны быть знакомы любому , кто использовал C  structs. В документе вы определяете типы, поля в этих типах и их порядок (смещение памяти!) В типе относительно друг друга.

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

Вы можете использовать буфер протокола Google в любом случае, но в этом посте мы рассмотрим его использование в качестве способа кодирования полезных нагрузок службы REST. Этот подход эффективен: вы можете использовать согласование контента для обслуживания высокоскоростных полезных нагрузок буфера протокола для клиентов (на любом количестве языков), которые его принимают, и чего-то более традиционного, например JSON, для тех, кто этого не делает.

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

Протокол Buffers — это несколько приятных функций, которые продвигают стабильные API:

  • Протокол буфера предлагает обратную совместимость бесплатно. Каждое поле пронумеровано в буфере протокола, поэтому вам не нужно менять поведение кода в дальнейшем, чтобы поддерживать обратную совместимость со старыми клиентами. Клиенты, которые не знают о новых полях, не будут пытаться разобрать их.
  • Протокол Буфера обеспечивают естественное место для указания проверки с использованием  required, optionalи  repeated ключевых слов. Каждый клиент применяет эти ограничения по-своему.
  • Протоколные буферы являются полиглотами и  работают со всеми видами технологий . В примере кода только для этого блога показан клиент Ruby, Python и Java для службы Java. Это просто вопрос использования одного из  многочисленных  поддерживаемых компиляторов.

Вы можете подумать, что можете просто использовать встроенный механизм сериализации Java в однородной сервисной среде, но, поскольку команда Protocol Buffers поспешила указать, когда они впервые представили технологию, есть некоторые проблемы даже с этим. Светильник языка Java В эпическом томе Джоша Блоха «  Эффективная Java» на стр. 213 содержатся дополнительные сведения.

Давайте сначала посмотрим на наш  .proto документ:

package demo;

option java_package = "demo";
option java_outer_classname = "CustomerProtos";

message Customer {
    required int32 id = 1;
    required string firstName = 2;
    required string lastName = 3;

    enum EmailType {
        PRIVATE = 1;
        PROFESSIONAL = 2;
    }

    message EmailAddress {
        required string email = 1;
        optional EmailType type = 2 [default = PROFESSIONAL];
    }

    repeated EmailAddress email = 5;
}

message Organization {
    required string name = 1;
    repeated Customer customer = 2;
}

Затем вы передаете это определение  protoc компилятору и указываете тип вывода, например так:

protoc -I=$IN_DIR --java_out=$OUT_DIR $IN_DIR/customer.proto

Вот небольшой скрипт Bash, который я собрал для генерации кода для моих различных клиентов:

#!/usr/bin/env bash


SRC_DIR=`pwd`
DST_DIR=`pwd`/../src/main/

echo source:            $SRC_DIR
echo destination root:  $DST_DIR

function ensure_implementations(){

    # Ruby and Go aren't natively supported it seems
    # Java and Python are

    gem list | grep ruby-protocol-buffers || sudo gem install ruby-protocol-buffers
    go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
}

function gen(){
    D=$1
    echo $D
    OUT=$DST_DIR/$D
    mkdir -p $OUT
    protoc -I=$SRC_DIR --${D}_out=$OUT $SRC_DIR/customer.proto
}

ensure_implementations

gen java
gen python
gen ruby

Это создаст соответствующие клиентские классы в  src/main/{java,ruby,python}папках. Давайте сначала посмотрим на сам сервис Spring MVC REST.

org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter. Этот тип является  HttpMessageConverterHttpMessageConverters кодировать и декодировать запросы и ответы в вызовах службы REST. Обычно они активируются после какого-либо согласования содержимого: например, если клиент указывает  Accept: application/x-protobuf, то наша служба REST отправит обратно ответ, закодированный с использованием буфера протокола.

package demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }

    private CustomerProtos.Customer customer(int id, String f, String l, Collection<String> emails) {
        Collection<CustomerProtos.Customer.EmailAddress> emailAddresses =
                emails.stream().map(e -> CustomerProtos.Customer.EmailAddress.newBuilder()
                        .setType(CustomerProtos.Customer.EmailType.PROFESSIONAL)
                        .setEmail(e).build())
                        .collect(Collectors.toList());

        return CustomerProtos.Customer.newBuilder()
                .setFirstName(f)
                .setLastName(l)
                .setId(id)
                .addAllEmail(emailAddresses)
                .build();
    }

    @Bean
    CustomerRepository customerRepository() {
        Map<Integer, CustomerProtos.Customer> customers = new ConcurrentHashMap<>();
        // populate with some dummy data
        Arrays.asList(
                customer(1, "Chris", "Richardson", Arrays.asList("crichardson@email.com")),
                customer(2, "Josh", "Long", Arrays.asList("jlong@email.com")),
                customer(3, "Matt", "Stine", Arrays.asList("mstine@email.com")),
                customer(4, "Russ", "Miles", Arrays.asList("rmiles@email.com"))
        ).forEach(c -> customers.put(c.getId(), c));

        // our lambda just gets forwarded to Map#get(Integer)
        return customers::get;
    }

}

interface CustomerRepository {
    CustomerProtos.Customer findById(int id);
}


@RestController
class CustomerRestController {

    @Autowired
    private CustomerRepository customerRepository;

    @RequestMapping("/customers/{id}")
    CustomerProtos.Customer customer(@PathVariable Integer id) {
        return this.customerRepository.findById(id);
    }
}

Большая часть этого кода довольно проста. Это приложение Spring Boot. Spring Boot автоматически регистрирует  HttpMessageConverter bean-компоненты, поэтому нам нужно только определить  ProtobufHttpMessageConverter bean-компонент, и он настраивается соответствующим образом. В  @Configuration семена класса некоторые даты манекена и макет  CustomerRepository объекта. Я не буду воспроизводить тип Java для нашего буфера протокола  demo/CustomerProtos.java, здесь он представляет собой сгенерированный битовый код и код разбора; не все, что интересно читать. Одно удобство состоит в том, что реализация Java автоматически предоставляет   методы компоновщика для быстрого создания экземпляров этих типов в Java.

Генерируемые кодом типы являются тупыми  struct объектами. Они подходят для использования в качестве DTO, но не должны использоваться в качестве основы для вашего API. Вы  не  расширять их с помощью наследования Java , чтобы ввести новые функциональные возможности ; в любом случае это нарушит реализацию, и это плохая практика ООП. Если вы хотите, чтобы все было чище, просто оберните и адаптируйте их соответствующим образом, возможно, обрабатывая преобразование из объекта ORM в тип клиента Protocol Buffer в зависимости от ситуации в этой оболочке.

HttpMessageConverters также может быть использован с REST клиента Spring, тем  RestTemplate. Вот соответствующий модульный тест на языке Java:

package demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = DemoApplication.class)
@WebAppConfiguration
@IntegrationTest
public class DemoApplicationTests {

    @Configuration
    public static class RestClientConfiguration {

        @Bean
        RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
            return new RestTemplate(Arrays.asList(hmc));
        }

        @Bean
        ProtobufHttpMessageConverter protobufHttpMessageConverter() {
            return new ProtobufHttpMessageConverter();
        }
    }

    @Autowired
    private RestTemplate restTemplate;

    private int port = 8080;

    @Test
    public void contextLoaded() {

        ResponseEntity<CustomerProtos.Customer> customer = restTemplate.getForEntity(
                "http://127.0.0.1:" + port + "/customers/2", CustomerProtos.Customer.class);

        System.out.println("customer retrieved: " + customer.toString());

    }

}

Все работает так, как вы ожидаете, не только в Java и Spring, но также в Ruby и Python. Для полноты, вот простой клиент, использующий Ruby (типы клиентов опущены):

#!/usr/bin/env ruby

require './customer.pb'
require 'net/http'
require 'uri'

uri = URI.parse('http://localhost:8080/customers/3')
body = Net::HTTP.get(uri)
puts Demo::Customer.parse(body)

..и вот клиент в Python (типы клиентов опущены):

#!/usr/bin/env python

import urllib
import customer_pb2

if __name__ == '__main__':
    customer = customer_pb2.Customer()
    customers_read = urllib.urlopen('http://localhost:8080/customers/1').read()
    customer.ParseFromString(customers_read)
    print customer

Avro  или  Thrift , но ни одна из них не является настолько зрелой и укоренившейся, как буферы протокола. Вам также не обязательно использовать буфер протокола с REST. Вы можете подключить его к какой-либо службе RPC, если это ваш стиль. Клиентских реализаций почти столько же, сколько сборок для Cloud Foundry — так что вы можете запустить практически все на Cloud Foundry и наслаждаться одинаково высокоскоростным, согласованным обменом сообщениями во всех ваших сервисах!

Код для этого примера можно ознакомиться в Интернете , а также, так что не стесняйтесь , чтобы проверить это!

в блоге Pivotal . Я использую эти еженедельные _ish_ (ОК! ОК! — не так легко делать их так регулярно, как на  этой неделе весной, но до сих пор я не пропустил неделю! :-)) публикует как возможность сосредоточиться не на конкретном новом выпуске как таковом, а на применении Spring в обслуживании к некоторым случаям использования в сообществе, которые могут быть междисциплинарными или просто полезными, если на них будет обращено внимание , До сих пор мы рассматривали разные вещи — Vaadin, Activiti, 12-факторная конфигурация стиля приложения, более интеллектуальные вызовы между сервисами, Couchbase и многое другое и т. Д. — и у нас есть кое-что интересное, выстроенное в очередь, тоже , Однако мне было интересно, о чем вы еще хотите поговорить. Если у вас есть какие-то идеи о том, что вы хотели бы, чтобы они были освещены, или о вашей публикации в сообществе,  свяжитесь со мной в Twitter (@starbuxman)  или по электронной почте (jlong ​​[at] pivotal [dot] io). ). Я остаюсь, как всегда, к вашим услугам.