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 .