Перейти к основному содержимому

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

· 9 мин. чтения

Задача: Наибольшая подстрока палиндром

Для заданной строки s, верните наибольшую подстроку палиндром входящую в s. Подстрока — это непрерывная непустая последовательность символов внутри строки. Стока является палиндромом, если она читается одинаково в обоих направлениях...

ANDROMEDA 42

1. Обзор

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

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

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

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

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

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

syntax = "proto3";
package foreach;
option java_package = "com.foreach.protobuf";
option java_outer_classname = "ForEachTraining";

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/foreach.proto

Это должно сгенерировать исходный файл для класса ForEachTraining в пакете com.foreach.protobuf , как указано в объявлениях параметров файла foreach.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 внутри этих сгенерированных типов. Маркер, состоящий из знака равенства и числа, в конце объявления каждого поля представляет собой уникальный тег, используемый для кодирования связанного поля в двоичной форме.

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

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

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

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

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

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

Теперь мы перейдем к сообщению « Студент », которое используется как сложный тип поля « Студент » в сообщении « Курс » . Его простые поля, включая 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 . Подобно полю студента в сообщении курса , это поле повторяется и имеет несколько связанных методов:

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

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

public java.lang.String getNumber();
public com.foreach.protobuf.ForEachTraining.Student.PhoneType getType();

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

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

3. Protobuf в 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.

Здесь важно то, что мы работаем со специфическими данными Protocol Buffer, а не со стандартными 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);
}
}

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

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

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

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

Второй — использование формата protobuf-java для ручного преобразования ответов буфера протокола в документы 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("john.doe@foreach.com"));
assertThat(response, containsString("richard.roe@foreach.com"));
assertThat(response, containsString("jane.doe@foreach.com"));
assertThat(response, containsString("phone"));
assertThat(response, containsString("number"));
assertThat(response, containsString("type"));
}

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

4.2. Тестирование с RestTemplate

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

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

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

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

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

<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: "john.doe@foreach.com"
phone {
number: "123456"
}
}
student {
id: 2
first_name: "Richard"
last_name: "Roe"
email: "richard.roe@foreach.com"
phone {
number: "234567"
type: LANDLINE
}
}
student {
id: 3
first_name: "Jane"
last_name: "Doe"
email: "jane.doe@foreach.com"
phone {
number: "345678"
}
phone {
number: "456789"
type: LANDLINE
}
}

5. Вывод

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

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