• Get application security done the right way! Detect, Protect, Monitor, Accelerate, and more…
  • Let’s explore how to implement gRPC in Java.

    gRPC (Google Remote Procedure Call): gRPC is an open-source RPC architecture developed by Google to enable high-speed communication between microservices. gRPC allows developers to integrate services written in different languages. gRPC uses the Protobuf messaging format (Protocol Buffers), a highly efficient, highly packed messaging format for serializing structured data.

    For some use cases, the gRPC API may be more efficient than the REST API.

    Let’s try to write a server on gRPC. First, we need to write several .proto files that describe services and models (DTO). For a simple server, we’ll use ProfileService and ProfileDescriptor.

    ProfileService looks like this:

    syntax = "proto3";
    package com.deft.grpc;
    import "google/protobuf/empty.proto";
    import "profile_descriptor.proto";
    service ProfileService {
      rpc GetCurrentProfile (google.protobuf.Empty) returns (ProfileDescriptor) {}
      rpc clientStream (stream ProfileDescriptor) returns (google.protobuf.Empty) {}
      rpc serverStream (google.protobuf.Empty) returns (stream ProfileDescriptor) {}
      rpc biDirectionalStream (stream ProfileDescriptor) returns (stream 	ProfileDescriptor) {}
    }
    

    gRPC supports a variety of client-server communication options. We’ll break them all down:

    • Normal server call – request/response.
    • Streaming from client to server.
    • Streaming from server to client.
    • And, of course, the bidirectional stream.

    The ProfileService service uses the ProfileDescriptor, which is specified in the import section:

    syntax = "proto3";
    package com.deft.grpc;
    message ProfileDescriptor {
      int64 profile_id = 1;
      string name = 2;
    }
    
    • int64 is Long for Java. Let the profile id belong.
    • String – just like in Java, this is a string variable.

    You can use Gradle or maven to build the project. It is more convenient for me to use maven. And further will be the code using maven. This is important enough to say because for Gradle, the future generation of the .proto will be slightly different, and the build file will need to be configured differently. To write a simple gRPC server, we only need one dependency:

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

    It’s just incredible. This starter does a tremendous amount of work for us.

    The project that we will create will look something like this:

    We need GrpcServerApplication to start the Spring Boot application. And GrpcProfileService, which will implement methods from the .proto service. To use protoc and generate classes from written .proto files, add protobuf-maven-plugin to pom.xml. The build section will look like this:

    <build>
            <extensions>
                <extension>
                    <groupId>kr.motd.maven</groupId>
                    <artifactId>os-maven-plugin</artifactId>
                    <version>1.6.2</version>
                </extension>
            </extensions>
            <plugins>
                <plugin>
                    <groupId>org.xolstice.maven.plugins</groupId>
                    <artifactId>protobuf-maven-plugin</artifactId>
                    <version>0.6.1</version>
                    <configuration>
                        <protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
                        <outputDirectory>${basedir}/target/generated-sources/grpc-java</outputDirectory>
                        <protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact>
                        <pluginId>grpc-java</pluginId>
                        <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.38.0:exe:${os.detected.classifier}</pluginArtifact>
                        <clearOutputDirectory>false</clearOutputDirectory>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>compile</goal>
                                <goal>compile-custom</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    
    • protoSourceRoot – specifying the directory where the .proto files are located.
    • outputDirectory – select the directory where the files will be generated.
    • clearOutputDirectory – a flag indicating not to clear generated files.

    At this stage, you can build a project. Next, you need to go to the folder that we specified in the output directory. The generated files will be there. Now you can gradually implement GrpcProfileService.

    The class declaration will look like this:

    @GRpcService
    public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase
    

    GRpcService annotation – Marks the class as a grpc-service bean.

    Since we inherit our service from ProfileServiceGrpc, ProfileServiceImplBase, we can override the methods of the parent class. The first method we’ll override is getCurrentProfile:

        @Override
        public void getCurrentProfile(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {
            System.out.println("getCurrentProfile");
            responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
                    .newBuilder()
                    .setProfileId(1)
                    .setName("test")
                    .build());
            responseObserver.onCompleted();
        }
    

    To respond to the client, you need to call the onNext method on the passed StreamObserver. After sending the response, send a signal to the client that the server has finished working onCompleted. When sending a request to the getCurrentProfile server, the response will be:

    {
      "profile_id": "1",
      "name": "test"
    }
    

    Next, let’s take a look at the server stream. With this messaging approach, the client sends a request to the server, the server responds to the client with a stream of messages. For example, it sends five requests in a loop. When sending is complete, the server sends a message to the client about the successful completion of the stream.

    The overridden server stream method will look like this:

    @Override
        public void serverStream(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {
            for (int i = 0; i < 5; i++) {
                responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
                        .newBuilder()
                        .setProfileId(i)
                        .build());
            }
            responseObserver.onCompleted();
        }
    

    Thus, the client will receive five messages with a ProfileId, equal to the response number.

    {
      "profile_id": "0",
      "name": ""
    }
    {
      "profile_id": "1",
      "name": ""
    }
    …
    {
      "profile_id": "4",
      "name": ""
    }
    

    Client stream is very similar to server stream. Only now the client transmits a stream of messages, and the server processes them. The server can process messages immediately or wait for all requests from the client and then process them.

        @Override
        public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> clientStream(StreamObserver<Empty> responseObserver) {
            return new StreamObserver<>() {
    
                @Override
                public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) {
                    log.info("ProfileDescriptor from client. Profile id: {}", profileDescriptor.getProfileId());
                }
    
                @Override
                public void onError(Throwable throwable) {
    
                }
    
                @Override
                public void onCompleted() {
                    responseObserver.onCompleted();
                }
            };
        }
    

    In the Client stream, you need to return the StreamObserver to the client, to which the server will receive messages. The onError method will be called if an error occurred in the stream. For example, it terminated incorrectly.

    To implement a bidirectional stream, it is necessary to combine creating a stream from the server and the client.

    @Override
        public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> biDirectionalStream(
                StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {
    
            return new StreamObserver<>() {
                int pointCount = 0;
                @Override
                public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) {
                    log.info("biDirectionalStream, pointCount {}", pointCount);
                    responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
                            .newBuilder()
                            .setProfileId(pointCount++)
                            .build());
                }
    
                @Override
                public void onError(Throwable throwable) {
    
                }
    
                @Override
                public void onCompleted() {
                    responseObserver.onCompleted();
                }
            };
        } 
    

    In this example, in response to the client’s message, the server will return a profile with an increased pointCount.

    Conclusion

    We have covered the basic options for messaging between a client and a server using gRPC: implemented server stream, client stream, bidirectional stream.

    The article was written by Sergey Golitsyn