Spring Boot gRPC Tutorial

This tutorial will go over how to create a gRPC service for a chat application. In this tutorial we will look at how to generate the message types and the services. Will look at how to send messages from the server to the client. This tutorial assumes that you know what gRPC is. You can find the source code for this tutorial at https://github.com/mattpenna-dev/gRPC-demo.

Creating the Project

First go to start.sprint.io. Fill out the project metadata with Group, Artifact, Name, Description, and package name. Pick which version of java you would like to use. You don't have to use maven for dependency management but this tutorial will be using it. Click on Add Dependencies on the right. Select Spring Web. Click Generate. This will create a zip file for the project. Unzip the file.

Generating Services and Classes

For this tutorial we will be using Protobuf (https://developers.google.com/protocol-buffers) and protobuf-maven-plugin (https://www.xolstice.org/protobuf-maven-plugin/index.html). Protobuf is a language neutral, platform neutral mechanism for generating structured data. Protobuf-maven-plugin is a maven plugin that is used to generate the java classes from the protobuf files.

Protobuf

First thing we will need to do is create a directory to store our .proto (Protobuf) files. Create a new directory inside src/main/java/resources called proto. You can create this directory anywhere. By default the protobuf-maven-plugin will look for the files in ${basedir}/src/main/proto. Since this is a resource we are going to place it in the resource directory and will update the maven plugin to look at this directory instead of the default.

First we need to create a new file called ChatMessages.proto in the directory created above. In this file we will create a Chat Message message, ChatMessageFromServer message and a Chat Service with a chat method. This chat method will be a bi-directional method (the server or the client can push data). To do this we will use streams as the request and streams as the resposne. We will add the following lines to this file:

syntax = "proto3";
option java_multiple_files = true;

import "google/protobuf/timestamp.proto";
package mattpenna.dev.gRPC.demo.proto;

message ChatMessage {
 string from = 1;
 string message = 2;
}

message ChatMessageFromServer {
 google.protobuf.Timestamp timestamp = 1;
 ChatMessage message = 2;
}

service ChatService {
 rpc chat(stream ChatMessage) returns (stream ChatMessageFromServer);
}

Lets look at some of the details of the proto file. The option javamultiplefiles. This will create the java classes in multiple files instead of one large file. In protofiles like java and other languages you can import types and dependencies. For this demo we will be using googles Timestamp object.

Maven plugin

Now that we have our .proto file create, we now need to generate the java classes from that file. To do this we will use the protobuf-maven-plugin. You will need to add the following to the build plugins section in the pom.

<plugin>    
    <groupId>org.xolstice.maven.plugins</groupId>
    <artifactId>protobuf-maven-plugin</artifactId>
    <version>0.6.1</version>
    <configuration>
        <protocArtifact>com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier}</protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier}</pluginArtifact>
        <protoSourceRoot>${basedir}/src/main/resources/proto</protoSourceRoot>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
                <goal>compile-custom</goal>
            </goals>
        </execution>
    </executions>
</plugin>

The protoSourceRoot points the plugin to the directory that we want to store our protobuf files. By default the plugin checks the ${basedir}/src/main/proto directory for the files.

You will also need to add the os-maven-plugin extension to the build section ino order to use the ${os.detected.classifier}

<extensions>
    <extension>
        <groupId>kr.motd.maven</groupId>
        <artifactId>os-maven-plugin</artifactId>
        <version>1.6.1</version>
    </extension>
</extensions>

To generate the classes you will need to issue a mvn clean compile command. This will generate the files and place them in the target generated sources directory.

gRPC Dependencies

At this point we now have created our protobuf files and generated our java classes. Now we need to update our project with gRPC maven dependencies. For this we will add the grpc-spring-boot-starter dependency to our dependencies list.

<dependency>
    <groupId>net.devh</groupId>
    <artifactId>grpc-spring-boot-starter</artifactId>
    <version>2.9.0.RELEASE</version>
</dependency>

Creating the Service

We have now installed all our dependencies and have generated our message and service classes. Now we need to override the default chat method created with our business logic. To do this we will create a new package called controllers (you can name this what you want. To follow the typical controller, service, repository strucutre I am going to create using contollers).

In this directory you will need to create a new class called ChatServiceImpl that extends the autogenerated ChatServiceGrpc.ChatServiceImplBase class. In this class we will now need to override the chat method.

In order to keep track of all the clients that are connect to our chat subscription we will create a LinkedHashSet to store the StreamObserver object that is sent in the chat request.

private static LinkedHashSet<StreamObserver<ChatMessageFromServer>> chatClients = new LinkedHashSet<>();

    @Override
    public StreamObserver<ChatMessage> chat(StreamObserver<ChatMessageFromServer> responseObserver) {
        chatClients.add(responseObserver);

Now we need to create the StreamObserver response object. The StreamObserver has 3 methods that we need to override:

  • onNext
  • onError
  • onCompleted

onNext

This method will receive the chat message from one of the clients. When we receive this chat message the server will send the message to all of the clients. To do this we will interate over the LinkedHashSet we created and we will will call the onNext method of that StreamObserver.

@Override
public void onNext(ChatMessage chatMessage) {
    for (StreamObserver<ChatMessageFromServer> chatClient : chatClients) {
        chatClient.onNext(ChatMessageFromServer.newBuilder().setMessage(chatMessage).build());
    }

}

onError

We don't really have to do anything with this method if we don't want to, but for good practice when we get an error we will return the execption to the client that made the request.

@Override
public void onError(Throwable throwable) {
    responseObserver.onError(throwable);
}

onCompleted

Last we will need to update the onComplete method. In this method we will want to remove StreamObserver that has disconnected.

@Override
public void onCompleted() {
    chatClients.remove(responseObserver);
}

After we have made all these updates we are left with:

package mattpenna.dev.gRPC.demo.controller;

import io.grpc.stub.StreamObserver;
import mattpenna.dev.gRPC.demo.proto.ChatMessage;
import mattpenna.dev.gRPC.demo.proto.ChatMessageFromServer;
import mattpenna.dev.gRPC.demo.proto.ChatServiceGrpc;

import java.util.LinkedHashSet;

public class ChatServiceImpl extends ChatServiceGrpc.ChatServiceImplBase {

    private static LinkedHashSet<StreamObserver<ChatMessageFromServer>> chatClients = new LinkedHashSet<>();

    @Override
    public StreamObserver<ChatMessage> chat(StreamObserver<ChatMessageFromServer> responseObserver) {
        chatClients.add(responseObserver);
        
        return new StreamObserver<>() {
            @Override
            public void onNext(ChatMessage chatMessage) {
                for (StreamObserver<ChatMessageFromServer> chatClient : chatClients) {
                    chatClient.onNext(ChatMessageFromServer.newBuilder().setMessage(chatMessage).build());
                }

            }

            @Override
            public void onError(Throwable throwable) {
                responseObserver.onError(throwable);
            }

            @Override
            public void onCompleted() {
                chatClients.remove(responseObserver);
            }
        };
    }
}

We have now created a gRPC server that can accept and send chat messages to clients. Next we will look at how to write integration test to test this service endpoint. Also this will show how to create a gRPC client.

Integration Testing

We will be using spring boot integration test stratgy for testing our new gRPC service. Spring boot initializer will create by default an integration test that validates that all you dependencies are wired up properly. You should see a class named GRpcDemoApplicationTests or what ever name you called the project. By default spring will create the context load test.

@Test
void contextLoads() {
}

To test our new endpoint we will need to add a new test to this test suite (You can create a new java class for this test to help for organization). Before we update this class first we need to add some test configurations. To do this we will create a new file in the test/resources directory called test-application.properties (you can use yaml if you prefer). In this file we will need to provide the following values:

grpc.server.inProcessName=test //name of in process name we will use to connect
grpc.server.port=-1 //disable the grpc server 
grpc.client.inProcess.address=in-process:test //set the in process address to use in our tests

For these tests we will be connecting to the gRPC service by using the in-process name instead of the server address. Now that we have our properties created we now need to tell the integration tests to use this file. To do that we will need to add the @TestPropertySource annotation to GRpcDemoApplicationTests class. We will also need to provide the location to point to the test properties file we created.

@SpringBootTest()
@TestPropertySource(locations = "classpath:test-application.properties")
class GRpcDemoApplicationTests {

Now we can create our gRPC clients that we will be using in the tests. To do this we will use the @GrpcClient annotation and provide it the value "inProcess" so that it knows to use the inprocess for the connection. This will grab the address value in our properties file (grpc.client.inProcess.address). For this test we will create 2 clients. This will simulate to clients talking to each other.

@GrpcClient("inProcess")
private ChatServiceGrpc.ChatServiceStub chatClient1;

@GrpcClient("inProcess")
private ChatServiceGrpc.ChatServiceStub chatClient2;

Now we have everything setup that we need, we can now start to write the actual test. We will create a new test call testChat. In this test we will need to create 2 stream observers (1 for each chat client). For this test we will have the onNext method just add received messages to an arraylist that we will use to validate our test is working and we received chat messages. We can leave the onError and onCompleted methods empty.

new StreamObserver<>() {
    @Override
    public void onNext(ChatMessage chatMessage) {
        messages.add(chatMessage);
    }

    @Override
    public void onError(Throwable throwable) {
    }

    @Override
    public void onCompleted() {
    }
};

Now we need to have the clients register with the server so they can start to receive messages. To do that we need to pass the streams created above to the clients chat method.

chatClient1.chat(chatClient1Observer);
chatClient2.chat(chatClient2Observer);

We are now ready to send messages to the server from the clients. To do this all we need to do is call the onNext method.

chatClient1Observer.onNext(generateChatMessage("Hello Chat Client2", "ChatClient1"));
chatClient2Observer.onNext(generateChatMessage("Hello Chat Client1", "ChatClient2"));

At this point we have created a new test. We have connected to the gRPC service. We have sent messages between client1 and client2 and added the messages received to an arraylist. Now we just need to validate the data received. For this test we are just going to validate that we have added to messages to the list.

Assert.notEmpty(messages, "Validate Messages are populated");

So after all of this your test should look something like:

@Test
    public void testChat()
    {
        List messages = new ArrayList<ChatMessageFromServer>();

        StreamObserver chatClient1Observer = generateObserver(messages);
        StreamObserver chatClient2Observer = generateObserver(messages);

        chatClient1.chat(chatClient1Observer);
        chatClient2.chat(chatClient2Observer);

        chatClient1Observer.onNext(generateChatMessage("Hello Chat Client2", "ChatClient1"));
        chatClient2Observer.onNext(generateChatMessage("Hello Chat Client1", "ChatClient2"));

        Assert.notEmpty(messages, "Validate Messages are populated");
    }

    private StreamObserver<ChatMessage> generateObserver(List messages)
    {
        return new StreamObserver<>() {
            @Override
            public void onNext(ChatMessage chatMessage) {
                messages.add(chatMessage);
            }

            @Override
            public void onError(Throwable throwable) {

            }

            @Override
            public void onCompleted() {

            }
        };
    }

    private ChatMessage generateChatMessage(String message, String from)
    {
        return ChatMessage.newBuilder()
                .setMessage(message)
                .setFrom(from)
                .build();
    }

References