Warning

This article was automatically translated by OpenAI (gpt-4.1).It may be edited eventually, but please be aware that it may contain incorrect information at this time.

Spring gRPC is the official Spring project for using gRPC in Spring applications.
By using Spring gRPC, you can integrate gRPC services into your Spring Boot applications.

At the time of writing, the version of Spring gRPC is 0.8.0. We'll use it together with Spring Boot 3.4.5.
The auto-configuration modules of Spring gRPC are planned to be included in Spring Boot 4.0 at the 1.0 release.

We'll create a Hello World app and introduce the basic usage of Spring gRPC.

Table of Contents

Creating a gRPC Server with Spring gRPC

First, let's create a gRPC server that implements Hello World.
With Spring gRPC, you can choose between a Netty-based standalone server and a Servlet-based server using GrpcServlet.
The Servlet-based server can provide services on the same port as regular Spring MVC.
Note that currently, if you use Spring WebFlux, you cannot provide gRPC and HTTP on the same port (spring-grpc#19).

This time, we'll create a Servlet-based server. If you use Spring Initializr and select both "Spring Web" and "Spring gRPC", the dependencies for the Servlet-based setup will be added automatically.

Create a new project using Spring Initializr with the following command:

curl -s https://start.spring.io/starter.tgz \
       -d artifactId=demo-grpc-server \
       -d name=demo-grpc-server \
       -d baseDir=demo-grpc-server  \
       -d packageName=com.example \
       -d dependencies=spring-grpc,web,actuator,configuration-processor,prometheus,native \
       -d type=maven-project \
       -d applicationName=DemoGrpcServerApplication | tar -xzvf -
cd demo-grpc-server 

Next, create the Protocol Buffers schema file. Here, we'll use the sample from the gRPC documentation.
However, in this article, we won't implement client-to-server streaming (LotsOfGreetings) and bidirectional streaming (BidiHello).

cat <<EOF > src/main/proto/hello.proto
syntax = "proto3";

package com.example;

option java_package = "com.example.proto";
option java_outer_classname = "HelloServiceProto";
option java_multiple_files = true;

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
  rpc LotsOfReplies (HelloRequest) returns (stream HelloResponse);
  rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
  rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
}

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}
EOF

First, compile the proto file to generate Java code. The protobuf-maven-plugin for generating Protocol Buffers Java code is automatically added when you create the project with Spring Initializr.

./mvnw compile

Check the generated files with the following command:

$ find target/generated-sources/protobuf -type f
target/generated-sources/protobuf/grpc-java/com/example/proto/HelloServiceGrpc.java
target/generated-sources/protobuf/java/com/example/proto/HelloServiceProto.java
target/generated-sources/protobuf/java/com/example/proto/HelloRequest.java
target/generated-sources/protobuf/java/com/example/proto/HelloResponseOrBuilder.java
target/generated-sources/protobuf/java/com/example/proto/HelloRequestOrBuilder.java
target/generated-sources/protobuf/java/com/example/proto/HelloResponse.java

Next, implement the gRPC service. Create a class that extends HelloServiceGrpc.HelloServiceImplBase and override the gRPC methods.
By adding the @Service annotation, it will be registered in the Spring DI container and automatically registered with the gRPC server.

cat<<EOF>src/main/java/com/example/HelloService.java
package com.example;

import com.example.proto.HelloRequest;
import com.example.proto.HelloResponse;
import com.example.proto.HelloServiceGrpc;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class HelloService extends HelloServiceGrpc.HelloServiceImplBase {

    private final Logger log = LoggerFactory.getLogger(HelloService.class);

    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
        log.info("sayHello");
        HelloResponse response = HelloResponse.newBuilder()
            .setReply(String.format("Hello %s!", request.getGreeting()))
            .build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }

    @Override
    public void lotsOfReplies(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
        log.info("lotsOfReplies");
        for (int i = 0; i < 10; i++) {
            HelloResponse response = HelloResponse.newBuilder()
                .setReply(String.format("[%05d] Hello %s!", i, request.getGreeting()))
                .build();
            responseObserver.onNext(response);
        }
        responseObserver.onCompleted();
    }

}
EOF

Start the application. Since we're using a Servlet-based server, you can access the gRPC service on the default port 8080.

./mvnw spring-boot:run 

To access the gRPC service from the command line, install grpcurl.

brew install grpcurl

First, use the gRPC reflection service to get a list of gRPC services. The reflection service is automatically registered if you created the project with Spring Initializr.

$ grpcurl --plaintext localhost:8080 list 

demo.HelloService
grpc.health.v1.Health
grpc.reflection.v1.ServerReflection

You can see that the Health check service is registered. Spring gRPC includes an implementation of gRPC Health Check by default.

Check the list of methods in the Health check service.

$ grpcurl --plaintext localhost:8080 describe grpc.health.v1.Health   

grpc.health.v1.Health is a service:
service Health {
  rpc Check ( .grpc.health.v1.HealthCheckRequest ) returns ( .grpc.health.v1.HealthCheckResponse );
  rpc Watch ( .grpc.health.v1.HealthCheckRequest ) returns ( stream .grpc.health.v1.HealthCheckResponse );
}

Run the Check method to check the status of the gRPC service.

$ grpcurl --plaintext localhost:8080 grpc.health.v1.Health/Check
{
  "status": "SERVING"
}

Now, let's check the methods of the implemented com.example.HelloService service.

$ grpcurl --plaintext localhost:8080 describe com.example.HelloService

com.example.HelloService is a service:
service HelloService {
  rpc BidiHello ( stream .com.example.HelloRequest ) returns ( stream .com.example.HelloResponse );
  rpc LotsOfGreetings ( stream .com.example.HelloRequest ) returns ( .com.example.HelloResponse );
  rpc LotsOfReplies ( .com.example.HelloRequest ) returns ( stream .com.example.HelloResponse );
  rpc SayHello ( .com.example.HelloRequest ) returns ( .com.example.HelloResponse );
}

Let's try running the SayHello method. The request is specified in JSON format. Use the --plaintext option when not using TLS.

$ grpcurl -d '{"greeting":"John Doe"}' --plaintext localhost:8080 com.example.HelloService/SayHello
{
  "reply": "Hello John Doe!"
}

Next, run the LotsOfReplies method. This is a server streaming method.

$ grpcurl -d '{"greeting":"John Doe"}' --plaintext localhost:8080 com.example.HelloService/LotsOfReplies
{
  "reply": "[00000] Hello John Doe!"
}
{
  "reply": "[00001] Hello John Doe!"
}
{
  "reply": "[00002] Hello John Doe!"
}
{
  "reply": "[00003] Hello John Doe!"
}
{
  "reply": "[00004] Hello John Doe!"
}
{
  "reply": "[00005] Hello John Doe!"
}
{
  "reply": "[00006] Hello John Doe!"
}
{
  "reply": "[00007] Hello John Doe!"
}
{
  "reply": "[00008] Hello John Doe!"
}
{
  "reply": "[00009] Hello John Doe!"
}

Now that we've confirmed the service works with grpcurl, let's test the gRPC service.

With Spring gRPC, the client stub for the gRPC service is also automatically registered, so you can use it in your test class by @Autowired.
As shown below, you can easily implement integration tests for gRPC service methods using Spring Boot's test features.

Note that the client stub is automatically registered only when using a channel named default-channel, and only for the BlockingStub.
If you want to register stubs other than BlockingStub, you can use the @ImportGrpcClients annotation to register the gRPC client.

cat<<'EOF'> src/test/java/com/example/HelloServiceTest.java
package com.example;

import com.example.proto.HelloRequest;
import com.example.proto.HelloResponse;
import com.example.proto.HelloServiceGrpc;
import com.google.common.collect.Streams;
import java.util.Iterator;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = "spring.grpc.client.default-channel.address=0.0.0.0:${local.server.port}")
class HelloServiceTest {

    @Autowired
    HelloServiceGrpc.HelloServiceBlockingStub stub;

    @Test
    void sayHello() {
        HelloResponse response = this.stub.sayHello(HelloRequest.newBuilder().setGreeting("John Doe").build());
        assertThat(response.getReply()).isEqualTo("Hello John Doe!");
    }

    @Test
    void lotsOfReplies() {
        Iterator<HelloResponse> response = this.stub
            .lotsOfReplies(HelloRequest.newBuilder().setGreeting("John Doe").build());
        List<String> replies = Streams.stream(response).map(HelloResponse::getReply).toList();
        assertThat(replies).containsExactly("[00000] Hello John Doe!", "[00001] Hello John Doe!",
                "[00002] Hello John Doe!", "[00003] Hello John Doe!", "[00004] Hello John Doe!",
                "[00005] Hello John Doe!", "[00006] Hello John Doe!", "[00007] Hello John Doe!",
                "[00008] Hello John Doe!", "[00009] Hello John Doe!");
    }

}
EOF

Run the tests and make sure everything passes.

./mvnw test

Creating a gRPC Client with Spring gRPC

Next, let's create a gRPC client application to interact with the server.

First, as before, use Spring Initializr to create a new project and build the client-side application.

cd ..
curl -s https://start.spring.io/starter.tgz \
       -d artifactId=demo-grpc-client \
       -d name=demo-grpc-client \
       -d baseDir=demo-grpc-client  \
       -d packageName=com.example \
       -d dependencies=spring-grpc,web,actuator,configuration-processor,prometheus,native \
       -d type=maven-project \
       -d applicationName=DemoGrpcClientApplication | tar -xzvf -
cd demo-grpc-client 

Next, create the same Protocol Buffers schema file as on the server side.

cat <<EOF > src/main/proto/hello.proto
syntax = "proto3";

package com.example;

option java_package = "com.example.proto";
option java_outer_classname = "HelloServiceProto";
option java_multiple_files = true;

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
  rpc LotsOfReplies (HelloRequest) returns (stream HelloResponse);
  rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
  rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
}

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}
EOF

Compile to generate the stub code.

./mvnw compile

Next, use the gRPC stub to implement a Spring MVC controller. As with the server-side test, if you use the BlockingStub with the default-channel channel,
the client stub will be automatically registered in the DI container and can be injected.

cat <<EOF > src/main/java/com/example/HelloController.java
package com.example;

import com.example.proto.HelloRequest;
import com.example.proto.HelloResponse;
import com.example.proto.HelloServiceGrpc;
import com.google.common.collect.Streams;
import java.util.Iterator;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    private final HelloServiceGrpc.HelloServiceBlockingStub helloServiceStub;

    public HelloController(HelloServiceGrpc.HelloServiceBlockingStub helloServiceStub) {
        this.helloServiceStub = helloServiceStub;
    }

    @GetMapping(path = "/")
    public Reply sayHello(@RequestParam String greeting) {
        HelloResponse response = helloServiceStub.sayHello(HelloRequest.newBuilder().setGreeting(greeting).build());
        return new Reply(response.getReply());
    }

    @GetMapping(path = "/lots-of-replies")
    public List<Reply> lotsOfReplies(@RequestParam String greeting) {
        Iterator<HelloResponse> replies = helloServiceStub
            .lotsOfReplies(HelloRequest.newBuilder().setGreeting(greeting).build());
        return Streams.stream(replies).map(r -> new Reply(r.getReply())).toList();
    }

    public record Reply(String reply) {
    }

}
EOF

Add the following properties:

cat <<EOF >> src/main/resources/application.properties
server.port=8082
spring.grpc.client.default-channel.address=localhost:8080
EOF

Start the client application.

./mvnw spring-boot:run 

Send a request to the client application running on port 8082 using curl.

$ curl -s "http://localhost:8082?greeting=John%20Doe" | jq .
{
  "reply": "Hello John Doe!"
}
$ curl -s "http://localhost:8082/lots-of-replies?greeting=John%20Doe" | jq .
[
  {
    "reply": "[00000] Hello John Doe!"
  },
  {
    "reply": "[00001] Hello John Doe!"
  },
  {
    "reply": "[00002] Hello John Doe!"
  },
  {
    "reply": "[00003] Hello John Doe!"
  },
  {
    "reply": "[00004] Hello John Doe!"
  },
  {
    "reply": "[00005] Hello John Doe!"
  },
  {
    "reply": "[00006] Hello John Doe!"
  },
  {
    "reply": "[00007] Hello John Doe!"
  },
  {
    "reply": "[00008] Hello John Doe!"
  },
  {
    "reply": "[00009] Hello John Doe!"
  }
]

You can see that the gRPC service response is returned via the client.

Observability Integration with Micrometer

Spring gRPC also supports observability with Micrometer out of the box.

In this article, we'll use OpenTelemetry for tracing and export metrics with Prometheus.

Add the following dependencies to pom.xml for both server and client. The micrometer-registry-prometheus for Prometheus metrics export is already added by Spring Initializr.

    <dependency>
      <groupId>io.micrometer</groupId>
      <artifactId>micrometer-tracing-bridge-otel</artifactId>
    </dependency>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-exporter-otlp</artifactId>
      <exclusions>
        <exclusion>
          <groupId>io.opentelemetry</groupId>
          <artifactId>opentelemetry-exporter-sender-okhttp</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-exporter-sender-jdk</artifactId>
    </dependency>

Start Zipkin as the OTLP tracing receiver with the following command:

docker run --name zipkin -d -p 9411:9411 -e UI_ENABLED=true ghcr.io/openzipkin-contrib/zipkin-otel

Next, add the following properties to application.properties for both server and client.

cat <<EOF >> src/main/resources/application.properties
management.endpoints.web.exposure.include=health,info,prometheus
management.tracing.sampling.probability=1.0
management.otlp.tracing.endpoint=http://localhost:9411/v1/traces
management.otlp.tracing.compression=gzip
EOF

Restart both server and client, then send the following requests:

curl -s "http://localhost:8082?greeting=John%20Doe"
curl -s "http://localhost:8082/lots-of-replies?greeting=John%20Doe" | jq .

Access http://localhost:9411 to open the Zipkin UI. You should see the following traces:

image

image

image

You can see that traces for gRPC methods are visible for both server and client.

Next, check the Prometheus metrics endpoint.

$ curl -s http://localhost:8080/actuator/prometheus | grep grpc | grep -v '^disk'
# HELP grpc_server_active_seconds  
# TYPE grpc_server_active_seconds summary
grpc_server_active_seconds_count{grpc_status_code="UNKNOWN",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 0
grpc_server_active_seconds_sum{grpc_status_code="UNKNOWN",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 0.0
grpc_server_active_seconds_count{grpc_status_code="UNKNOWN",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 0
grpc_server_active_seconds_sum{grpc_status_code="UNKNOWN",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 0.0
# HELP grpc_server_active_seconds_max  
# TYPE grpc_server_active_seconds_max gauge
grpc_server_active_seconds_max{grpc_status_code="UNKNOWN",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 0.0
grpc_server_active_seconds_max{grpc_status_code="UNKNOWN",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 0.0
# HELP grpc_server_received_total  
# TYPE grpc_server_received_total counter
grpc_server_received_total{grpc_status_code="UNKNOWN",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 1.0
grpc_server_received_total{grpc_status_code="UNKNOWN",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 1.0
# HELP grpc_server_seconds  
# TYPE grpc_server_seconds summary
grpc_server_seconds_count{error="none",grpc_status_code="OK",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 1
grpc_server_seconds_sum{error="none",grpc_status_code="OK",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 0.003178875
grpc_server_seconds_count{error="none",grpc_status_code="OK",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 1
grpc_server_seconds_sum{error="none",grpc_status_code="OK",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 0.009552291
# HELP grpc_server_seconds_max  
# TYPE grpc_server_seconds_max gauge
grpc_server_seconds_max{error="none",grpc_status_code="OK",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 0.003178875
grpc_server_seconds_max{error="none",grpc_status_code="OK",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 0.009552291
# HELP grpc_server_sent_total  
# TYPE grpc_server_sent_total counter
grpc_server_sent_total{grpc_status_code="UNKNOWN",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 10.0
grpc_server_sent_total{grpc_status_code="UNKNOWN",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 1.0
$ curl -s http://localhost:8082/actuator/prometheus | grep grpc | grep -v '^disk'
# HELP grpc_client_active_seconds  
# TYPE grpc_client_active_seconds summary
grpc_client_active_seconds_count{grpc_status_code="UNKNOWN",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 0
grpc_client_active_seconds_sum{grpc_status_code="UNKNOWN",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 0.0
grpc_client_active_seconds_count{grpc_status_code="UNKNOWN",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 0
grpc_client_active_seconds_sum{grpc_status_code="UNKNOWN",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 0.0
# HELP grpc_client_active_seconds_max  
# TYPE grpc_client_active_seconds_max gauge
grpc_client_active_seconds_max{grpc_status_code="UNKNOWN",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 0.0
grpc_client_active_seconds_max{grpc_status_code="UNKNOWN",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 0.0
# HELP grpc_client_received_total  
# TYPE grpc_client_received_total counter
grpc_client_received_total{grpc_status_code="UNKNOWN",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 10.0
grpc_client_received_total{grpc_status_code="UNKNOWN",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 1.0
# HELP grpc_client_seconds  
# TYPE grpc_client_seconds summary
grpc_client_seconds_count{error="none",grpc_status_code="OK",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 1
grpc_client_seconds_sum{error="none",grpc_status_code="OK",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 0.00535025
grpc_client_seconds_count{error="none",grpc_status_code="OK",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 1
grpc_client_seconds_sum{error="none",grpc_status_code="OK",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 0.104690708
# HELP grpc_client_seconds_max  
# TYPE grpc_client_seconds_max gauge
grpc_client_seconds_max{error="none",grpc_status_code="OK",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 0.00535025
grpc_client_seconds_max{error="none",grpc_status_code="OK",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 0.104690708
# HELP grpc_client_sent_total  
# TYPE grpc_client_sent_total counter
grpc_client_sent_total{grpc_status_code="UNKNOWN",rpc_method="LotsOfReplies",rpc_service="com.example.HelloService",rpc_type="SERVER_STREAMING"} 1.0
grpc_client_sent_total{grpc_status_code="UNKNOWN",rpc_method="SayHello",rpc_service="com.example.HelloService",rpc_type="UNARY"} 1.0

You can see metrics starting with grpc_server_* and grpc_client_*.

Introducing Reactive Programming with Reactor

In the standard gRPC Java API, you use the callback-based StreamObserver for streaming processing, but this can lead to verbose code when handling complex stream operations. Spring gRPC allows integration with Reactor, enabling you to implement gRPC services more declaratively and concisely using a reactive API.

To use Reactor, add the following dependencies and plugin settings. By using reactive-grpc developed by Salesforce, Reactor-based gRPC stubs are automatically generated.

    <dependency>
      <groupId>io.projectreactor</groupId>
      <artifactId>reactor-core</artifactId>
    </dependency>
    <dependency>
      <groupId>com.salesforce.servicelibs</groupId>
      <artifactId>reactor-grpc-stub</artifactId>
    </dependency>
    <dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-test</artifactId>
        <scope>test</scope>
    </dependency>
      <plugin>
        <groupId>org.xolstice.maven.plugins</groupId>
        <artifactId>protobuf-maven-plugin</artifactId>
        <version>0.6.1</version>
        <configuration>
          <protocArtifact>
            com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier}
          </protocArtifact>
          <pluginId>grpc-java</pluginId>
          <pluginArtifact>
            io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
          </pluginArtifact>
        </configuration>
        <executions>
          <execution>
            <id>compile</id>
            <goals>
              <goal>compile</goal>
              <goal>compile-custom</goal>
            </goals>
            <configuration>
              <pluginParameter>jakarta_omit,@generated=omit</pluginParameter>
              <!-- !!!! -->
              <protocPlugins>
                <protocPlugin>
                  <id>reactor-grpc</id>
                  <groupId>com.salesforce.servicelibs</groupId>
                  <artifactId>reactor-grpc</artifactId>
                  <version>1.2.4</version>
                  <mainClass>com.salesforce.reactorgrpc.ReactorGrpcGenerator</mainClass>
                </protocPlugin>
              </protocPlugins>
              <!-- !!!! -->
            </configuration>
          </execution>
        </executions>
      </plugin>

Recompile both server and client to generate Reactor-based code.

./mvnw clean compile

Check the generated code and you'll see a class called ReactorHelloServiceGrpc has been generated.

$ find target/generated-sources/protobuf -type f
target/generated-sources/protobuf/grpc-java/com/example/proto/HelloServiceGrpc.java
target/generated-sources/protobuf/java/com/example/proto/HelloServiceProto.java
target/generated-sources/protobuf/java/com/example/proto/ReactorHelloServiceGrpc.java
target/generated-sources/protobuf/java/com/example/proto/HelloRequest.java
target/generated-sources/protobuf/java/com/example/proto/HelloResponseOrBuilder.java
target/generated-sources/protobuf/java/com/example/proto/HelloRequestOrBuilder.java
target/generated-sources/protobuf/java/com/example/proto/HelloResponse.java

By using this Reactor-based gRPC stub on the client side, you can use asynchronous and reactive APIs. It can also be used with Servlet-based Spring MVC.

cat <<EOF > src/main/java/com/example/HelloController.java
package com.example;

import com.example.proto.HelloRequest;
import com.example.proto.ReactorHelloServiceGrpc;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
public class HelloController {

    private final ReactorHelloServiceGrpc.ReactorHelloServiceStub helloServiceStub;

    public HelloController(ReactorHelloServiceGrpc.ReactorHelloServiceStub helloServiceStub) {
        this.helloServiceStub = helloServiceStub;
    }

    @GetMapping(path = "/")
    public Mono<Reply> sayHello(@RequestParam String greeting) {
        return helloServiceStub.sayHello(HelloRequest.newBuilder().setGreeting(greeting).build())
            .map(r -> new Reply(r.getReply()));
    }

    @GetMapping(path = "/lots-of-replies")
    public Flux<Reply> lotsOfReplies(@RequestParam String greeting) {
        return helloServiceStub.lotsOfReplies(HelloRequest.newBuilder().setGreeting(greeting).build())
            .map(r -> new Reply(r.getReply()));
    }

    public record Reply(String reply) {
    }

}
EOF

When using the Reactor-based gRPC stub, you need to register it in the DI container using the @ImportGrpcClients annotation, so create a GrpcConfig class as follows:

cat <<EOF > src/main/java/com/example/GrpcConfig.java
package com.example;

import com.example.proto.ReactorHelloServiceGrpc;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.client.ImportGrpcClients;

@Configuration(proxyBeanMethods = false)
@ImportGrpcClients(types = ReactorHelloServiceGrpc.ReactorHelloServiceStub.class)
public class GrpcConfig {

}
EOF

Restart the client and send the following requests:

$ curl -s "http://localhost:8082?greeting=John%20Doe" | jq .
{
  "reply": "Hello John Doe!"
}
$ curl -s "http://localhost:8082/lots-of-replies?greeting=John%20Doe" | jq .
[
  {
    "reply": "[00000] Hello John Doe!"
  },
  {
    "reply": "[00001] Hello John Doe!"
  },
  {
    "reply": "[00002] Hello John Doe!"
  },
  {
    "reply": "[00003] Hello John Doe!"
  },
  {
    "reply": "[00004] Hello John Doe!"
  },
  {
    "reply": "[00005] Hello John Doe!"
  },
  {
    "reply": "[00006] Hello John Doe!"
  },
  {
    "reply": "[00007] Hello John Doe!"
  },
  {
    "reply": "[00008] Hello John Doe!"
  },
  {
    "reply": "[00009] Hello John Doe!"
  }
]

If you return a Flux type, you can return responses in formats more suitable for "Streaming", such as newline-delimited JSON or Server-Sent Events.

$ curl "http://localhost:8082/lots-of-replies?greeting=John%20Doe" -H "Accept: application/x-ndjson"
{"reply":"[00000] Hello John Doe!"}
{"reply":"[00001] Hello John Doe!"}
{"reply":"[00002] Hello John Doe!"}
{"reply":"[00003] Hello John Doe!"}
{"reply":"[00004] Hello John Doe!"}
{"reply":"[00005] Hello John Doe!"}
{"reply":"[00006] Hello John Doe!"}
{"reply":"[00007] Hello John Doe!"}
{"reply":"[00008] Hello John Doe!"}
{"reply":"[00009] Hello John Doe!"}
$ curl "http://localhost:8082/lots-of-replies?greeting=hello" -H "Accept: text/event-stream"
data:{"reply":"[00000] Hello John Doe!"}

data:{"reply":"[00001] Hello John Doe!"}

data:{"reply":"[00002] Hello John Doe!"}

data:{"reply":"[00003] Hello John Doe!"}

data:{"reply":"[00004] Hello John Doe!"}

data:{"reply":"[00005] Hello John Doe!"}

data:{"reply":"[00006] Hello John Doe!"}

data:{"reply":"[00007] Hello John Doe!"}

data:{"reply":"[00008] Hello John Doe!"}

data:{"reply":"[00009] Hello John Doe!"}

Let's also implement the server side with a Reactor-based API.

cat<<EOF >src/main/java/com/example/HelloService.java
package com.example;

import com.example.proto.HelloRequest;
import com.example.proto.HelloResponse;
import com.example.proto.ReactorHelloServiceGrpc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
public class HelloService extends ReactorHelloServiceGrpc.HelloServiceImplBase {

    private final Logger log = LoggerFactory.getLogger(HelloService.class);

    @Override
    public Mono<HelloResponse> sayHello(Mono<HelloRequest> request) {
        log.info("sayHello");
        return request
            .map(req -> HelloResponse.newBuilder().setReply(String.format("Hello %s!", req.getGreeting())).build());
    }

    // Alternatively:
    //@Override
    //public Mono<HelloResponse> sayHello(HelloRequest request) {
    //	log.info("sayHello");
    //	return Mono
    //		.just(HelloResponse.newBuilder().setReply(String.format("Hello %s!", request.getGreeting())).build());
    //}

    @Override
    public Flux<HelloResponse> lotsOfReplies(Mono<HelloRequest> request) {
        log.info("lotsOfReplies");
        return request.flatMapMany(req -> Flux.range(0, 10)
            .map(i -> HelloResponse.newBuilder()
                .setReply(String.format("[%05d] Hello %s!", i, req.getGreeting()))
                .build()));
    }

}
EOF

In this example, we don't have full-fledged streaming processing, so the difference may not be obvious, but when implementing complex streaming processing, using Reactor makes it easier to write the logic.

Even as is, the existing tests should pass.

./mvnw test

Let's also rewrite the test code to use the Reactor-based API. The @ImportGrpcClients setting is required.

cat<<'EOF' >src/test/java/com/example/HelloServiceTest.java
package com.example;

import com.example.proto.HelloRequest;
import com.example.proto.HelloResponse;
import com.example.proto.ReactorHelloServiceGrpc;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.grpc.client.ImportGrpcClients;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = "spring.grpc.client.default-channel.address=0.0.0.0:${local.server.port}")
@ImportGrpcClients(types = ReactorHelloServiceGrpc.ReactorHelloServiceStub.class)
class HelloServiceTest {

    @Autowired
    ReactorHelloServiceGrpc.ReactorHelloServiceStub stub;

    @Test
    void sayHello() {
        Mono<HelloResponse> response = this.stub.sayHello(HelloRequest.newBuilder().setGreeting("John Doe").build());
        StepVerifier.create(response)
            .assertNext(r -> assertThat(r.getReply()).isEqualTo("Hello John Doe!"))
            .verifyComplete();
    }

    @Test
    void lotsOfReplies() {
        Flux<HelloResponse> response = this.stub
            .lotsOfReplies(HelloRequest.newBuilder().setGreeting("John Doe").build());
        StepVerifier.create(response)
            .assertNext(r -> assertThat(r.getReply()).isEqualTo("[00000] Hello John Doe!"))
            .assertNext(r -> assertThat(r.getReply()).isEqualTo("[00001] Hello John Doe!"))
            .assertNext(r -> assertThat(r.getReply()).isEqualTo("[00002] Hello John Doe!"))
            .assertNext(r -> assertThat(r.getReply()).isEqualTo("[00003] Hello John Doe!"))
            .assertNext(r -> assertThat(r.getReply()).isEqualTo("[00004] Hello John Doe!"))
            .assertNext(r -> assertThat(r.getReply()).isEqualTo("[00005] Hello John Doe!"))
            .assertNext(r -> assertThat(r.getReply()).isEqualTo("[00006] Hello John Doe!"))
            .assertNext(r -> assertThat(r.getReply()).isEqualTo("[00007] Hello John Doe!"))
            .assertNext(r -> assertThat(r.getReply()).isEqualTo("[00008] Hello John Doe!"))
            .assertNext(r -> assertThat(r.getReply()).isEqualTo("[00009] Hello John Doe!"))
            .verifyComplete();
    }

}
EOF

Please confirm that the tests pass here as well.

./mvnw test

You can use either the Blocking or Reactor client stub for gRPC testing.

Native Image Build

Spring gRPC supports native image builds with GraalVM. Native image builds greatly reduce startup time and memory usage.

Use GraalVM to build native images for both server and client with the following command:

./mvnw native:compile -Pnative

Once the native image build succeeds, start the server and client with the following commands.

$ ./target/demo-grpc-server

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.4.5)

2025-05-18T10:16:30.110+09:00  INFO 46360 --- [demo-grpc-server] [           main] [                                                 ] com.example.DemoGrpcServerApplication    : Starting AOT-processed DemoGrpcServerApplication using Java 21.0.6 with PID 46360 (/private/tmp/demo-grpc-server/target/demo-grpc-server started by toshiaki in /private/tmp/demo-grpc-server)
2025-05-18T10:16:30.110+09:00  INFO 46360 --- [demo-grpc-server] [           main] [                                                 ] com.example.DemoGrpcServerApplication    : No active profile set, falling back to 1 default profile: "default"
2025-05-18T10:16:30.121+09:00  INFO 46360 --- [demo-grpc-server] [           main] [                                                 ] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2025-05-18T10:16:30.122+09:00  INFO 46360 --- [demo-grpc-server] [           main] [                                                 ] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2025-05-18T10:16:30.122+09:00  INFO 46360 --- [demo-grpc-server] [           main] [                                                 ] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.40]
2025-05-18T10:16:30.127+09:00  INFO 46360 --- [demo-grpc-server] [           main] [                                                 ] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2025-05-18T10:16:30.127+09:00  INFO 46360 --- [demo-grpc-server] [           main] [                                                 ] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 16 ms
2025-05-18T10:16:30.137+09:00  INFO 46360 --- [demo-grpc-server] [           main] [                                                 ] toConfiguration$GrpcServletConfiguration : Registering gRPC service: com.example.HelloService
2025-05-18T10:16:30.137+09:00  INFO 46360 --- [demo-grpc-server] [           main] [                                                 ] toConfiguration$GrpcServletConfiguration : Registering gRPC service: grpc.reflection.v1.ServerReflection
2025-05-18T10:16:30.137+09:00  INFO 46360 --- [demo-grpc-server] [           main] [                                                 ] toConfiguration$GrpcServletConfiguration : Registering gRPC service: grpc.health.v1.Health
2025-05-18T10:16:30.150+09:00  WARN 46360 --- [demo-grpc-server] [           main] [                                                 ] i.m.c.i.binder.jvm.JvmGcMetrics          : GC notifications will not be available because no GarbageCollectorMXBean of the JVM provides any. GCs=[young generation scavenger, complete scavenger]
2025-05-18T10:16:30.152+09:00  INFO 46360 --- [demo-grpc-server] [           main] [                                                 ] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 3 endpoints beneath base path '/actuator'
2025-05-18T10:16:30.154+09:00  INFO 46360 --- [demo-grpc-server] [           main] [                                                 ] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2025-05-18T10:16:30.155+09:00  INFO 46360 --- [demo-grpc-server] [           main] [                                                 ] com.example.DemoGrpcServerApplication    : Started DemoGrpcServerApplication in 0.055 seconds (process running for 0.065)
$ ./target/demo-grpc-client 

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.4.5)

2025-05-18T10:18:14.001+09:00  INFO 46573 --- [demo-grpc-client] [           main] [                                                 ] com.example.DemoGrpcClientApplication    : Starting AOT-processed DemoGrpcClientApplication using Java 21.0.6 with PID 46573 (/private/tmp/demo-grpc-client/target/demo-grpc-client started by toshiaki in /private/tmp/demo-grpc-client)
2025-05-18T10:18:14.001+09:00  INFO 46573 --- [demo-grpc-client] [           main] [                                                 ] com.example.DemoGrpcClientApplication    : No active profile set, falling back to 1 default profile: "default"
2025-05-18T10:18:14.017+09:00  INFO 46573 --- [demo-grpc-client] [           main] [                                                 ] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8082 (http)
2025-05-18T10:18:14.018+09:00  INFO 46573 --- [demo-grpc-client] [           main] [                                                 ] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2025-05-18T10:18:14.018+09:00  INFO 46573 --- [demo-grpc-client] [           main] [                                                 ] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.40]
2025-05-18T10:18:14.024+09:00  INFO 46573 --- [demo-grpc-client] [           main] [                                                 ] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2025-05-18T10:18:14.024+09:00  INFO 46573 --- [demo-grpc-client] [           main] [                                                 ] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 23 ms
2025-05-18T10:18:14.036+09:00  INFO 46573 --- [demo-grpc-client] [           main] [                                                 ] toConfiguration$GrpcServletConfiguration : Registering gRPC service: grpc.reflection.v1.ServerReflection
2025-05-18T10:18:14.036+09:00  INFO 46573 --- [demo-grpc-client] [           main] [                                                 ] toConfiguration$GrpcServletConfiguration : Registering gRPC service: grpc.health.v1.Health
2025-05-18T10:18:14.052+09:00  WARN 46573 --- [demo-grpc-client] [           main] [                                                 ] i.m.c.i.binder.jvm.JvmGcMetrics          : GC notifications will not be available because no GarbageCollectorMXBean of the JVM provides any. GCs=[young generation scavenger, complete scavenger]
2025-05-18T10:18:14.054+09:00  INFO 46573 --- [demo-grpc-client] [           main] [                                                 ] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 3 endpoints beneath base path '/actuator'
2025-05-18T10:18:14.056+09:00  INFO 46573 --- [demo-grpc-client] [           main] [                                                 ] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8082 (http) with context path '/'
2025-05-18T10:18:14.057+09:00  INFO 46573 --- [demo-grpc-client] [           main] [                                                 ] com.example.DemoGrpcClientApplication    : Started DemoGrpcClientApplication in 0.074 seconds (process running for 0.094

Startup time is greatly reduced.


We tried out some basic features of Spring gRPC. Integrating gRPC with Spring has become much easier.
Spring gRPC also supports Spring Security. Next, I'd like to try authentication using Spring Security.

Found a mistake? Update the entry.
Share this article: