Spring REST API с буфером протокола

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 должен начинаться с объявления syntax = «proto3» . Если используется версия компилятора 2, это объявление будет опущено. Затем следует объявление package , которое является пространством имен для этой структуры сообщений, чтобы избежать конфликтов имен с другими проектами.

Следующие два объявления используются только для Java: параметр java package указывает пакет для наших сгенерированных классов, в котором можно жить, а параметр java outer classname указывает имя класса, охватывающего все типы, определенные в этом файле .proto__.

В подразделе 2.3 ниже будут описаны остальные элементы и их компиляция в код Java.

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

После того, как структура сообщения определена, нам нужен компилятор для преобразования этого не зависящего от языка контента в код Java. Вы можете следовать инструкциям в Protocol Buffers репозиторий , чтобы получить соответствующую версию компилятора. В качестве альтернативы вы можете загрузить предварительно собранный двоичный компилятор из центрального репозитория Maven, выполнив поиск по адресу https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22com.google.protobuf%22 . % 20AND% 20a% 3A% 22protoc% 22[ com.google.protobuf: protoc ]артефакт, затем выберите подходящую версию для вашей платформы.

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

protoc --java__out=java resources/baeldung.proto

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

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

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

Мы можем использовать другую версию среды выполнения при условии, что она совпадает с версией компилятора. Чтобы ознакомиться с последней версией, посетите страницу https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22com.google.protobuf%22%20AND%20a%3A%22protobuf-java%22 .[эта ссылка].

2.3. Составление описания сообщения

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

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

Давайте начнем с сообщения Course . У него есть два простых поля, включая id и course name . Их типы буферов протокола int32 и string преобразуются в типы Java int и String__. Вот их связанные геттеры после компиляции (реализации для краткости опущены):

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

Обратите внимание, что имена типизированных полей должны быть в виде змеи (отдельные слова разделены символами подчеркивания), чтобы поддерживать взаимодействие с другими языками. Компилятор преобразует эти имена в регистр верблюдов в соответствии с соглашениями Java.

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

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

Теперь мы перейдем к сообщению Student , которое используется как сложный тип поля student сообщения Course . Его простые поля, включая id , first name , last name и email , используются для создания методов доступа Java:

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

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

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

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

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

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

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

3. Protobuf In Spring REST API

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

3.1. Бобовая декларация

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

@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
}

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

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

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

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

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

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

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

@RestController
public class CourseController {
    @Autowired
    CourseRepository courseRepo;

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

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

4. REST Клиенты и Тестирование

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

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

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

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

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

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

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

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

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

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

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 в указанное место назначения, получаем ответ в виде сообщений буфера протокола и проверяем его с помощью API RestTemplate :

@Autowired
private RestTemplate restTemplate;

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

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

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

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

4.3. Тестирование с HttpClient

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

Последние версии этих зависимостей см. На странице https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22com.googlecode.protobuf-java-format%22%20AND% . 20a% 3A% 22protobuf-java-format% 22[protobuf-java-format]и https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org.apache.httpcomponents%22% 20AND% 20a% 3A% 22httpclient% 22[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. Заключение

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

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