«1. Обзор

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

Это руководство поможет вам настроить REST API, чтобы воспользоваться преимуществами этой двоичной структуры сообщений.

2. Буферы протоколов

В этом разделе представлена ​​основная информация о буферах протоколов и о том, как они применяются в экосистеме Java.

2.1. Введение в буферы протоколов

Чтобы использовать буферы протоколов, нам нужно определить структуры сообщений в файлах .proto. Каждый файл представляет собой описание данных, которые могут быть переданы с одного узла на другой или сохранены в источниках данных. Вот пример файлов .proto, который называется baeldung.proto и находится в каталоге src/main/resources. Этот файл будет использоваться в этом руководстве позже:

syntax = "proto3";
package baeldung;
option java_package = "com.baeldung.protobuf";
option java_outer_classname = "BaeldungTraining";

message Course {
    int32 id = 1;
    string course_name = 2;
    repeated Student student = 3;
}
message Student {
    int32 id = 1;
    string first_name = 2;
    string last_name = 3;
    string email = 4;
    repeated PhoneNumber phone = 5;
    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
    }
    enum PhoneType {
        MOBILE = 0;
        LANDLINE = 1;
    }
}

В этом руководстве мы используем версию 3 как компилятора буфера протокола, так и языка буфера протокола, поэтому файл .proto должен начинаться с синтаксиса = «proto3». декларация. Если используется компилятор версии 2, это объявление будет опущено. Далее идет объявление пакета, которое является пространством имен для этой структуры сообщения, чтобы избежать конфликтов имен с другими проектами.

Следующие два объявления используются только для Java: параметр java_package указывает пакет, в котором будут жить созданные нами классы, а параметр java_outer_classname указывает имя класса, содержащего все типы, определенные в этом файле .proto.

В подразделе 2.3 ниже описаны оставшиеся элементы и то, как они компилируются в код Java.

2.2. Буферы протоколов с Java

После того, как структура сообщения определена, нам нужен компилятор для преобразования этого независимого от языка содержимого в код Java. Вы можете следовать инструкциям в репозитории Protocol Buffers, чтобы получить соответствующую версию компилятора. Кроме того, вы можете загрузить готовый двоичный компилятор из центрального репозитория Maven, выполнив поиск артефакта com.google.protobuf:protoc, а затем выбрав подходящую версию для вашей платформы.

Затем скопируйте компилятор в каталог src/main вашего проекта и выполните следующую команду в командной строке:

protoc --java_out=java resources/baeldung.proto

Это должно сгенерировать исходный файл для класса BaeldungTraining в com.baeldung.protobuf. package, как указано в объявлениях опций файла baeldung.proto.

В дополнение к компилятору требуется среда выполнения Protocol Buffers. Этого можно добиться, добавив в POM-файл Maven следующую зависимость:

<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.0.0-beta-3</version>
</dependency>

Мы можем использовать другую версию среды выполнения при условии, что она совпадает с версией компилятора. Для последней, пожалуйста, проверьте эту ссылку.

2.3. Компиляция описания сообщения

С помощью компилятора сообщения в файле .proto компилируются в статические вложенные классы Java. В приведенном выше примере сообщения «Курс» и «Студент» преобразуются в классы «Курс» и «Студент» соответственно. В то же время поля сообщений компилируются в геттеры и сеттеры в стиле JavaBeans внутри этих сгенерированных типов. Маркер, состоящий из знака равенства и числа, в конце объявления каждого поля представляет собой уникальный тег, используемый для кодирования связанного поля в двоичной форме.

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

Начнем с сообщения Курса. Он имеет два простых поля, включая идентификатор и имя_курса. Их типы буферов протоколов, int32 и string, транслируются в типы Java int и String. Вот их связанные геттеры после компиляции (с реализациями, опущенными для краткости): . Компилятор преобразует эти имена в верблюжий регистр в соответствии с соглашениями Java.

public int getId();
public java.lang.String getCourseName();

«Последнее поле сообщения Курса, студент, имеет комплексный тип Студент, который будет описан ниже. Перед этим полем стоит ключевое слово Repeat, что означает, что оно может повторяться любое количество раз. Компилятор генерирует некоторые методы, связанные с полем студента, следующим образом (без реализации):

Теперь мы перейдем к сообщению Студента, которое используется как сложный тип поля студента сообщения Курса. Его простые поля, включая id, first_name, last_name и email, используются для создания методов доступа Java:

public java.util.List<com.baeldung.protobuf.BaeldungTraining.Student> getStudentList();
public int getStudentCount();
public com.baeldung.protobuf.BaeldungTraining.Student getStudent(int index);

Последнее поле, phone, имеет сложный тип PhoneNumber. Подобно полю студента в сообщении курса, это поле является повторяющимся и имеет несколько связанных методов:

public int getId();
public java.lang.String getFirstName();
public java.lang.String getLastName();
public java.lang.String.getEmail();

Сообщение PhoneNumber компилируется во вложенный тип BaeldungTraining.Student.PhoneNumber с двумя геттерами, соответствующими полям сообщения: ~ ~~

public java.util.List<com.baeldung.protobuf.BaeldungTraining.Student.PhoneNumber> getPhoneList();
public int getPhoneCount();
public com.baeldung.protobuf.BaeldungTraining.Student.PhoneNumber getPhone(int index);

PhoneType, сложный тип поля типа сообщения PhoneNumber, является типом перечисления, который будет преобразован в тип перечисления Java, вложенный в класс BaeldungTraining.Student:

public java.lang.String getNumber();
public com.baeldung.protobuf.BaeldungTraining.Student.PhoneType getType();

3. Protobuf в Spring REST API

public enum PhoneType implements com.google.protobuf.ProtocolMessageEnum {
    MOBILE(0),
    LANDLINE(1),
    UNRECOGNIZED(-1),
    ;
    // Other declarations
}

Этот раздел поможет вам настроить службу REST с помощью Spring Boot.

3.1. Объявление компонента

Давайте начнем с определения нашего основного @SpringBootApplication:

Компонент ProtobufHttpMessageConverter используется для преобразования ответов, возвращаемых аннотированными методами @RequestMapping, в сообщения буфера протокола.

@SpringBootApplication
public class Application {
    @Bean
    ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }

    @Bean
    public CourseRepository createTestCourses() {
        Map<Integer, Course> courses = new HashMap<>();
        Course course1 = Course.newBuilder()
          .setId(1)
          .setCourseName("REST with Spring")
          .addAllStudent(createTestStudents())
          .build();
        Course course2 = Course.newBuilder()
          .setId(2)
          .setCourseName("Learn Spring Security")
          .addAllStudent(new ArrayList<Student>())
          .build();
        courses.put(course1.getId(), course1);
        courses.put(course2.getId(), course2);
        return new CourseRepository(courses);
    }

    // Other declarations
}

Другой компонент, CourseRepository, содержит некоторые тестовые данные для нашего API.

Здесь важно то, что мы работаем со специфическими данными Protocol Buffer, а не со стандартными POJO.

Вот простая реализация CourseRepository:

3.2. Конфигурация контроллера

public class CourseRepository {
    Map<Integer, Course> courses;
    
    public CourseRepository (Map<Integer, Course> courses) {
        this.courses = courses;
    }
    
    public Course getCourse(int id) {
        return courses.get(id);
    }
}

Мы можем определить класс @Controller для тестового URL-адреса следующим образом:

И снова — здесь важно то, что DTO курса, который мы возвращаем из уровня контроллера, не является стандартный POJO. Это будет триггером для его преобразования в сообщения буфера протокола перед передачей обратно клиенту.

@RestController
public class CourseController {
    @Autowired
    CourseRepository courseRepo;

    @RequestMapping("/courses/{id}")
    Course customer(@PathVariable Integer id) {
        return courseRepo.getCourse(id);
    }
}

4. REST-клиенты и тестирование

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

Первый использует преимущества API RestTemplate с предварительно настроенным bean-компонентом ProtobufHttpMessageConverter для автоматического преобразования сообщений.

Второй использует формат protobuf-java для ручного преобразования ответов буфера протокола в документы JSON.

Для начала нам нужно настроить контекст для интеграционного теста и указать Spring Boot найти информацию о конфигурации в классе Application, объявив тестовый класс следующим образом:

Все фрагменты кода в этом разделе будут быть помещены в класс ApplicationTest.

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebIntegrationTest
public class ApplicationTest {
    // Other declarations
}

4.1. Ожидаемый ответ

Первым шагом для доступа к службе REST является определение URL-адреса запроса:

Этот COURSE1_URL будет использоваться для получения первого тестового двойного курса из службы REST, которую мы создали ранее. После отправки запроса GET на указанный выше URL-адрес соответствующий ответ проверяется с использованием следующих утверждений:

private static final String COURSE1_URL = "http://localhost:8080/courses/1";

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

private void assertResponse(String response) {
    assertThat(response, containsString("id"));
    assertThat(response, containsString("course_name"));
    assertThat(response, containsString("REST with Spring"));
    assertThat(response, containsString("student"));
    assertThat(response, containsString("first_name"));
    assertThat(response, containsString("last_name"));
    assertThat(response, containsString("email"));
    assertThat(response, containsString("[email protected]"));
    assertThat(response, containsString("[email protected]"));
    assertThat(response, containsString("[email protected]"));
    assertThat(response, containsString("phone"));
    assertThat(response, containsString("number"));
    assertThat(response, containsString("type"));
}

4.2. Тестирование с помощью RestTemplate

Вот как мы создаем клиент, отправляем запрос GET в указанное место назначения, получаем ответ в виде сообщений буфера протокола и проверяем его с помощью RestTemplate API:

Чтобы сделать это тестовый пример, нам нужно, чтобы bean-компонент типа RestTemplate был зарегистрирован в классе конфигурации:

@Autowired
private RestTemplate restTemplate;

@Test
public void whenUsingRestTemplate_thenSucceed() {
    ResponseEntity<Course> course = restTemplate.getForEntity(COURSE1_URL, Course.class);
    assertResponse(course.toString());
}

«


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

«Другой bean-компонент типа ProtobufHttpMessageConverter также требуется для автоматического преобразования полученных сообщений буфера протокола. Этот компонент такой же, как тот, который определен в подразделе 3.1. Поскольку в этом руководстве клиент и сервер используют один и тот же контекст приложения, мы можем объявить bean-компонент RestTemplate в классе Application и повторно использовать bean-компонент ProtobufHttpMessageConverter.

4.3. Тестирование с помощью HttpClient

Первым шагом к использованию HttpClient API и ручному преобразованию сообщений буфера протокола является добавление следующих двух зависимостей в файл POM Maven:

<dependency>
    <groupId>com.googlecode.protobuf-java-format</groupId>
    <artifactId>protobuf-java-format</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.2</version>
</dependency>

Последние версии этих зависимостей см. в protobuf-java-format и артефактах httpclient в центральном репозитории Maven.

Давайте перейдем к созданию клиента, выполнению запроса GET и преобразованию связанного ответа в экземпляр InputStream с использованием заданного URL-адреса:

private InputStream executeHttpRequest(String url) throws IOException {
    CloseableHttpClient httpClient = HttpClients.createDefault();
    HttpGet request = new HttpGet(url);
    HttpResponse httpResponse = httpClient.execute(request);
    return httpResponse.getEntity().getContent();
}

Теперь мы преобразуем сообщения буфера протокола в форму объекта InputStream. в документ JSON:

private String convertProtobufMessageStreamToJsonString(InputStream protobufStream) throws IOException {
    JsonFormat jsonFormat = new JsonFormat();
    Course course = Course.parseFrom(protobufStream);
    return jsonFormat.printToString(course);
}

А вот как тестовый пример использует частные вспомогательные методы, объявленные выше, и проверяет ответ:

@Test
public void whenUsingHttpClient_thenSucceed() throws IOException {
    InputStream responseStream = executeHttpRequest(COURSE1_URL);
    String jsonOutput = convertProtobufMessageStreamToJsonString(responseStream);
    assertResponse(jsonOutput);
}

4.4. Ответ в формате JSON

Чтобы было понятно, сюда включены ответы в формате JSON, которые мы получили в тестах, описанных в предыдущих подразделах:

id: 1
course_name: "REST with Spring"
student {
    id: 1
    first_name: "John"
    last_name: "Doe"
    email: "[email protected]"
    phone {
        number: "123456"
    }
}
student {
    id: 2
    first_name: "Richard"
    last_name: "Roe"
    email: "[email protected]"
    phone {
        number: "234567"
        type: LANDLINE
    }
}
student {
    id: 3
    first_name: "Jane"
    last_name: "Doe"
    email: "[email protected]"
    phone {
        number: "345678"
    }
    phone {
        number: "456789"
        type: LANDLINE
    }
}

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

В этом руководстве кратко представлен протокол. Buffers и иллюстрирует настройку REST API с использованием формата Spring. Затем мы перешли к поддержке клиентов и механизму сериализации-десериализации.


Реализацию всех примеров и фрагментов кода можно найти в проекте GitHub.