1. 概述
gRPC 是一個高性能、開源的 RPC 框架,最初由 Google 開發。 它有助於消除冗餘代碼並連接異構服務,無論是在同一數據中心還是跨數據中心。 該 API 基於 Protocol Buffers,提供 protoc 編譯器,用於為不同的 支持的語言 生成代碼。
我們可以將 gRPC 視為 REST、SOAP 或 GraphQL 的替代方案,它建立在 HTTP/2 之上,利用多路複用或流式連接等功能。
在本教程中,我們將學習如何使用 Spring Boot 實現 gRPC 服務提供者和消費者。
2. 挑戰
首先,我們注意到 Spring Boot 不直接支持 gRPC。 僅支持 Protocol Buffers,這允許我們實現基於 Protocol Buffers 的 REST 服務。 因此,我們需要通過使用第三方庫或自行解決一些挑戰來引入 gRPC:
- 平台依賴編譯器:protoc 編譯器是平台依賴的。 因此,如果應在構建期間生成樁,則構建會變得更復雜且更易出錯。
- 依賴項:我們需要在 Spring Boot 應用程序中包含兼容的依賴項。 不幸的是,protoc for Java 增加了 一個 javax.annotation.Generated 註解,這迫使我們添加對舊的 Java EE Annotations for Java 庫的依賴項以進行編譯。
- 服務器運行時:gRPC 服務提供程序需要在一個服務器中運行。 gRPC for Java 項目提供了一個 shaded Netty,我們需要將其包含在 Spring Boot 應用程序中或用 Spring Boot 提供的服務器替換。
- 消息傳輸:Spring Boot 提供了不同的客户端,例如 RestClient(阻塞式)或 WebClient(非阻塞式),不幸的是,它們無法配置和用於 gRPC,因為 gRPC 使用了 自定義傳輸技術,用於阻塞和非阻塞調用。
- 配置:由於 gRPC 帶來了自己的技術,我們需要以 Spring Boot 的方式配置它們。
3. 示例項目
幸運的是,我們還可以使用第三方 Spring Boot Starter 來幫助我們應對挑戰,例如來自 LogNet 的 Starter,或者 gRPC 生態項目。這兩個 Starter 易於集成,但後者同時提供 Provider 和 Consumer 支持,以及許多其他 集成功能,因此我們選擇它作為我們的示例。
在這個示例中,我們設計了一個簡單的 HelloWorld API,並使用一個包含單個 Proto 文件的示例:
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 EE 註解對 Java 依賴項的定義,可以與 Spring Boot 項目的生命週期隔離。
3.2. 服務提供者
實現服務提供者非常簡單。首先,我們需要添加啓動器和我們的樁項目所需的依賴項:
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>3.1.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,因為 starter 依賴自帶了陰影 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. 服務消費者
對於服務消費者,我們需要將依賴項添加到啓動器和樁(stub)中:
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-client-spring-boot-starter</artifactId>
<version>3.1.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. 映射傳輸對象
使用 protoc 生成的數據類型在調用帶有 null 值的 setter 方法時可能會失敗:
public HelloWorldResponse map(HelloWorldMessage message) {
return HelloWorldResponse
.newBuilder()
.setGreeting( message.getGreeting() ) // might be null
.build();
}因此,在調用設置器之前,我們需要進行空值檢查。當我們使用映射框架時,需要配置映射器生成器以執行此類空值檢查。例如,一個 MapStruct 映射器需要一些特殊配置:
@Mapper(
componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS
)
public interface HelloWorldMapper {
HelloWorldResponse map(HelloWorldMessage message);
}4.4. 測試
啓動器不提供任何專門的支持來實施測試。 即使 grpc-java 項目也僅提供對 JUnit 4 的最小支持,並且不支持 JUnit 5。
4.5. 本地圖像
當我們需要構建本地圖像時,目前 gRPC 尚不提供支持。由於客户端注入是通過反射實現的,因此這需要額外的配置才能正常工作。
5. 結論
在本文中,我們瞭解到可以將 gRPC 提供程序和消費者輕鬆地集成到我們的 Spring Boot 應用程序中。 然而,需要注意的是,這也會帶來一些限制,例如缺乏對測試和原生鏡像的支持。