1. Overview
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.
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.
In this tutorial, we’ll learn how to implement gRPC service providers and consumers with Spring Boot.
2. Challenges
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:
- 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
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.
In this sample, we design just a simple HelloWorld API with a single Proto file:
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
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.
3.2. Service Provider
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:
grpc:
server:
port: 9090
Then, we need to implement the service and annotate it with @GrpcService:
@GrpcService
public class HelloWorldController extends HelloWorldServiceGrpc.HelloWorldServiceImplBase {
@Override
public StreamObserver<HelloWorldRequest> sayHello(
StreamObserver<HelloWorldResponse> responseObserver
) {
// ...
}
}
3.3. Service Consumer
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:
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:
@GrpcClient("hello")
HelloWorldServiceGrpc.HelloWorldServiceStub stub;
4. Pitfalls
Implementing and consuming a gRPC service with Spring Boot is pretty easy. But there are some pitfalls that we should be aware of.
4.1. SSL-Handshake
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.
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.
4.2. Consumer Injection Without @Autowired
We implement the consumer by injecting a client object into our Spring component:
@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.
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
The data types generated by protoc can fail when invoking setters with null values:
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:
@Mapper(
componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS
)
public interface HelloWorldMapper {
HelloWorldResponse map(HelloWorldMessage message);
}
4.4. Testing
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.
4.5. Native Images
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.
5. Conclusion
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.
As usual, all the code implementations are available over on GitHub.