1. 概述

gRPC 是 Google 开发的高性能开源 RPC 框架。它能有效消除样板代码,连接数据中心内外的多语言服务。其 API 基于 Protocol Buffers,通过 protoc 编译器为多种支持的语言生成代码。

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

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

2. 挑战

首先要注意 Spring Boot 并不直接支持 gRPC。虽然支持 Protocol Buffers(可用于实现基于 protobuf 的 REST 服务),但我们需要通过第三方库或自行解决以下挑战:

  • 平台依赖的编译器:protoc 编译器与平台相关。若在构建时生成存根,会使构建过程更复杂且易出错。
  • 依赖冲突:需在 Spring Boot 应用中引入兼容的依赖。Java 版的 protoc添加 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 可帮我们解决这些挑战,例如 LogNetgrpc ecosystem project。后者同时支持提供者和消费者,且具备更多集成特性,因此我们选用它。

本示例中,我们设计一个简单的 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);
}

这里使用了双向流式通信特性。

3.1. gRPC 存根生成

由于提供者和消费者使用相同存根,我们将其放在独立的非 Spring 项目中生成。这样能隔离 protoc 编译器配置和 Java EE 注解依赖,避免与 Spring Boot 项目生命周期耦合。

3.2. 服务提供者

实现服务提供者非常简单。首先添加 starter 和存根项目依赖:

<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 和存根依赖:

<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 客户端时引用:

@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. 消费者注入限制

通过字段注入客户端:

@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 生成的数据类型在调用 setter 时可能因 null 值失败:

public HelloWorldResponse map(HelloWorldMessage message) {
    return HelloWorldResponse
      .newBuilder()
      .setGreeting( message.getGreeting() ) // 可能为 null
      .build();
}

解决方案:调用 setter 前必须进行 null 检查。使用映射框架时需特殊配置,例如 MapStruct 需添加:

@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 提供者和消费者的方法。需注意其局限性,如测试支持和原生镜像兼容性不足。

所有代码实现可在 GitHub 获取。


原始标题:Introduction to gRPC with Spring Boot