Building Microservices with Spring Boot and?gRPC

Building Microservices with Spring Boot and?gRPC


gRPC Remote Procedure Call is a binary message based protocol for writing cross-language applications.

As part of this tutorial, we will see how the gRPC framework enables a more efficient communication across distributed systems.


Understanding gRPC

Client-Server Model

Similar to REST APIs, gRPC APIs define one or more services and request/response messages.

No alt text provided for this image
Client-Server model in gRPC

On the server-side, the gRPC server implements a service interface to handle client calls. While on the client-side, clients are provided with a gRPC stub that has the same methods as the server. This means you must first create your API (API-First approach).

The gRPC?Protocol

The gRPC protocol is built on top of HTTP/2, which provides a foundation for long-lived, real-time communication streams.

The protocol introduces three concepts: channels, remote procedure calls (RPCs), and messages.

No alt text provided for this image
gRPC protocol

Each channel may have a number of RPCs, while each RPC may have a number of messages.

Client-Server Communication

Now we have a better understanding of how the gRPC protocol works, let’s examine four different ways clients and servers can communicate.

No alt text provided for this image
Client-Server communication in gRPC

  1. Unary RPC: It is the simplest type of RPC where the client sends a single request and gets back a single response.
  2. Server Streaming RPC: A server-streaming RPC is similar to a unary RPC, except that the server returns a stream of messages in response to a client’s request.
  3. Client Streaming RPC: A client-streaming RPC is similar to a unary RPC, except that the client sends a stream of messages to the server and the server responds with a single message.
  4. Bidirectional Streaming RPC: In a bidirectional streaming RPC, the call is initiated by the client invoking the method. The two streams are independent, so the client and server can read and write messages in any order.

Protocol Buffers

The Protocol Buffer language is a compact, language-neutral, platform-neutral extensible mechanism for serializing structured data. By default, gRPC uses the Protocol Buffers as both its Interface Definition Language (IDL) and as its underlying message interchange format.

The Protocol Buffer file is an ordinary text file with a?.proto extension, which allows client-side stubs and service interface generation.?

Let’s take the foo.proto file described below as an example.

syntax = "proto3";

package foo.grpc;

option java_multiple_files = true;        

This example uses the proto3 syntax defined by the syntax property.?

The compiler generates Java classes based on the?.proto file. It is convenient to specify a destination package in the package property.

By default, the compiler generates a single Java file. In order to generate individual Java files, we set the java_multiple_files property to true.

Next, let’s define the FooRequest and FooResponse messages.

message FooRequest {
    string bar = 1;
}

message FooResponse {
    string result = 1;
}        

Message fields must be defined with type, name and a number (usually auto-incremental).

To conclude, let’s define the service contract for FooService.

service FooService {
    
    // Unary RPC
    rpc foo(FooRequest) returns (FooResponse);

    // Server Streaming RPC
    rpc fooStreamResponses(FooRequest) returns (stream FooResponse);

    // Client Streaming RPC
    rpc fooStreamRequests(stream FooRequest) returns (FooResponse);

    // Bidirectional Streaming RPC
    rpc fooStreamRequestsAndResponses(stream FooRequest) returns (stream FooResponse);
}        

Here, the foo() operation enables unary RPC, fooSreamResponses() allows server streaming, fooSreamRequests() allows client streaming, and fooSreamRequestsAndResponses() enables bidirectional streaming.

Note that by simply adding the stream keyword to the request/response, we can enable client/server streaming.


Spring Boot with?gRPC

Scenario

The following example illustrates a simplified representation of a real-life distributed system.

No alt text provided for this image

Here, Loans Service is called by Accounts Service to create, read, update or delete loans.

Project Structure

As shown below, our Maven project is divided into three modules: common, loans and accounts.

├───accounts
│   ├───src
│   │   └───main
│   │       ├───java
│   │       └───resources
│   └───pom.xml
├───common
│   ├───src
│   │   └───main
│   │       └───proto
│   └───pom.xml
├────loans
│   ├───src
│   │   └───main
│   │       ├───java
│   │       └───resources
│   └───pom.xml
└───pom.xml        

Creating a Common?Module

The common module holds the?.proto files and the Java code the compiler generates.

The first step is to define the loans.proto file in the common/src/main/proto directory.

syntax = "proto3";

import "google/protobuf/empty.proto";

package common.grpc.loans;

option java_multiple_files = true;

service LoansService {

  rpc createLoan(Loan) returns (LoanId);

  rpc readLoan(LoanId) returns (Loan);

  rpc renegotiateLoan(Loan) returns (google.protobuf.Empty);

  rpc deleteLoan(LoanId) returns (google.protobuf.Empty);
}

message Borrower {
  string name = 1;
  int32 age = 2;
  double annual_income = 3;
  optional double annual_debt = 4;
  optional bool delinquent_debt = 5;
}

message Loan {
  string guid = 1;
  double requested_amount = 2;
  int32 term_months = 3;
  float annual_interest = 4;
  Borrower borrower = 5;
}

message LoanId {
  string guid = 1;
}        

Next, in order to generate Java code using the protocol buffer compiler, we use the Maven Protocol Buffers plugin.

Let’s add the the plugins and dependencies we need to common/pom.xml.

<properties>
    <grpc.version>1.40.1</grpc.version>
    <protobuf.version>3.19.6</protobuf.version>
</properties>

<dependencies>
    <dependency>
        <artifactId>grpc-netty-shaded</artifactId>
        <groupId>io.grpc</groupId>
        <scope>runtime</scope>
        <version>${grpc.version}</version>
    </dependency>
    <dependency>
        <artifactId>grpc-protobuf</artifactId>
        <groupId>io.grpc</groupId>
        <version>${grpc.version}</version>
    </dependency>
    <dependency>
        <artifactId>grpc-stub</artifactId>
        <groupId>io.grpc</groupId>
        <version>${grpc.version}</version>
    </dependency>
    <dependency>
        <artifactId>protobuf-java</artifactId>
        <groupId>com.google.protobuf</groupId>
        <version>${protobuf.version}</version>
    </dependency>
    <dependency>
        <artifactId>javax.annotation-api</artifactId>
        <groupId>javax.annotation</groupId>
        <version>1.2</version>
    </dependency>
</dependencies>

<build>
    <extensions>
        <extension>
            <artifactId>os-maven-plugin</artifactId>
            <groupId>kr.motd.maven</groupId>
            <version>1.6.2</version>
        </extension>
    </extensions>
    <plugins>
        <plugin>
            <artifactId>protobuf-maven-plugin</artifactId>
            <groupId>org.xolstice.maven.plugins</groupId>
            <version>0.6.1</version>
            <configuration>
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
                    </pluginArtifact>
                <pluginId>grpc-java</pluginId>
                <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
                    </protocArtifact>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>compile-custom</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>        

As a result, the plugin generates Java code in the target/classes directory each time Maven compiles the source code.

Creating the gRPC?Server

The loans module has a simple implementation for the gRPC Server.

To use the Java code generated in the common module, we add a dependency to loans/pom.xml.

<dependency>
    <artifactId>common</artifactId>
    <groupId>com.manerajona</groupId>
    <version>${project.version}</version>
</dependency>        

Additionally, we want to add a dependency for the autoconfiguration of the embedded gRPC server. LogNet’s gRPC Spring Boot Starter provides us with this functionality.

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

As a next step, we extend and override the default implementation of LoansServiceImplBase generated by the protocol buffer compiler.

@GRpcService
public class LoansServiceImpl extends LoansServiceImplBase {

    private final ConcurrentMap<LoanId, Loan> loans = new ConcurrentHashMap<>();

    @Override
    public void createLoan(Loan loan, StreamObserver<LoanId> responseObserver) {
        LoanId loanId = LoanId.newBuilder().setGuid(loan.getGuid()).build();
        loans.put(loanId, loan);

        responseObserver.onNext(loanId);
        responseObserver.onCompleted();
    }

    @Override
    public void readLoan(LoanId loanId, StreamObserver<Loan> responseObserver) {
        Loan loan = loans.get(loanId);

        responseObserver.onNext(loan);
        responseObserver.onCompleted();
    }

    @Override
    public void renegotiateLoan(Loan loan, StreamObserver<Empty> responseObserver) {
        LoanId loanId = LoanId.newBuilder().setGuid(loan.getGuid()).build();
        loans.computeIfPresent(loanId, (k, v) -> loan);

        responseObserver.onNext(Empty.getDefaultInstance());
        responseObserver.onCompleted();
    }

    @Override
    public void deleteLoan(LoanId loanId, StreamObserver<Empty> responseObserver) {
        loans.remove(loanId);

        responseObserver.onNext(Empty.getDefaultInstance());
        responseObserver.onCompleted();
    }
}        

Here, Loan objects are stored in ConcurrentMap<LoanId, Loan>.

A response is added after each operation using StreamObserver’s onNext() method, and the call is resumed after onCompleted().

In order to automatically configure the gRPC server, we annotate LoansServiceImpl with @GRpcService. By default the gRPC server will run on localhost:6565.

Creating the?Client

The accounts module has a simple implementation the client.

First, we add the common and spring-boot-starter-web dependencies to accounts/pom.xml.

<dependency>
    <artifactId>common</artifactId>
    <groupId>com.manerajona</groupId>
    <version>${project.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>        

Next, let’s create a configuration class for the gRPC Service Stub.

@Configuration
class GrpcClientConfiguration {

    @Bean
    LoansServiceBlockingStub loansServiceStub() {
        Channel channel = ManagedChannelBuilder.forAddress("localhost", 6565)
            .usePlaintext()
            .build();

        return LoansServiceGrpc.newBlockingStub(channel);
    }
}        

Here, we have created a bean of type LoansServiceBlockingStub. This stub is blocking, meaning that for each request it blocks the RPC call until it receives a response. You can also create a non-blocking stub to make asynchronous calls.

Note that the LoansService.newBlockingStub() method accepts Channel as argument. By using ManagedChannelBuilder, we create the channel for the address and port where the server is running, and we specify usePlainText() to exchange messages without any encryption (not recommended.)

As a final step, we test the stub using Spring ApplicationRunner.

@Component
public class AccountsApplicationRunner implements ApplicationRunner {

    private final LoansServiceBlockingStub stub;

    public AccountsApplicationRunner(LoansServiceBlockingStub stub) {
        this.stub = stub;
    }

    @Override
    public void run(ApplicationArguments args) {

        // create
        var borrowerBuilder = Borrower.newBuilder()
            .setName("John Doe")
            .setAge(34)
            .setAnnualIncome(100_000);

        var loanBuilder = Loan.newBuilder()
            .setGuid(UUID.randomUUID().toString())
            .setBorrower(borrowerBuilder.build())
            .setRequestedAmount(1_000_000_000)
            .setTermMonths(12)
            .setAnnualInterest(6.5f);

        LoanId id = stub.createLoan(loanBuilder.build());
        System.out.println("** Loan created with " + id);

        // read
        System.out.println("** Loan read:\n" + stub.readLoan(id));

        // update
        Loan loanUpdated = loanBuilder
            .setTermMonths(24)
            .setAnnualInterest(7.5f)
            .build();

        stub.renegotiateLoan(loanUpdated);
        System.out.println("** Loan updated:\n" + stub.readLoan(id));

        // delete
        stub.deleteLoan(id);
        System.out.println("** Is Loan deleted: " +
            stub.readLoan(id).getGuid().isBlank());
    }
}        

This image shows each operation called and printed on the console:

No alt text provided for this image

Conclusion

In this article, we learned how to use gRPC in Java and Spring Boot. Let’s now look at some of the pros and cons of adopting this technology:

Pros:

  • It enables API-First design.
  • It enables streaming.
  • It is an efficient, compact and secure way to exchange messages between clients and servers.
  • It is agnostic to the programming language used in the application.

Cons:

  • There may be some challenges using it on the frontend.
  • Older infrastructure might not be ready for moving from HTTP/1 to HTTP/2.


Thanks for reading. I hope this was helpful!

The example code is available on GitHub.

要查看或添加评论,请登录

Jonathan Manera的更多文章

  • Service Discovery Patterns with Netflix Eureka

    Service Discovery Patterns with Netflix Eureka

    Modern distributed systems assign network locations dynamically, so you don’t know what IP address or port each service…

  • Spring Cloud Circuit Breaker Resilience4j

    Spring Cloud Circuit Breaker Resilience4j

    A distributed system, which comprises many services interacting to achieve business goals, is prone to failures in the…

  • Aspect Oriented Programming for?Babies

    Aspect Oriented Programming for?Babies

    Aspect Oriented Programming is a powerful tool that enables a clean modularization of “crosscutting” concerns of…

  • 7 Best Practices for Java API Documentation

    7 Best Practices for Java API Documentation

    The Javadoc Tool simplifies the documentation process in Java, allowing developers to document their code seamlessly as…

  • The Holy Trinity: JUnit5, Mockito, and Instancio

    The Holy Trinity: JUnit5, Mockito, and Instancio

    Unit testing is a software testing technique where individual components of an application (units) are tested in…

  • Code Quality Analysis with Sonar

    Code Quality Analysis with Sonar

    SonarQube is an open-source platform for continuous code quality inspection. This tool provides a detailed analysis of…

  • Mapping Bidirectional Object Associations using MapStruct

    Mapping Bidirectional Object Associations using MapStruct

    In object-oriented programming, an association is a relationship between two entities that defines how two objects…

  • API-First Design with OpenAPI Generator

    API-First Design with OpenAPI Generator

    APIs are contracts between services and their clients. These contracts are usually documented using an interface…

    1 条评论
  • Testing Microservices with Testcontainers

    Testing Microservices with Testcontainers

    When writing integration tests for Spring Boot applications, we usually need to access external resources such as…

  • 10 Meditations for Creative Professionals

    10 Meditations for Creative Professionals

    Sometimes, in our day-to-day life, we feel that something is not quite right. We find it hard to be as productive as we…

社区洞察

其他会员也浏览了