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:
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.
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:
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).
2 COMMENTS