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 .