SpringBoot中使用gRPC简介

gRPC是一个高性能、开源的 RPC 框架,最初由 Google 开发。它有助于消除样板代码并连接数据中心内和跨数据中心的多语言服务。该 API 基于Protocol Buffers,它提供了一个protoc编译器来生成不同支持语言的代码。

我们可以将 gRPC 视为 REST、SOAP 或 GraphQL 的替代方案,它构建在 HTTP/2 之上,以使用多路复用或流连接等功能。

在本教程中,我们将学习如何使用 Spring Boot 实现 gRPC 服务提供者和消费者。

Spring Boot 中没有对 gRPC 的直接支持
SpringBoot仅支持Protocol Buffers,这使得我们能够实现基于protobuf的REST服务。因此,我们需要通过使用第三方库或自己应对一些挑战来包含 gRPC:例如LogNetgrpc 生态系统项目中的一个

在此示例中,我们仅使用单个 Proto 文件设计一个简单的 HelloWorld API:

syntax = "proto3";
option java_package =
"com.sample.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);
}

因为提供者和消费者的存根是相同的,所以我们在一个单独的、独立于 Spring 的项目中生成它们。这样做的好处是,项目的生命周期(包括协议编译器配置和 Java 依赖的 Java EE Annotations)可以与 Spring Boot 项目的生命周期隔离。

服务提供者
实现服务提供者非常容易。首先,我们需要添加启动器和存根项目的依赖项:

<dependency>
    <groupId>net.devh</groupId>
    <artifactId>grpc-server-spring-boot-starter</artifactId>
    <version>2.15.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.sample.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
    ) {
        // ...
    }
}

服务消费者
对于服务使用者,我们需要将依赖项添加到启动器和存根中:

<dependency>
    <groupId>net.devh</groupId>
    <artifactId>grpc-client-spring-boot-starter</artifactId>
    <version>2.15.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.sample.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;

注意事项
使用 Spring Boot 实现和使用 gRPC 服务非常简单。但我们应该注意一些陷阱:

1、SSL握手
通过 HTTP 传输数据意味着发送未加密的信息,除非我们使用 SSL。集成的Netty服务器默认不使用SSL,因此我们需要显式配置它。

否则,对于本地测试,我们可以不保护连接。在这种情况下,我们需要配置消费者,如已经所示:

grpc:
  client:
    hello:
      negotiation-type: plaintext

消费者的默认设置是使用 TLS,而提供者的默认设置是跳过 SSL 加密。因此,消费者和提供者的默认值彼此不匹配。

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);
    }
}

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);
}

当我们想要构建原生镜像时,目前不支持 gRPC。因为客户端注入是通过反射完成的,所以如果没有额外的配置,这将无法工作。

结论
在本文中,我们了解到可以在 Spring Boot 应用程序中轻松实现 gRPC 提供者和使用者。然而,我们应该注意到,这有一些限制,例如缺少对测试和本机映像的支持。