Introduction to gRPC with Spring Boot – 使用 Spring Boot 的 gRPC 简介

最后修改: 2024年 1月 26日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

gRPC is a high-performance, open-source RPC framework initially developed by Google. It helps to eliminate boilerplate code and connect polyglot services in and across data centers. The API is based on Protocol Buffers, which provides a protoc compiler to generate code for different supported languages.

gRPC是一个高性能的开源 RPC 框架,最初由 Google 开发。它有助于消除模板代码,并在数据中心内和数据中心间连接多语言服务。该 API 基于协议缓冲区,它提供了一个protoc编译器,可为不同的支持语言生成代码。

We can view gRPC as an alternative to REST, SOAP, or GraphQL, built on top of HTTP/2 to use features like multiplexing or streaming connections.

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

In this tutorial, we’ll learn how to implement gRPC service providers and consumers with Spring Boot.

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

2. Challenges

2.挑战

First, we can note that there is no direct support for gRPC in Spring Boot. Only Protocol Buffers are supported, which allows us to implement protobuf-based REST services. So, we need to include gRPC by using a third-party library, or by managing a few challenges by ourselves:

首先,我们可以注意到,Spring Boot 并不直接支持 gRPC。Spring Boot 仅支持协议缓冲区,这允许我们实现基于协议缓冲区的 REST 服务。因此,我们需要通过使用第三方库或自行应对一些挑战来加入 gRPC:

  • Platform-dependent compiler: The protoc compiler is platform-dependent. So, if the stubs should be generated during build-time, the build gets more complex and error-prone.
  • Dependencies: We need compatible dependencies within our Spring Boot application. Unfortunately, protoc for Java adds a javax.annotation.Generated annotation, which forces us to add a dependency to the old Java EE Annotations for Java library for compilation.
  • Server Runtime: gRPC service providers need to run within a server. The gRPC for Java project provides a shaded Netty, which we need to either include in our Spring Boot application or replace by a server already provided by Spring Boot.
  • Message Transport: Spring Boot provides different clients, like the RestClient (blocking) or the WebClient (non-blocking), that unfortunately cannot be configured and used for gRPC, because gRPC uses custom transport technologies for both blocking and non-blocking calls.
  • Configuration: Because gRPC brings its own technologies, we need configuration properties to configure them the Spring Boot way.

3. Sample Projects

3.项目样本

Fortunately, there are third-party Spring Boot Starters that we can use to master the challenges for us, such as the one from LogNet or the grpc ecosystem project. Both starters are easy to integrate, but the latter one has both provider and consumer support as well as many other integration features, so that’s the one we chose for our examples.

幸运的是,我们可以使用第三方 Spring Boot 启动程序来应对这些挑战,例如 LogNetgrpc 生态系统项目。这两个启动器都很容易集成,但后者同时支持提供者和消费者以及许多其他集成功能,因此我们在示例中选择了后者。

In this sample, we design just a simple HelloWorld API with a single Proto file:

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

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

As we can see, we use the Bidirectional Streaming feature.

正如我们所见,我们使用了双向流功能。

3.1. gRPC Stubs

3.1. gRPC 存根

Because the stubs are the same for both provider and consumer, we generate them within a separate, Spring-indepentent project. This has the advantage that the project’s lifecycle, including the protoc compiler configuration and the Java EE Annotations for Java dependency, can be isolated from the Spring Boot project’s lifecycle.

由于提供者和消费者的存根是相同的,因此我们在独立的 Spring 项目中生成存根。这样做的好处是,项目的生命周期(包括 protoc 编译器配置和 Java EE Annotations for Java 依赖关系)可以与 Spring Boot 项目的生命周期隔离开来。

3.2. Service Provider

3.2.服务提供商

Implementing the service provider is pretty easy. First, we need to add the dependencies for the starter and our stubs project:

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

<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>

There’s no need to include Spring MVC or WebFlux because the starter dependency brings the shaded Netty server. We can configure it within the application.yml, for example, by configuring the server port:

无需包含 Spring MVC 或 WebFlux,因为启动器依赖关系会带来阴影 Netty 服务器。我们可以application.yml中对其进行配置,例如,配置服务器端口:

grpc:
  server:
    port: 9090

Then, we need to implement the service and annotate it with @GrpcService:

然后,我们需要实现该服务,并用 @GrpcService 对其进行注解:

@GrpcService
public class HelloWorldController extends HelloWorldServiceGrpc.HelloWorldServiceImplBase {

    @Override
    public StreamObserver<HelloWorldRequest> sayHello(
        StreamObserver<HelloWorldResponse> responseObserver
    ) {
        // ...
    }
}

3.3. Service Consumer

3.3.服务消费者

For the service consumer, we need to add the dependencies to the starter and the stubs:

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

<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>

Then, we configure the connection to the service in the application.yml:

然后,我们在 application.yml配置与服务的连接

grpc:
  client:
    hello:
      address: localhost:9090
      negotiation-type: plaintext

The name “hello” is a custom one. This way, we can configure multiple connections and refer to this name when injecting the gRPC client into our Spring component:

“hello”是一个自定义名称。这样,我们就可以配置多个连接,并在将 gRPC 客户端注入 Spring 组件时引用该名称:

@GrpcClient("hello")
HelloWorldServiceGrpc.HelloWorldServiceStub stub;

4. Pitfalls

4.陷阱

Implementing and consuming a gRPC service with Spring Boot is pretty easy. But there are some pitfalls that we should be aware of.

使用 Spring Boot 实现和消费 gRPC 服务非常简单。但我们也应注意一些陷阱。

4.1. SSL-Handshake

4.1 SSL 握手

Transferring data over HTTP means sending information unencrypted unless we use SSL. The integrated Netty server does not use SSL by default, so we need to explicitly configure it.

除非我们使用 SSL,否则通过 HTTP 传输数据意味着发送的信息是未加密的。集成的 Netty 服务器默认不使用 SSL,因此我们需要明确配置它

Otherwise, for local tests, we can leave the connection unprotected. In this case, we need to configure the consumer, as already shown:

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

grpc:
  client:
    hello:
      negotiation-type: plaintext

The default for the consumer is to use TLS, while the default for the provider is to skip SSL encryption. So, the defaults for consumer and provider don’t match each other.

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

4.2. Consumer Injection Without @Autowired

4.2.无 @Autowired 的消费者注入

We implement the consumer by injecting a client object into our Spring component:

我们通过向 Spring 组件注入一个客户端对象来实现消费者:

@GrpcClient("hello")
HelloWorldServiceGrpc.HelloWorldServiceStub stub;

This is implemented by a BeanPostProcessor and works as an addition to Spring’s built-in dependency injection mechanism. That means we can’t use the @GrpcClient annotation in conjunction with @Autowired or constructor injection. Instead, we’re restricted to using field injection.

该注解由 BeanPostProcessor 实现,作为 Spring 内置依赖注入机制的补充。这意味着我们无法将 @GrpcClient 注解与 @Autowired 或构造器注入结合使用。相反,我们只能使用字段注入。

We could only separate the injection by using a configuration class:

我们只能通过使用配置类来分离注入:

@Configuration
public class HelloWorldGrpcClientConfiguration {

    @GrpcClient("hello")
    HelloWorldServiceGrpc.HelloWorldServiceStub helloWorldClient;

    @Bean
    MyHelloWorldClient helloWorldClient() {
      return new MyHelloWorldClient(helloWorldClient);
    }
}

4.3. Mapping Transfer Objects

4.3.映射传输对象

The data types generated by protoc can fail when invoking setters with null values:

当调用带有空值的设置器时,protoc 生成的数据类型可能会失败:

public HelloWorldResponse map(HelloWorldMessage message) {
    return HelloWorldResponse
      .newBuilder()
      .setGreeting( message.getGreeting() ) // might be null
      .build();
}

So, we need null checks before invoking the setters. When we use mapping frameworks, we need to configure the mapper generation to do such null checks. A MapStruct mapper, for example, would need some special configuration:

因此,我们需要在调用设置器之前进行空检查。当我们使用映射框架时,我们需要配置映射器生成来进行这种空检查。例如,MapStruct 映射器 就需要一些特殊配置:

@Mapper(
  componentModel = "spring",
  nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
  nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS
)
public interface HelloWorldMapper {
    HelloWorldResponse map(HelloWorldMessage message);
}

4.4. Testing

4.4.测试

The starter doesn’t include any special support for implementing tests. Even the gRPC for Java project has only minimal support for JUnit 4, and no support for JUnit 5.

启动器不包括任何用于实施测试的特殊支持。甚至 gRPC for Java 项目也只对 JUnit 4 提供了最低限度的支持,而且 不支持 JUnit 5

4.5. Native Images

4.5.本地图像

When we want to build native images, there’s currently no support for gRPC. Because the client injection is done via reflection, this won’t work without extra configuration.

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

5. Conclusion

5.结论

In this article, we’ve learned that we can easily implement gRPC providers and consumers within our Spring Boot application. We should note, however, that this comes with some restrictions, like missing support for testing and native images.

在本文中,我们了解到可以在 Spring Boot 应用程序中轻松实现 gRPC 提供者和消费者。不过,我们应该注意到,这样做有一些限制,比如缺少对测试和本地图像的支持。

As usual, all the code implementations are available over on GitHub.

与往常一样,所有代码的实现都可以访问 GitHub