Introduction to gRPC with Spring Boot

Introduction to gRPC with Spring Boot

In this article, you will learn how to implement Spring Boot apps that communicate over gRPC. gRPC is a modern open-source Remote Procedure Call (RPC) framework that can run in any environment. By default, it uses Google’s Protocol Buffer for serializing and deserializing structured data. Of course, we can also switch to other data formats such as JSON. In order to simplify our adventure with gRPC and Spring Boot, we will use a dedicated starter for that. Since there is no officially supported starter for integration between gRPC and Spring Boot, we will choose the most popular third-party project. It has around 3.1k stars on GitHub. You can find detailed documentation about its features here.

If you are looking for a quick intro to the Protocol Buffers with Java you can read my article available here. It was written more than 5 years ago. However, I updated it in recent days. I also updated the repository with code examples to the latest Spring Boot 3 and Java 17.

Source Code

If you would like to try it by yourself, you can always take a look at my source code. In order to do that, you need to clone my GitHub repository. It contains four apps. Two of them account-service and customer-service are related to my previous article, which introduces Protocol Buffers with Java. For the current article, please refer to another two apps account-service-grpc and customer-service-grpc. They are pretty similar to the corresponding apps but use our third-party Spring Boot and gRPC communication instead of REST. Also, they need to use Spring Boot 2, because our third-party starter still doesn’t support Spring Boot 3. Anyway, once you clone the repository just follow my instructions 🙂

Generate Model Classes and Services for gRPC

In the first step, we will generate model classes and gRPC services using the .proto manifests. We need to include some additional Protobuf schemas to use the google.protobuf.* package (1). Our gRPC service will provide methods for searching accounts using various criteria and a single method for adding a new account (2). Those methods will use primitives from the google.protobuf.* package and model classes defined inside the .proto file as messages. There are two messages defined. The Account message represents a single model class. It contains three fields: id, number, and customer_id (3). The Accounts message contains a list of Account objects (4).

syntax = "proto3";

package model;

option java_package = "pl.piomin.services.grpc.account.model";
option java_outer_classname = "AccountProto";

// (1)
import "empty.proto";
import "wrappers.proto";

// (2)
service AccountsService {
  rpc FindByNumber(google.protobuf.StringValue) returns (Account) {}
  rpc FindByCustomer(google.protobuf.Int32Value) returns (Accounts) {}
  rpc FindAll(google.protobuf.Empty) returns (Accounts) {}
  rpc AddAccount(Account) returns (Account) {}
}

// (3)
message Account {
  int32 id = 1;
  string number = 2;
  int32 customer_id = 3;
}

// (4)
message Accounts {
  repeated Account account = 1;
}

As you probably remember, there are two sample Spring Boot apps. Let’s take a look at the .proto schema for the second app customer-service-grpc. It is a little bit more complicated than the previous definition. Our gRPC service will also provide several methods for searching objects and a single method for adding a new customer (1). The customer-service-grpc is communicating with the account-service-grpc app, so we need to generate Account and Accounts messages (2). Of course, you may create an additional interface module with generated Protobuf classes and share it across both our sample apps. Finally, we have to define our model classes. The Customer class contains three primitive fields id, pesel, name, the enum type, and a list of accounts assigned to the particular customer (3). There is also the Customers message containing a list of Customer objects (4).

syntax = "proto3";

package model;

option java_package = "pl.piomin.services.grpc.customer.model";
option java_outer_classname = "CustomerProto";

import "empty.proto";
import "wrappers.proto";

// (1)
service CustomersService {
  rpc FindByPesel(google.protobuf.StringValue) returns (Customer) {}
  rpc FindById(google.protobuf.Int32Value) returns (Customer) {}
  rpc FindAll(google.protobuf.Empty) returns (Customers) {}
  rpc AddCustomer(Customer) returns (Customer) {}
}

// (2)
message Account {
  int32 id = 1;
  string number = 2;
  int32 customer_id = 3;
}

message Accounts {
  repeated Account account = 1;
}

// (3)
message Customer {
  int32 id = 1;
  string pesel = 2;
  string name = 3;
  CustomerType type = 4;
  repeated Account accounts = 5;
  enum CustomerType {
    INDIVIDUAL = 0;
    COMPANY = 1;
  }
}

// (4)
message Customers {
  repeated Customer customers = 1;
}

In order to generate Java classes from .proto schemas, we will use the Maven plugin. You can between some available plugins for that. My choice fell on the protoc-jar-maven-plugin plugin. In the configuration, we need to override the default location of our .proto schema to src/main/proto. We also need to include additional Protobuf schemas we used in the .proto manifest using the includeDirectories tag. Those manifests are located inside the src/main/proto-imports directory. The output target directory is src/main/generated. By default, the plugin does not generate gRPC services. In order to enable it we need to include the outputTarget with the grpc-java type. For generating classes we will use the protoc-gen-grpc-java library.

<plugin>
  <groupId>com.github.os72</groupId>
  <artifactId>protoc-jar-maven-plugin</artifactId>
  <version>3.11.4</version>
  <executions>
    <execution>
      <phase>generate-sources</phase>
      <goals>
        <goal>run</goal>
      </goals>
      <configuration>
        <addProtoSources>all</addProtoSources>
        <includeMavenTypes>direct</includeMavenTypes>
        <outputDirectory>src/main/generated</outputDirectory>
        <inputDirectories>
          <include>src/main/proto</include>
        </inputDirectories>
        <includeDirectories>
          <include>src/main/proto-imports</include>
        </includeDirectories>
        <outputTargets>
           <outputTarget>
             <type>java</type>
             <outputDirectory>src/main/generated</outputDirectory>
           </outputTarget>
           <outputTarget>
             <type>grpc-java</type>
             <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.57.2</pluginArtifact>  
             <outputDirectory>src/main/generated</outputDirectory>
          </outputTarget>
        </outputTargets>
      </configuration>
    </execution>
  </executions>
</plugin>

We will also attach the generated Java code under the src/main/generated as a source directory with the build-helper-maven-plugin Maven plugin.

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>build-helper-maven-plugin</artifactId>
  <executions>
    <execution>
      <id>add-source</id>
      <phase>generate-sources</phase>
      <goals>
        <goal>add-source</goal>
      </goals>
      <configuration>
        <sources>
          <source>src/main/generated</source>
        </sources>
      </configuration>
    </execution>
  </executions>
</plugin>

Once you execute the mvn clean package command Maven will generate the required Java classes. Here’s the final structure of directories in the account-service-grpc app after generating the Java classes.

$ tree
.
├── pom.xml
└── src
    ├── main
    │   ├── generated
    │   │   └── pl
    │   │       └── piomin
    │   │           └── services
    │   │               └── grpc
    │   │                   └── account
    │   │                       └── model
    │   │                           ├── AccountProto.java
    │   │                           └── AccountsServiceGrpc.java
    │   ├── java
    │   │   └── pl
    │   │       └── piomin
    │   │           └── services
    │   │               └── grpc
    │   │                   └── account
    │   │                       ├── AccountApplication.java
    │   │                       ├── repository
    │   │                       │   └── AccountRepository.java
    │   │                       └── service
    │   │                           └── AccountsService.java
    │   ├── proto
    │   │   └── account.proto
    │   ├── proto-imports
    │   │   ├── empty.proto
    │   │   └── wrappers.proto
    │   └── resources
    └── test
        └── java
            └── pl
                └── piomin
                    └── services
                        └── grpc
                            └── account
                                └── AccountServicesTests.java

Using the gRPC Spring Boot Starter

Once we generate the required Protobuf model classes and gRPC stubs we can proceed to the implementation. In the first step, we need to include the following Spring Boot starter:

<dependency>
  <groupId>net.devh</groupId>
  <artifactId>grpc-server-spring-boot-starter</artifactId>
  <version>2.14.0.RELEASE</version>
</dependency>

Then we have to create the gRPC service implementation class. It needs to extend the AccountsServiceImplBase generated based on the .proto declaration. We also need to annotate the whole class with the @GrpcService (1). After that, we will override all the methods exposed over gRPC. Our service is using a simple in-memory repository (2). Each method provides a parameter object and the io.grpc.stub.StreamObserver class used for returning the responses in a reactive way (3) (4).

@GrpcService // (1)
public class AccountsService extends AccountsServiceGrpc.AccountsServiceImplBase {

    @Autowired
    AccountRepository repository; // (2)

    @Override
    public void findByNumber(StringValue request, StreamObserver<AccountProto.Account> responseObserver) { // (3)
        AccountProto.Account a = repository.findByNumber(request.getValue());
        responseObserver.onNext(a); # (4)
        responseObserver.onCompleted();
    }

    @Override
    public void findByCustomer(Int32Value request, StreamObserver<AccountProto.Accounts> responseObserver) {
        List<AccountProto.Account> accounts = repository.findByCustomer(request.getValue());
        AccountProto.Accounts a = AccountProto.Accounts.newBuilder().addAllAccount(accounts).build();
        responseObserver.onNext(a);
        responseObserver.onCompleted();
    }

    @Override
    public void findAll(Empty request, StreamObserver<AccountProto.Accounts> responseObserver) {
        List<AccountProto.Account> accounts = repository.findAll();
        AccountProto.Accounts a = AccountProto.Accounts.newBuilder().addAllAccount(accounts).build();
        responseObserver.onNext(a);
        responseObserver.onCompleted();
    }

    @Override
    public void addAccount(AccountProto.Account request, StreamObserver<AccountProto.Account> responseObserver) {
        AccountProto.Account a = repository.add(request.getCustomerId(), request.getNumber());
        responseObserver.onNext(a);
        responseObserver.onCompleted();
    }
}

Here’s the AccountRepository implementation:

public class AccountRepository {

    List<AccountProto.Account> accounts;
    AtomicInteger id;

    public AccountRepository(List<AccountProto.Account> accounts) {
        this.accounts = accounts;
        this.id = new AtomicInteger();
        this.id.set(accounts.size());
    }

    public List<AccountProto.Account> findAll() {
        return accounts;
    }

    public List<AccountProto.Account> findByCustomer(int customerId) {
        return accounts.stream().filter(it -> it.getCustomerId() == customerId).toList();
    }

    public AccountProto.Account findByNumber(String number) {
        return accounts.stream()
                .filter(it -> it.getNumber().equals(number))
                .findFirst()
                .orElseThrow();
    }

    public AccountProto.Account add(int customerId, String number) {
        AccountProto.Account a = AccountProto.Account.newBuilder()
                .setId(id.incrementAndGet())
                .setCustomerId(customerId)
                .setNumber(number)
                .build();
        return a;
    }

}

We are adding some test data on startup. Here’s our app main class:

@SpringBootApplication
public class AccountApplication {

    public static void main(String[] args) {
        SpringApplication.run(AccountApplication.class, args);
    }

    @Bean
    AccountRepository repository() {
        List<AccountProto.Account> accounts = new ArrayList<>();
        accounts.add(AccountProto.Account.newBuilder().setId(1).setCustomerId(1).setNumber("111111").build());
        accounts.add(AccountProto.Account.newBuilder().setId(2).setCustomerId(2).setNumber("222222").build());
        accounts.add(AccountProto.Account.newBuilder().setId(3).setCustomerId(3).setNumber("333333").build());
        accounts.add(AccountProto.Account.newBuilder().setId(4).setCustomerId(4).setNumber("444444").build());
        accounts.add(AccountProto.Account.newBuilder().setId(5).setCustomerId(1).setNumber("555555").build());
        accounts.add(AccountProto.Account.newBuilder().setId(6).setCustomerId(2).setNumber("666666").build());
        accounts.add(AccountProto.Account.newBuilder().setId(7).setCustomerId(2).setNumber("777777").build());
        return new AccountRepository(accounts);
    }
}

Before starting the app, we will also include Spring Boot Actuator to expose some metrics related to gRPC. We will expose under a different port than the gRPC service, so we also need to include the Spring Boot Web starter:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

In the application.yml file we should enable the metrics endpoint:

spring.application.name: account-service-grpc

management.endpoints.web.exposure.include: metrics
management.endpoint.metrics.enabled: true

By default, the gRPC services are available under the 9090 port. We can override that number using the grpc.server.port property. Set the port to 0 to use a free random port. Let’s start our sample app:

spring-boot-grpc-startup

Calling gRPC Services

We can use the grpcurl CLI tool to call the gRPC services exposed by our sample app. By default, the gRPC server will be started on a port 9090 using PLAINTEXT mode. In order to print a list of available services we need to execute the following command:

$ grpcurl --plaintext localhost:9090 list
grpc.health.v1.Health
grpc.reflection.v1alpha.ServerReflection
model.AccountsService

Then, let’s print the list of methods exposed by the model.AccountService:

$ grpcurl --plaintext localhost:9090 list model.AccountsService
model.AccountsService.AddAccount
model.AccountsService.FindAll
model.AccountsService.FindByCustomer
model.AccountsService.FindByNumber

We can also print the details about each method by using the describe keyword in the command:

$ grpcurl --plaintext localhost:9090 describe model.AccountsService.FindByNumber
model.AccountsService.FindByNumber is a method:
rpc FindByNumber ( .google.protobuf.StringValue ) returns ( .model.Account );

Now, let’s call the endpoint described with the command visible above. The name of our method is model.AccountsService.FindByNumber. We are also setting the input string parameter with the 111111 value.

$ grpcurl --plaintext -d '"111111"' localhost:9090 model.AccountsService.FindByNumber
{
  "id": 1,
  "number": "111111",
  "customer_id": 1
}

After that, we can take a look at the model.AccountsService.FindByNumber gRPC method. It takes an integer as the input parameter and returns a list of objects.

$ grpcurl --plaintext -d '1' localhost:9090 model.AccountsService.FindByCustomer
{
  "account": [
    {
      "id": 1,
      "number": "111111",
      "customer_id": 1
    },
    {
      "id": 5,
      "number": "555555",
      "customer_id": 1
    }
  ]
}

Finally, we can call the method for adding a new account. It takes the JSON object as the input parameter. Then it will return a newly created Account object with the incremented id field.

$ grpcurl --plaintext -d '{"customer_id": 6, "number": "888888"}' localhost:9090 model.AccountsService.AddAccount
{
  "id": 8,
  "number": "888888",
  "customer_id": 6
}

The gRPC Spring Boot starter adds three additional metrics to the Actuator.

We can display a number of requests per a gRPC method. Here’s the request and response for the FindByNumber method.

spring-boot-grpc-metrics

We can also display an average processing time per method as shown below.

Testing the gRPC Services

In the previous section, we ran the app and tested gRPC services manually using the grpcurl CLI tool. However, we can also implement unit or integrated tests based on the Spring Boot Test module. We will create the integration test for our app with the gRPC client. In order to do that, we need to include the following three dependencies in Maven pom.xml:

<dependency>
  <groupId>io.grpc</groupId>
  <artifactId>grpc-testing</artifactId>
  <version>1.51.0</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>net.devh</groupId>
  <artifactId>grpc-client-spring-boot-starter</artifactId>
  <version>2.14.0.RELEASE</version>
  <scope>test</scope>
</dependency>

In the test implementation visible below, we need to enable an “in-process” server (1) and disable an external server (2). Then we have to configure the client to connect to the “in-process server” (3). We will use the gRPC client already generated during the Maven build. It is available as the AccountsServiceBlockingStub class. We just to inject and annotate it properly with the @GrpcClient (4). After that, we may use the client stub to call our gRPC services (5).

@SpringBootTest(properties = {
        "grpc.server.inProcessName=test", // (1)
        "grpc.server.port=-1", // (2)
        "grpc.client.inProcess.address=in-process:test" // (3)
})
@DirtiesContext
public class AccountServicesTests {

    @GrpcClient("inProcess") // (4)
    AccountsServiceGrpc.AccountsServiceBlockingStub service;

    @Test
    void shouldFindAll() {
        AccountProto.Accounts a = service.findAll(Empty.newBuilder().build()); // (5)
        assertNotNull(a);
        assertFalse(a.getAccountList().isEmpty());
    }

    @Test
    void shouldFindByCustomer() {
        AccountProto.Accounts a = service.findByCustomer(Int32Value.newBuilder().setValue(1).build());
        assertNotNull(a);
        assertFalse(a.getAccountList().isEmpty());
    }

    @Test
    void shouldFindByNumber() {
        AccountProto.Account a = service.findByNumber(StringValue.newBuilder().setValue("111111").build());
        assertNotNull(a);
        assertNotEquals(0, a.getId());
    }

    @Test
    void shouldAddAccount() {
        AccountProto.Account a = AccountProto.Account.newBuilder()
                .setNumber("123456")
                .setCustomerId(10)
                .build();

        a = service.addAccount(a);
        assertNotNull(a);
        assertNotEquals(0, a.getId());
    }

}

Here are the results of our tests:

Communication Between gRPC Microservices

In this section, we will switch to the customer-service-grpc app. The same as with the previous app, we need to generate the classes and gRPC service stubs with the Maven command mvn clean package. The service implementation is also similar to the account-service-grpc. However, this time, we use a client to call the external gRPC method. Here’s the implementation of the @GrpcService. As you see, we are injecting the AccountClient bean and then using it to call the gRPC method exposed by the account-service-grpc app (1). Then we use the client bean to find the account assigned to the particular customer (2).

@GrpcService
public class CustomersService extends CustomersServiceGrpc.CustomersServiceImplBase {

    @Autowired
    CustomerRepository repository;
    @Autowired
    AccountClient accountClient; // (1)

    @Override
    public void findById(Int32Value request, StreamObserver<CustomerProto.Customer> responseObserver) {
        CustomerProto.Customer c = repository.findById(request.getValue());
        CustomerProto.Accounts a = accountClient.getAccountsByCustomerId(c.getId()); // (2)
        List<CustomerProto.Account> l = a.getAccountList();
        c = CustomerProto.Customer.newBuilder(c).addAllAccounts(l).build();
        responseObserver.onNext(c);
        responseObserver.onCompleted();
    }

    @Override
    public void findByPesel(StringValue request, StreamObserver<CustomerProto.Customer> responseObserver) {
        CustomerProto.Customer c = repository.findByPesel(request.getValue());
        responseObserver.onNext(c);
        responseObserver.onCompleted();
    }

    @Override
    public void findAll(Empty request, StreamObserver<CustomerProto.Customers> responseObserver) {
        List<CustomerProto.Customer> customerList = repository.findAll();
        CustomerProto.Customers c = CustomerProto.Customers.newBuilder().addAllCustomers(customerList).build();
        responseObserver.onNext(c);
        responseObserver.onCompleted();
    }

    @Override
    public void addCustomer(CustomerProto.Customer request, StreamObserver<CustomerProto.Customer> responseObserver) {
        CustomerProto.Customer c = repository.add(request.getType(), request.getName(), request.getPesel());
        responseObserver.onNext(c);
        responseObserver.onCompleted();
    }
}

Now, let’s take a look at the implementation of the AccountClient class. We use the generated client stub to call the external gRPC method (1). Pay attention to the value inside inside the annotation. It is the name of our client.

@Service
public class AccountClient {

    private static final Logger LOG = LoggerFactory.getLogger(AccountClient.class);

    @GrpcClient("account-service-grpc") // (1)
    AccountsServiceGrpc.AccountsServiceBlockingStub stub;

    public CustomerProto.Accounts getAccountsByCustomerId(int customerId) {
        try {
            return stub.findByCustomer(Int32Value.newBuilder().setValue(customerId).build());
        } catch (final StatusRuntimeException e) {
            LOG.error("Error in communication", e);
            return null;
        }
    }
}

The last thing we need to do is to provide the address of the target service. Fortunately, the gRPC Spring Boot supports service discovery with Spring Cloud. We will use Eureka as a discovery server. Therefore both our sample apps need to include the Spring Cloud Eureka client.

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

We also need to add the dependencyManagement section in the pom.xml containing the version of Spring Cloud we use.

<dependencyManagement>
  <dependencies>
    <dependency>
     <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-dependencies</artifactId>
       <version>2021.0.8</version>
       <type>pom</type>
       <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

In order to avoid port conflicts with the account-service-grpc we will override the default gRPC and HTTP (Actuator) ports. We also need to provide several configuration settings for the @GrpcClient. First of all, we should have the same name as set inside the @GrpcClient annotation in the AccountClient class. The client communicates over the plaintext protocol and reads the address of the target service from the discovery server under the name set in the discovery:/// field.

server.port: 8081
grpc.server.port: 9091

grpc:
  client:
    account-service-grpc:
      address: 'discovery:///account-service-grpc'
      enableKeepAlive: true
      keepAliveWithoutCalls: true
      negotiationType: plaintext

Finally, we can run a discovery server and our two sample microservices. The Eureka server is available in our repository inside the discovery-server directory. Once you run you can go to the UI dashboard available under the http://localhost:8761 address.

Then run both our sample Spring Boot gRPC microservices. You can run all the apps using the following Maven command:

$ mvn spring-boot:run

Finally, let’s call the customer-service-grpc method that communicates with account-service-grpc. We use the grpcurl tool once again. As you see it returns a list of accounts inside the Customer object:

spring-boot-grpc-client

Final Thoughts

The gRPC Spring Boot Starter provides several useful features that simplify developer life. We can easily create services with @GrpcService, clients with @GrpcClient, or integrate gRPC with Spring Boot Actuator metrics and Spring Cloud discovery. However, there are also some drawbacks. The library is not very actively developed. There are about 2-3 releases per year, and there is still no support for Spring Boot 3. If you are looking for a more actively developed starter (but less popular) you can try the following one (I found out about it thanks to the discussion on Reddit after releasing the article).

Leave a Reply