gRPC 與 Spring Boot 簡介
1. 概述
gRPC 是一個高效能、開源的 RPC 框架,最初由 Google 開發。它有助於消除樣板代碼並連接資料中心內和跨資料中心的多語言服務。這個 API 是基於Protocol Buffers ,它提供了一個protoc
編譯器來產生不同支援語言的程式碼。
我們可以將 gRPC 視為 REST、SOAP 或 GraphQL 的替代方案,它建構在 HTTP/2 之上,以使用多工或流連接等功能。
在本教程中,我們將學習如何使用 Spring Boot 實現 gRPC 服務提供者和消費者。
2. 挑戰
首先,我們可以注意到Spring Boot 中沒有對 gRPC 的直接支援。僅支援 Protocol Buffer,這使得我們能夠實現基於 protobuf 的 REST 服務。因此,我們需要透過使用第三方函式庫或自己來應對一些挑戰來包含 gRPC:
- 平台相關的編譯器:
protoc
編譯器是平台相關的。因此,如果應在建置時產生存根,則建置會變得更加複雜且容易出錯。 - 依賴關係:我們的 Spring Boot 應用程式中需要相容的依賴關係。不幸的是,
protoc
for Java添加了javax.annotation.Generated
註釋,這迫使我們添加對舊Java EE Annotations for Java
庫的依賴項以進行編譯。 - 伺服器執行時:gRPC 服務提供者需要在伺服器內運作。 gRPC for Java專案提供了一個著色的 Netty ,我們需要將其包含在 Spring Boot 應用程式中或替換為 Spring Boot 已提供的伺服器。
- 訊息傳輸:Spring Boot 提供了不同的客戶端,例如
RestClient
(阻塞)或WebClient
(非阻塞),遺憾的是這些客戶端無法設定並用於 gRPC,因為 gRPC 對阻塞和非阻塞呼叫都使用自訂傳輸技術。 - 配置:由於 gRPC 帶來了自己的技術,因此我們需要配置屬性來以 Spring Boot 方式配置它們。
3. 範例項目
幸運的是,我們可以使用第三方 Spring Boot Starters 來應對挑戰,例如LogNet或grpc 生態系統專案中的一個。兩種啟動器都很容易集成,但後者俱有提供者和消費者支援以及許多其他集成功能,因此我們選擇它作為範例。
在此範例中,我們僅使用單一 Proto 檔案設計一個簡單的 HelloWorld API :
syntax = "proto3";
option java_package = "com.baeldung.helloworld.stubs";
option java_multiple_files = true;
message HelloWorldRequest {
// a name to greet, default is "World"
optional string name = 1;
}
message HelloWorldResponse {
string greeting = 1;
}
service HelloWorldService {
rpc SayHello(stream HelloWorldRequest) returns (stream HelloWorldResponse);
}
正如我們所看到的,我們使用雙向流功能。
3.1. gRPC 存根
因為提供者和消費者的存根是相同的,所以我們在一個單獨的、獨立於 Spring 的專案中產生它們。這樣做的好處是,專案的生命週期(包括protoc
編譯器配置和 Java 依賴的 Java EE Annotations)可以與 Spring Boot 專案的生命週期隔離。
3.2.服務提供者
實現服務提供者非常容易。首先,我們需要新增啟動器和存根專案的依賴項:
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>2.15.0.RELEASE</version>
</dependency>
<dependency>
<groupId>com.baeldung.spring-boot-modules</groupId>
<artifactId>helloworld-grpc-java</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
不需要包含 Spring MVC 或 WebFlux,因為啟動器依賴項帶來了著色的 Netty 伺服器。我們可以在application.yml
中進行配置,例如透過配置伺服器連接埠:
grpc:
server:
port: 9090
然後,我們需要實作該服務並使用@GrpcService
進行註解:
@GrpcService
public class HelloWorldController extends HelloWorldServiceGrpc.HelloWorldServiceImplBase {
@Override
public StreamObserver<HelloWorldRequest> sayHello(
StreamObserver<HelloWorldResponse> responseObserver
) {
// ...
}
}
3.3.服務消費者
對於服務使用者,我們需要將相依性新增至啟動器和存根:
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-client-spring-boot-starter</artifactId>
<version>2.15.0.RELEASE</version>
</dependency>
<dependency>
<groupId>com.baeldung.spring-boot-modules</groupId>
<artifactId>helloworld-grpc-java</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
然後,我們在application.yml
中設定與服務的連線:
grpc:
client:
hello:
address: localhost:9090
negotiation-type: plaintext
“hello”
這個名字是一個自訂的名字。這樣,我們就可以設定多個連接,並在將 gRPC 用戶端注入 Spring 元件時引用該名稱:
@GrpcClient("hello")
HelloWorldServiceGrpc.HelloWorldServiceStub stub;
4. 陷阱
使用 Spring Boot 實作和使用 gRPC 服務非常簡單。但我們應該注意一些陷阱。
4.1. SSL握手
透過 HTTP 傳輸資料意味著發送未加密的訊息,除非我們使用 SSL。整合的Netty伺服器預設不使用SSL,因此我們需要明確配置它。
否則,對於本機測試,我們可以不保護連線。在這種情況下,我們需要配置消費者,如已經所示:
grpc:
client:
hello:
negotiation-type: plaintext
消費者的預設設定是使用 TLS,而提供者的預設設定是跳過 SSL 加密。因此,消費者和提供者的預設值彼此不符。
4.2.沒有@Autowired
消費者註入
我們透過將客戶端物件注入到 Spring 元件中來實現消費者:
@GrpcClient("hello")
HelloWorldServiceGrpc.HelloWorldServiceStub stub;
這是由BeanPostProcessor
實現的,作為 Spring 內建依賴注入機制的補充。這意味著我們不能將@GrpcClient
註解與@Autowired
或建構子注入結合使用。相反,我們僅限於使用場注入。
我們只能透過使用配置類別來分離注入:
@Configuration
public class HelloWorldGrpcClientConfiguration {
@GrpcClient("hello")
HelloWorldServiceGrpc.HelloWorldServiceStub helloWorldClient;
@Bean
MyHelloWorldClient helloWorldClient() {
return new MyHelloWorldClient(helloWorldClient);
}
}
4.3.映射傳輸對象
當呼叫具有空值的 setter 時, protoc
產生的資料類型可能會失敗:
public HelloWorldResponse map(HelloWorldMessage message) {
return HelloWorldResponse
.newBuilder()
.setGreeting( message.getGreeting() ) // might be null
.build();
}
因此,在呼叫 setter 之前我們需要進行 null 檢查。當我們使用映射框架時,我們需要配置映射器產生來執行此類空檢查。例如,MapStruct 映射器需要一些特殊配置:
@Mapper(
componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS
)
public interface HelloWorldMapper {
HelloWorldResponse map(HelloWorldMessage message);
}
4.4.測試
啟動器不包含對實施測試的任何特殊支援。即使是 gRPC for Java 專案也僅對 JUnit 4 提供最低限度的支持,並且不支援 JUnit 5 。
4.5.原生影像
當我們想要建立原生鏡像時,目前不支援 gRPC 。因為客戶端注入是透過反射完成的,所以如果沒有額外的配置,這將無法運作。
5. 結論
在本文中,我們了解到可以在 Spring Boot 應用程式中輕鬆實現 gRPC 提供者和使用者。然而,我們應該注意到,這有一些限制,例如缺少對測試和本機映像的支援。
與往常一樣,所有程式碼實作都可以在 GitHub 上取得。