1、简介 {#1简介}
gRPC 是一个高性能的开源 RPC 框架,最初由谷歌开发。它有助于消除样板代码,并在数据中心内外连接多语言服务。该 API 基于 Protocol Buffer,它提供了一个 protoc 编译器,用于生成不同支持的语言的代码。
我们可以将 gRPC 视为 REST 、SOAP 或 GraphQL 的替代品,它建立在 HTTP/2 的基础上,可以使用多路复用或流式连接等功能。
本文将带你了解如何使用 Spring Boot 实现 gRPC 服务提供者和消费者。
2、面临的问题 {#2面临的问题}
首先,Spring Boot 并不直接支持 gRPC。它只支持 Protocol Buffer,允许我们实现基于 protobuf 的 REST 服务。因此,需要通过使用第三方库来加入 gRPC:
- 平台相关的编译器:protoc 编译器是平台相关的。因此,如果在构建过程中需要生成存根(Stub),构建过程会变得更加复杂且容易出错。
- 依赖: 需要在 Spring Boot 应用中添加兼容的依赖。不幸的是,Java 的 protoc 添加了
javax.annotation.Generated
注解(Issues),这迫使我们需要添加一个依赖项来编译旧的 Java EE 注解库。 - 服务器运行时:gRPC 服务提供者需要在服务器中运行。gRPC for Java 项目提供了一个 shaded Netty,我们需要将其包含在 Spring Boot 应用中,或者用 Spring Boot 已经提供的服务器来代替。
- 消息传输: Spring Boot 提供了不同的客户端,如
RestClient
(阻塞)或WebClient
(非阻塞),但遗憾的是,这些客户端无法配置和用于 gRPC,因为 gRPC 在阻塞和非阻塞调用中都使用了 自定义 transport 技术。 - 配置: 由于 gRPC 使用了自己的技术,我们需要配置属性以按照 Spring Boot 的方式对其进行配置。
3、示例项目 {#3示例项目}
幸运的是,可以使用第三方 Spring Boot Starter 来应对这些挑战,例如 LogNet 或 grpc ecosystem project。这两个 Starter 都很容易整合,但后者同时支持提供者和消费者以及许多其他集成功能,因此本文选择了后者。
在本例中,只设计了一个简单的 HelloWorld API 和一个 Proto 文件:
syntax = "proto3";
option java_package = "com.baeldung.helloworld.stubs";
option java_multiple_files = true;
message HelloWorldRequest {
// 一个要问候的名称,默认为 "World"。
optional string name = 1;
}
message HelloWorldResponse {
string greeting = 1;
}
service HelloWorldService {
rpc SayHello(stream HelloWorldRequest) returns (stream HelloWorldResponse);
}
你可以看到,使用了双向流式传输(Bidirectional Streaming)功能。
3.1、gRPC Stub {#31grpc-stub}
由于服务提供者和消费者使用相同的 Stub,我们在一个独立的项目中生成它们,与 Spring 无关。这样做的好处是,该项目的生命周期(包括 protoc 编译器配置和 Java EE Annotations for Java 依赖)可以与 Spring Boot 项目的生命周期隔离开来。
3.2、服务提供者 {#32服务提供者}
实现服务提供者非常简单。首先,添加 Starter 和 Stub 依赖:
<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,因为 Starter 依赖包含了 shaded 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、服务消费者 {#33服务消费者}
对于服务消费者,需要添 Starter 和 stub 依赖:
<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、坑 {#4坑}
使用 Spring Boot 实现和消费 gRPC 服务非常简单。但也要注意一些坑!
4.1、SSL {#41ssl}
除非使用 SSL,否则通过 HTTP 传输数据意味着发送的信息是未加密的。集成的 Netty 服务器默认不使用 SSL,因此需要对其进行显式配置。
对于本地测试,我们可以让连接不受保护。在这种情况下,我们需要配置消费者,如前所述:
grpc:
client:
hello:
negotiation-type: plaintext
消费者的默认值是使用 TLS,而提供者的默认值是跳过 SSL 加密。因此,消费者和提供者的默认值并不一致。
4.2、不使用 @Autowired 进行消费者注入 {#42不使用-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、映射传输对象 {#43映射传输对象}
由 protoc 生成的数据类型在使用 null
值调用 setter 方法时可能会失败:
public HelloWorldResponse map(HelloWorldMessage message) {
return HelloWorldResponse
.newBuilder()
.setGreeting( message.getGreeting() ) // 可能为 null
.build();
}
因此,需要在调用 setter 方法之前进行 null
检查。当使用映射框架时,需要配置映射器生成来进行这种 null
检查。例如,MapStruct mapper 就需要一些特殊配置:
@Mapper(
componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS
)
public interface HelloWorldMapper {
HelloWorldResponse map(HelloWorldMessage message);
}
4.4、测试 {#44测试}
Starter 不包括任何进行测试的特殊支持。即使是 gRPC for Java 项目也只对 JUnit 4 提供了最低限度的支持,而 对 JUnit 5 则没有支持。
4.5、原生镜像 {#45原生镜像}
当使用本地镜像进行构建时,目前还 不支持 gRPC。由于客户端注入是通过反射完成的,如果没有额外的配置,这将无法运行。
5、总结 {#5总结}
本文介绍了如何在 Spring Boot 中通过 grpc ecosystem 项目实现 gPRC 服务提供者和消费者,以及在 Spring Boot 中使用 gPRC 进行服务调用时的一些注意事项。
Ref:https://www.baeldung.com/spring-boot-grpc