1. 概述
Protocol Buffers 是一種語言和平台中立的序列化和反序列化結構化數據的機制,由其創建者 Google 宣稱其比 XML 和 JSON 等其他類型的負載更快、更小、更簡單。
本教程將指導您設置 REST API,以利用這種基於二進制的消息結構。
2. Protocol Buffers
This section gives some basic information on Protocol Buffers and how they are applied in the Java ecosystem.
2.1. Introduction to Protocol Buffers
In order to make use of Protocol Buffers, we need to define message structures in .proto files. Each file is a description of the data that might be transferred from one node to another, or stored in data sources. Here is an example of .proto files, which is named baeldung.proto and lives in the src/main/resources directory. This file will be used in this tutorial later on:
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;
}
}
In this tutorial, we use version 3 of both protocol buffer compiler and protocol buffer language, therefore the .proto file must start with the syntax = “proto3” declaration. If a compiler version 2 is in use, this declaration would be omitted. Next comes the package declaration, which is the namespace for this message structure to avoid naming conflicts with other projects.
The following two declarations are used for Java only: java_package option specifies the package for our generated classes to live in, and java_outer_classname option indicates name of the class enclosing all the types defined in this .proto file.
Subsection 2.3 below will describe the remaining elements and how those are compiled into Java code.
2.2. Protocol Buffers With Java
After a message structure is defined, we need a compiler to convert this language neutral content to Java code. You can follow the instructions in the Protocol Buffers repository in order to get an appropriate compiler version. Alternatively, you may download a pre-built binary compiler from the Maven central repository by searching for the com.google.protobuf:protoc artifact, then picking up an appropriate version for your platform.
Next, copy the compiler to the src/main directory of your project and execute the following command in the command line:
protoc --java_out=java resources/baeldung.proto
This should generate a source file for the BaeldungTraining class within the com.baeldung.protobuf package, as specified in the option declarations of the baeldung.proto file.
In addition to the compiler, Protocol Buffers runtime is required. This can be achieved by adding the following dependency to the Maven POM file:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.25.3</version>
</dependency>
We may use another version of the runtime, provided that it is the same as the compiler’s version. For the latest one, please check out this link.
2.3. Compiling a Message Description
By using a compiler, messages in a .proto file are compiled into static nested Java classes. In the above example, the Course and Student messages are converted to Course and Student Java classes, respectively. At the same time, messages’ fields are compiled into JavaBeans style getters and setters inside those generated types. The marker, composed of an equal sign and a number, at the end of each field declaration is the unique tag used to encode the associated field in the binary form.
We will walk through typed fields of the messages to see how those are converted to accessor methods.
Let’s start with the Course message. It has two simple fields, including id and course_name. Their protocol buffer types, int32 and string, are translated into Java int and String types. Here are their associated getters after compilation (with implementations being left out for brevity):
public int getId();
public java.lang.String getCourseName();
Note that names of typed fields should be in snake case (individual words are separated by underscore characters) to maintain the cooperation with other languages. The compiler will convert those names to camel case according to Java conventions.
The last field of Course message, student, is of the Student complex type, which will be described below. This field is prepended by the repeated keyword, meaning that it may be repeated any number of times. The compiler generates some methods associated with the student field as follows (without implementations):
public java.util.List<com.baeldung.protobuf.BaeldungTraining.Student> getStudentList();
public int getStudentCount();
public com.baeldung.protobuf.BaeldungTraining.Student getStudent(int index);
Now we will move on to the Student message, which is used as complex type of the student field of Course message. Its simple fields, including id, first_name, last_name and email are used to create Java accessor methods:
public int getId();
public java.lang.String getFirstName();
public java.lang.String getLastName();
public java.lang.String.getEmail();
The last field, phone, is of the PhoneNumber complex type. Similar to the student field of Course message, this field is repetitive and has several associated methods:
public java.util.List<com.baeldung.protobuf.BaeldungTraining.Student.PhoneNumber> getPhoneList();
public int getPhoneCount();
public com.baeldung.protobuf.BaeldungTraining.Student.PhoneNumber getPhone(int index);
The PhoneNumber message is compiled into the BaeldungTraining.Student.PhoneNumber nested type, with two getters corresponding to the message’s fields:
public java.lang.String getNumber();
public com.baeldung.protobuf.BaeldungTraining.Student.PhoneType getType();
PhoneType, the complex type of the type field of the PhoneNumber message, is an enumeration type, which will be transformed into a Java enum type nested within the BaeldungTraining.Student class:
public enum PhoneType implements com.google.protobuf.ProtocolMessageEnum {
MOBILE(0),
LANDLINE(1),
UNRECOGNIZED(-1),
;
// Other declarations
}
3. Protobuf 在 Spring REST API 中
本節將指導您設置使用 Spring Boot 的 REST 服務。
3.1. Bean 聲明
讓我們從定義我們的主要 @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 bean 用於將由 @RequestMapping 註解的方法返回的響應轉換為 Protocol Buffer 消息。
另一個 bean,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 的 @Controller 類:
@RestController
public class CourseController {
@Autowired
CourseRepository courseRepo;
@RequestMapping("/courses/{id}")
Course customer(@PathVariable Integer id) {
return courseRepo.getCourse(id);
}
}
而且 – 重要的是,我們返回的 Course DTO 不是標準的 POJO。 這將觸發將其轉換為 Protocol Buffer 消息然後再傳輸回客户端。
– using two methods.
The first one takes advantage of the bean to automatically convert messages.
The second is using class by declaring a test class as follows:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebIntegrationTest
public class ApplicationTest {
// Other declarations
}
All code snippets in this section will be placed in the private static final String COURSE1_URL = "http://localhost:8080/courses/1";
This 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"));
}
We will make use of this helper method in both test cases covered in the succeeding sub-sections.
Here is how we create a client, send a GET request to the designated destination, receive the response in the form of protocol buffer messages and verify it using the @Autowired
private RestTemplate restTemplate;
@Test
public void whenUsingRestTemplate_thenSucceed() {
ResponseEntity<Course> course = restTemplate.getForEntity(COURSE1_URL, Course.class);
assertResponse(course.toString());
}
To make this test case work, we need a bean of the @Bean
RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
return new RestTemplate(Arrays.asList(hmc));
}
Another bean of the bean in the bean.
The first step to use the <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.14</version>
</dependency>
For the latest versions of these dependencies, please have a look at protobuf-java-format and httpclient artifacts in Maven central repository.
Let’s move on to create a client, execute a GET request and convert the associated response to an 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();
}
Now, we will convert protocol buffer messages in the form of an private String convertProtobufMessageStreamToJsonString(InputStream protobufStream) throws IOException {
JsonFormat jsonFormat = new JsonFormat();
Course course = Course.parseFrom(protobufStream);
return jsonFormat.printToString(course);
}
And here is how a test case uses private helper methods declared above and validates the response:
@Test
public void whenUsingHttpClient_thenSucceed() throws IOException {
InputStream responseStream = executeHttpRequest(COURSE1_URL);
String jsonOutput = convertProtobufMessageStreamToJsonString(responseStream);
assertResponse(jsonOutput);
}
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. 結論
本教程快速介紹了 Protocol Buffers 並演示了使用 Spring 格式設置 REST API 的方法。然後,我們轉向了客户端支持和序列化-反序列化機制。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebIntegrationTest
public class ApplicationTest {
// Other declarations
}
This 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"));
}
We will make use of this helper method in both test cases covered in the succeeding sub-sections. To make this test case work, we need a bean of the @Bean
RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
return new RestTemplate(Arrays.asList(hmc));
}
Another bean of the bean in the bean. For the latest versions of these dependencies, please have a look at protobuf-java-format and httpclient artifacts in Maven central repository. Let’s move on to create a client, execute a GET request and convert the associated response to an 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();
}
Now, we will convert protocol buffer messages in the form of an private String convertProtobufMessageStreamToJsonString(InputStream protobufStream) throws IOException {
JsonFormat jsonFormat = new JsonFormat();
Course course = Course.parseFrom(protobufStream);
return jsonFormat.printToString(course);
}
And here is how a test case uses private helper methods declared above and validates the response: 本教程快速介紹了 Protocol Buffers 並演示了使用 Spring 格式設置 REST API 的方法。然後,我們轉向了客户端支持和序列化-反序列化機制。Here is how we create a client, send a GET request to the designated destination, receive the response in the form of protocol buffer messages and verify it using the @Autowired
private RestTemplate restTemplate;
@Test
public void whenUsingRestTemplate_thenSucceed() {
ResponseEntity<Course> course = restTemplate.getForEntity(COURSE1_URL, Course.class);
assertResponse(course.toString());
}
The first step to use the <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.14</version>
</dependency>
@Test
public void whenUsingHttpClient_thenSucceed() throws IOException {
InputStream responseStream = executeHttpRequest(COURSE1_URL);
String jsonOutput = convertProtobufMessageStreamToJsonString(responseStream);
assertResponse(jsonOutput);
}
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. 結論