1. 概述
協議緩衝區 (Protocol Buffers) 是一種語言和平台無關的數據序列化和反序列化機制,由其創建者 Google 宣稱其比 XML 和 JSON 等其他類型的報負載載更快、更小、更簡單。
本教程將引導您設置一個 REST API,以利用這種基於二進制的消息結構。
2. Protocol Buffers
本節介紹 Protocol Buffers 的基本信息以及它們在 Java 生態系統中的應用。
2.1. 協議緩衝區簡介
為了使用協議緩衝區,我們需要在 <em>.proto</em> 文件中定義消息結構。每個文件都描述了可能從一個節點傳輸到另一個節點,或存儲在數據源中的數據。以下是一個 <em>.proto</em> 文件的示例,名為 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;
}
}在本教程中,我們使用 protocol buffer 編譯器和 protocol buffer 語言的 3 版本,因此 .proto 文件必須以 syntax = “proto3” 聲明開頭。如果使用編譯器版本 2,則會省略此聲明。接下來是 package 聲明,它為該消息結構提供命名空間,以避免與其他項目中的命名衝突。
以下兩個聲明僅用於 Java:java_package 選項指定生成的類所在的包,java_outer_classname 選項指示包含該 .proto 文件中所有類型定義的類的名稱。
第 2.3 小節將描述剩餘的元素以及它們如何編譯為 Java 代碼。
2.2. 使用 Java 進行 Protocol Buffers
在消息結構定義完成後,我們需要一個編譯器將該語言中立內容轉換為 Java 代碼。請按照 Protocol Buffers 倉庫 中的説明獲取合適的編譯器版本。 另一種方法是,您可以在 Maven 中央倉庫中下載預構建的二進制編譯器,通過搜索 com.google.protobuf:protoc 藝術品,然後選擇適合您平台的適當版本。
接下來,將編譯器複製到您項目的 src/main 目錄,然後在命令行中執行以下命令:
protoc --java_out=java resources/baeldung.proto這應生成 BaeldungTraining 類及其在 com.baeldung.protobuf 包中的源文件,如 option 聲明中所指定,這些聲明位於 baeldung.proto 文件中。
除了編譯器之外,還需要 Protocol Buffers 運行時環境。可以通過在 Maven POM 文件中添加以下依賴來實現:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.25.3</version>
</dependency>我們可能會使用其他版本的運行時,前提是它與編譯器的版本相同。欲瞭解最新版本,請查看 此鏈接。
2.3. 編譯消息描述
通過使用編譯器,<em class="proto">.proto</em> 文件中的消息會被編譯成靜態嵌套的 Java 類。 在上面的示例中,<em class="proto">Course</em> 和 <em class="proto">Student</em> 消息被分別轉換為 <em class="java">Course</em> 和 <em class="java">Student</em> Java 類。 同時,消息的字段會被編譯成生成類型內的 JavaBeans 風格的 getter 和 setter 方法。 每個字段聲明末尾的標記(由等號和數字組成)是用於在二進制形式中編碼相關字段的唯一標識符。
我們將逐步瞭解消息的類型字段,以瞭解它們是如何轉換為訪問器方法。
讓我們從 <em class="proto">Course</em> 消息開始。 它有兩個簡單的字段,包括 <em class="proto">id</em> 和 <em class="proto">course_name</em>。 它們的 Protocol Buffer 類型 <em class="proto">int32</em> 和 <em class="proto">string</em> 被翻譯成 Java 的 <em class="java">int</em> 和 <em class="java">String</em> 類型。 它們的關聯 getter 方法在編譯後如下所示(為了簡潔,此處省略了實現):
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 消息,它被用作 Course 消息的 student 字段的複雜類型。其簡單字段,包括 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 嵌套類型,其中包含兩個與消息字段對應的 getter。
public java.lang.String getNumber();
public com.baeldung.protobuf.BaeldungTraining.Student.PhoneType getType();PhoneType,是 PhoneNumber 消息的 type 字段的複雜類型,是一個枚舉類型,將被轉換為一個嵌套在 BaeldungTraining.Student 類中的 Java enum 類型:
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 聲明
讓我們從定義我們的主要 <em @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註解方法返回的響應轉換為協議緩衝區消息。
另一個 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. 控制器配置
我們可以通過以下方式定義測試 URL 的 <em @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 消息,然後再被傳輸回客户端。
4. REST 客户端和測試
現在我們已經瞭解了簡單的 API 實現——現在讓我們演示客户端上 Protocol Buffer 消息的解串,使用兩種方法。
第一種方法利用了 RestTemplate API 和預配置的 ProtobufHttpMessageConverter Bean,自動將消息轉換為目標格式。
第二種方法是使用 protobuf-java-format 手動將 Protocol Buffer 響應轉換為 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 請求,接收以 Protocol Buffer 消息形式的響應,並使用 RestTemplate API 進行驗證:
@Autowired
private RestTemplate restTemplate;
@Test
public void whenUsingRestTemplate_thenSucceed() {
ResponseEntity<Course> course = restTemplate.getForEntity(COURSE1_URL, Course.class);
assertResponse(course.toString());
}為了使此測試用例生效,我們需要在配置類中註冊一個類型的 RestTemplate 類型的 Bean:
@Bean
RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
return new RestTemplate(Arrays.asList(hmc));
}另一個 <em ProtobufHttpMessageConverter</em> 類型的 Bean 也需要被配置,以自動轉換接收到的 Protocol Buffer 消息。這個 Bean 與子章節 3.1 中定義的 Bean 相同。由於本教程中的客户端和服務器共享相同的應用程序上下文,因此我們可以將 <em RestTemplate</em> Bean 聲明在 <em Application</em> 類中,並重用 <em ProtobufHttpMessageConverter</em> Bean。
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.14</version>
</dependency>請查閲這些依賴項的最新版本:protobuf-java-format 和 httpclient 在 Maven 中央倉庫中的 Artifacts。
現在,讓我們創建一個客户端,執行一個 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. 結論
本教程快速介紹了 Protocol Buffers 並演示了使用 Spring 框架設置 REST API 的方法。隨後,我們轉向了客户端支持以及序列化-反序列化機制。