Building Microservices With gRPC and Java

Background

As the world is moving more and more toward microservices, the need for low latency and scalable frameworks to build these microservices has also increased. To overcome the need, lots of effort is being made from various tools and frameworks providers to cater to the requirements. Also learning from the experience of building large-scale microservice applications over decades, technology majors are sharing their knowledge of reusable components so that others can build with the same scale and performance.

What Is gRPC

gRPC is an open-source framework (created by Google) which is a universal RPC framework for building network applications at scale with performance. Its implementation is available in multiple languages ​​and it supports cross-platform communication.

Use Case

gRPC is a right fit for RPC communication between service to service. Here, we will use Java implementation to implement microservice and related frameworks to make it fully functional. To make it accessible to the external world, we will be creating a wrapper REST service that will use gRPC clients to communicate to gRPC services.

Preparation

We need to set up a base environment to build and run the sample. The basic requirement is to have Java and Maven installed. Other dependencies like gRPC tooling and server runtime libraries will automatically be downloaded during the build process. Refer to the code below for the core dependency needed for building the application.

<dependency>
    <groupId>io.github.lognet</groupId>
    <artifactId>grpc-spring-boot-starter</artifactId>
    <version>4.5.0</version>
</dependency>

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

<dependency> 
    <groupId>org.mapstruct</groupId> 
    <artifactId>mapstruct</artifactId> 
    <version>${mapstruct.version}</version> 
</dependency>

Core Concepts

gRPC is basically a wire protocol agnostic of platform and encoding. Meaning, you can use any type of encoding like binary, JSON, XML, etc., but the recommended way is to use “protobuf,” which supports binary encoding using a specialized serializing/deserializing mechanism. Its pluggable design allows users to extend it to support required platforms and stacks.

The core construct of protobuf is proto IDL (Interface Definition Language), which defines message type and service definition. It also provides tooling to generate model class and service interfaces for the required platform.

Message Type

We can start with defining a proto definition for the message type in the .proto file. Look at the example below.

message AccountProto {    
     int32 accId = 1;
     string accName = 2;     
     string accBalance = 3;
     bool status = 4;     
     ...
}

For the complete reference on data type and keywords, refer to the language guide.

Protobuf provides a tool to generate code for model classes out of a message definition that’s applicable to your platform/language. The following command will generate the account class in Java based on the given message definition.

$ > protoc -I=proto-demo --java_out=proto-demo account.proto

Service Definition

The gRPC service definition is a set of operations that need to be performed on the defined message type. Those operations can take one of four communication forms:

  1. Unary RPC — It is the simplest form of communication. It is synchronous in nature and allows users to send requests in blocking mode and wait for the response until the server finishes the processing.
  2. Server streaming RPC — In this form, clients send the data in one shot but the server returns a response in the form of streams.
  3. Client streaming RPC — Unlike server streaming, in this form, clients send the requested data in a stream and the server returns the data as a whole.
  4. Bi-directional streaming RPC — In this form, both the server and clients support streaming the data on request and with a response.

The sample service definition for your message type with standard CRUD operation would take the following input:

service AccountService {
    rpc GetAccount (google.protobuf.Int32Value) returns (AccountProto);
    rpc GetAllAccounts (google.protobuf.Empty) returns (stream AccountProto);
    rpc CreateAccount (AccountProto) returns (AccountProto);
    rpc UpdateAccount (AccountProto) returns (AccountProto);
    rpc DeleteAccount (google.protobuf.Int32Value) returns (google.protobuf.Empty);
}

The extension tool provided by the gRPC-Java implementation helps to generate service interfaces that need to be implemented by users according to the domain logic and server stubs that will be used by clients to make calls to services.

$ > protoc -I=grpc-demosrcmainproto --java_out=grpc-demosrcmainproto account.proto

Standard Server and Client

The gRPC-Java library offers a reactive server implementation (based on Netty) to deploy your services and a blocking/non-blocking client implementation to connect your services from other services.

You need to register your service implementation and start the server programmatically.

server = ServerBuilder.forPort(port), .addService(new GreeterImpl()).build().start();

Find the GitHub reference here.

To connect to the service onto the Netty-based gRPC server, you need to create a message channel and connect it with generated server stub to make the call.

ManagedChannel channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); 
blockingStub = GreeterGrpc.newBlockingStub(channel); 
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
HelloReply response = blockingStub.sayHello(request); 

Find the GitHub reference here.

Web Client

There is also a gRPC web module that allows web clients to access your gRPC services seamlessly. Their earlier version supported connecting web clients through a reverse proxy, but now it is possible to do it without a proxy in between.

Another way is to have a wrapper service layer exposed to the external-facing world using the REST/GraphQL protocol and connected through a gRPC client.

Data Mapping and Persistence

We may need to add another layer on top of it to make a fully functional domain service using data access libraries like Hibernate. As with Hibernate or any data library, required entities need to be decorated in a certain fashion, and that may not be feasible with the model generated by protobuf models. So, we may need some mapping logic to convert model classes to entity classes. One such good library is MapStruct, which does automatic mapping based on bean conventions. We just need to provide mapping interfaces as:

@Mapper
public interface AccountProtoMapper {
        Account map(AccountProto accountProto);
        AccountProto map(Account account);
}

Third-Party Support

To further ease the whole build and runtime environment, there are some popular third-party Maven plugins and libraries which also help.

1. Run Proto Tool Integrated With Build

The protocol buffers Maven plugin (by Xolstice) runs proto tools and their extension along with building and generating source code from .proto files. It also attached .proto files as resources of the project. Refer to the sample configuration to generate Java code.

<configuration>
    <protocArtifact>com.google.protobuf:protoc:${protoc.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>

The reference can be found here.

2. Generate Mapper Classes

The MapStruct library provides support for generating mapper classes of message types that can convert messages from/to entities of classes. It provides an annotation processor that generates a mapper class by analyzing the specified annotation at build time. Refer to the below configuration for the Maven compile plugin.

<configuration>
        <annotationProcessorPaths>
               <path>
                       <groupId>org.mapstruct</groupId>
                       <artifactId>mapstruct-processor</artifactId>
                       <version>${mapstruct.version}</version>
               </path>
        </annotationProcessorPaths>
</configuration>

3. Auto Start the gRPC Server and Register Service Implementation

By default, the gRPC server doesn’t start along with the webserver (Netty in this case). Also, it needs to register all the gRPC services before server startup. The LogNet’s gRPC Spring Boot auto-scans all the classes annotated with @GRpcService annotation and registers service definitions with the server builder. After building the server it auto starts the gRPC server on a configured port in Spring application properties.

Apart from registering services and starting servers, it also supports security, health checks, and service discoveries with auto-configuration. For more details, refer to here.

The complete sample implementation for the approach described above is available on GitHub.

Differentiations

  • Google built gRPC out of their need to learn from building large-scale microservices from scratch. Most of the popular microservices frameworks still lack at the point where performance and cross-platform support are required since no big system can be built in a single stack and over a single encoding.
  • The most popular is the support of HTTP/2 and it is still a roadmap for many providers. It gives an advantage to binary wire transfers using a multiplex non-blocking data flow over a single TCP connection. Further supporting the header compression, it gives added performance advantages.
  • Along with supporting protobuf as a primary encoding mechanism, it also added support for JSON-based services, which can easily be consumed by low-end clients. While protobuf is a recommended way to implement gRPC, Google has added support for FlatBuffers and has increased adoption internally as well as across the industry.
  • Initial versions supported exposing gRPC services through reverse proxies like Envoy- and Ngnix-based systems. Recent developments in gRPC web have bridged that gap and added support for exposing gRCP services to web clients by adopting HTTP/2 across JavaScript libraries. Further development is in progress to add support to popular web frameworks like Angular and React.
  • With a full-featured development stack, it also provides full support for unit test helpers like InProcessServer and InProcessChannelBuilder.

Adoption

  • Google uses it across its internal tools and platforms, such as Bigtable, Pub/Sub, Speech, and TensorFlow.
  • CoreOS was an early adopter of gRPC and added support in their service discovery tool and container engine.
  • Microsoft is the advanced adopter of gRPC and added support to its web API and building tools to ease developer life to seamlessly build the services.
  • Now more and more projects are using it across the industry, such as open-source developers like Docker, Square, Netflix, and Cisco.

Alternate Approaches

Apart from building gRPC, applications from Google provide tooling/libraries and Spring Boot starter framework that are mentioned in the article. A few specialized microservice frameworks provide out-of-the-box support for gRPC service implementation.

  1. Quarkus gRPC
  2. Asp.NET gRPC
  3. Akka gRPC
  4. Salesforce gRPC

.

Leave a Comment