Spring Boot 实现 gPRC 服务调用

1、简介

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

我们可以将 gRPC 视为 RESTSOAPGraphQL 的替代品,它建立在 HTTP/2 的基础上,可以使用多路复用或流式连接等功能。

本文将带你了解如何使用 Spring Boot 实现 gRPC 服务提供者和消费者。

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、示例项目

幸运的是,可以使用第三方 Spring Boot Starter 来应对这些挑战,例如 LogNetgrpc 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

由于服务提供者和消费者使用相同的 Stub,我们在一个独立的项目中生成它们,与 Spring 无关。这样做的好处是,该项目的生命周期(包括 protoc 编译器配置和 Java EE Annotations for Java 依赖)可以与 Spring Boot 项目的生命周期隔离开来。

3.2、服务提供者

实现服务提供者非常简单。首先,添加 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、服务消费者

对于服务消费者,需要添 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、坑

使用 Spring Boot 实现和消费 gRPC 服务非常简单。但也要注意一些坑!

4.1、SSL

除非使用 SSL,否则通过 HTTP 传输数据意味着发送的信息是未加密的。集成的 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() ) // 可能为 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、测试

Starter 不包括任何进行测试的特殊支持。即使是 gRPC for Java 项目也只对 JUnit 4 提供了最低限度的支持,而 对 JUnit 5 则没有支持

4.5、原生镜像

当使用本地镜像进行构建时,目前还 不支持 gRPC。由于客户端注入是通过反射完成的,如果没有额外的配置,这将无法运行。

5、总结

本文介绍了如何在 Spring Boot 中通过 grpc ecosystem 项目实现 gPRC 服务提供者和消费者,以及在 Spring Boot 中使用 gPRC 进行服务调用时的一些注意事项。


Ref:https://www.baeldung.com/spring-boot-grpc