Introduction to gRPC with Quarkus

Introduction to gRPC with Quarkus

In this article, you will learn how to implement and consume gRPC services with Quarkus. Quarkus provides built-in support for gRPC through the extension. We will create a simple app, which uses that extension and also interacts with the Postgres database through the Panache Reactive ORM module. You can compare gRPC support available in Quarkus with Spring Boot by reading the following article on my blog. This is a good illustration of what Quarkus may simplify in your development.

Source Code

If you would like to try it by yourself, you can always take a look at my source code. In order to do that, you need to clone my GitHub repository. It contains several different Quarkus apps. For the current article, please refer to the person-grpc-service app. You should go to that directory and then just follow my instructions 🙂

Generate Model Classes and Services for gRPC

In the first step, we will generate model classes and gRPC services using the .proto manifests. The same as for the Spring Boot app we will create the Protobuf manifest and place it inside the src/main/proto directory. We need to include some additional Protobuf schemas to use the google.protobuf.* package (1). Our gRPC service will provide methods for searching persons using various criteria and a single method for adding a new person (2). Those methods will use primitives from the google.protobuf.* package and model classes defined inside the .proto file as messages. The Person message represents a single model class. It contains three fields: idname, age and gender (3). The Persons message contains a list of Person objects (4). The gender field inside the Person message is an enum (5).

syntax = "proto3";

package model;

option java_package = "pl.piomin.quarkus.grpc.model";
option java_outer_classname = "PersonProto";

// (1)
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";

// (2)
service PersonsService {
  rpc FindByName(google.protobuf.StringValue) returns (Persons) {}
  rpc FindByAge(google.protobuf.Int32Value) returns (Persons) {}
  rpc FindById(google.protobuf.Int64Value) returns (Person) {}
  rpc FindAll(google.protobuf.Empty) returns (Persons) {}
  rpc AddPerson(Person) returns (Person) {}
}

// (3)
message Person {
  int64 id = 1;
  string name = 2;
  int32 age = 3;
  Gender gender = 4;
}

// (4)
message Persons {
  repeated Person person = 1;
}

// (5)
enum Gender {
  MALE = 0;
  FEMALE = 1;
}

Once again I will refer here to my previous article about Spring Boot for gRPC. With Quarkus we don’t need to include any Maven plugin responsible for generating Java classes. This feature is automatically included by the Quarkus gRPC module. This saves a lot of time – especially at the beginning with Java gRPC (I know this from my own experience). Of course, if you want to override a default behavior, you can include your own plugin. Here’s the example from the official Quarkus docs.

Now, we just need to build the project with the mvn clean package command. It will automatically generate the following list of classes (I highlighted the two most important for us):

By default, Quarkus generates our gRPC classes in the target/generated-sources/grpc directory. Let’s include it as the source directory using the build-helper-maven-plugin Maven plugin.

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>build-helper-maven-plugin</artifactId>
  <executions>
    <execution>
      <id>add-source</id>
      <phase>generate-sources</phase>
      <goals>
        <goal>add-source</goal>
      </goals>
      <configuration>
        <sources>
          <source>target/generated-sources/grpc</source>
        </sources>
      </configuration>
    </execution>
  </executions>
</plugin>

Dependencies

By default, the quarkus-grpc extension relies on the reactive programming model. Therefore we will include a reactive database driver and a reactive version of the Panache Hibernate module. It is also worth adding the RESTEasy Reactive module. Thanks to that, we will be able to run e.g. Quarkus Dev UI, which also provides useful features for the gRPC services. Of course, we are going to write some JUnit tests to verify core functionalities, so that’s why quarkus-junit5 is included in the Maven pom.xml.

<dependencies>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-grpc</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-reactive-panache</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-reactive-pg-client</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

Using the Quarkus gRPC Extension

Once we included all the required libraries and generated classes for Protobuf integration we can with the implementation. Let’s begin with the persistence layer. We already have message classes generated, but we still need to create an entity class. Here’s the PersonEntity class. We will take advantage of PanacheEntity, and thanks to that we don’t need to e.g. define getters/setters.

@Entity
public class PersonEntity extends PanacheEntity {

    public String name;
    public int age;
    public Gender gender;

}

Thanks to the Quarkus Panache field access rewrite, when your users read person.name they will actually call your getName() accessor, and similarly for field writes and the setter. This allows for proper encapsulation at runtime as all the fields calls will be replaced by the corresponding getter/setter calls.

After that, we can create the repository class. It implements the reactive version of the PanacheRepository interface. The important thing is that the PanacheRepository interface returns Mutiny Uni objects. Therefore, if need to add some additional methods inside the repository we should also return results as the Uni object.

@ApplicationScoped
public class PersonRepository implements PanacheRepository<PersonEntity> {

    public Uni<List<PersonEntity>> findByName(String name){
        return find("name", name).list();
    }

    public Uni<List<PersonEntity>> findByAge(int age){
        return find("age", age).list();
    }
}

Finally, we can proceed to the most important element in our tutorial – the implementation of the gRPC service. The implementation class should be annotated with @GrpcService (1). In order to interact with the database reactively I also had to annotate it with @WithSession (2). It creates a Mutiny session for each method gRPC method inside. We can also register optional interceptors, for example, to log incoming requests and outgoing responses (3). Our service class needs to implement the PersonsService interface generated by the Quarkus gRPC extension (4).

Then we can go inside the class. In the first step, we will inject the repository bean (5). After that, we will override all the gRPC methods generated from the .proto manifest (6). All those methods use the PersonRepository bean to interact with the database. Once they obtain a result it is required to convert it to the Protobuf object (7). When we add a new person to the database, we need to do it in the transaction scope (8).

@GrpcService // (1)
@WithSession // (2)
@RegisterInterceptor(LogInterceptor.class) // (3)
public class PersonsServiceImpl implements PersonsService { // (4)

    private PersonRepository repository; // (5)

    public PersonsServiceImpl(PersonRepository repository) {
        this.repository = repository;
    }

    @Override // (6)
    public Uni<PersonProto.Persons> findByName(StringValue request) {
        return repository.findByName(request.getValue())
                .map(this::mapToPersons); // (7)
    }

    @Override
    public Uni<PersonProto.Persons> findByAge(Int32Value request) {
        return repository.findByAge(request.getValue())
                .map(this::mapToPersons);
    }

    @Override
    public Uni<PersonProto.Person> findById(Int64Value request) {
        return repository.findById(request.getValue())
                .map(this::mapToPerson);
    }

    @Override
    public Uni<PersonProto.Persons> findAll(Empty request) {
        return repository.findAll().list()
                .map(this::mapToPersons);
    }

    @Override
    @WithTransaction // (8)
    public Uni<PersonProto.Person> addPerson(PersonProto.Person request) {
        PersonEntity entity = new PersonEntity();
        entity.age = request.getAge();
        entity.name = request.getName();
        entity.gender = Gender.valueOf(request.getGender().name());
        return repository.persist(entity)
           .map(personEntity -> mapToPerson(entity));
    }

    private PersonProto.Persons mapToPersons(List<PersonEntity> list) {
        PersonProto.Persons.Builder builder = 
           PersonProto.Persons.newBuilder();
        list.forEach(p -> builder.addPerson(mapToPerson(p)));
        return builder.build();
    }

    private PersonProto.Person mapToPerson(PersonEntity entity) {
        PersonProto.Person.Builder builder = 
           PersonProto.Person.newBuilder();
        if (entity != null) {
            return builder.setAge(entity.age)
                    .setName(entity.name)
                    .setId(entity.id)
                    .setGender(PersonProto.Gender
                       .valueOf(entity.gender.name()))
                    .build();
        } else {
            return null;
        }
    }
}

At the end let’s discuss the optional step. With Quarkus gRPC we can implement a server interceptor by creating the @ApplicationScoped bean implementing ServerInterceptor. In order to apply an interceptor to all exposed services, we should annotate it with @GlobalInterceptor. In our case, the interceptor is registered to a single service with @RegisterInterceptor annotation. Then we will the SimpleForwardingServerCall class to log outgoing messages, and SimpleForwardingServerCallListener to log outgoing messages.

@ApplicationScoped
public class LogInterceptor  implements ServerInterceptor {

    Logger log;

    public LogInterceptor(Logger log) {
        this.log = log;
    }

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {

        ServerCall<ReqT, RespT> listener = new ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) {
            @Override
            public void sendMessage(RespT message) {
                log.infof("[Sending message] %s",  message.toString().replaceAll("\n", " "));
                super.sendMessage(message);
            }
        };

        return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(next.startCall(listener, headers)) {
            @Override
            public void onMessage(ReqT message) {
                log.infof("[Received message] %s", message.toString().replaceAll("\n", " "));
                super.onMessage(message);
            }
        };
    }
}

Quarkus JUnit gRPC Tests

Of course, there must be tests in our app. Thanks to the Quarkus dev services and built-in integration with Testcontainers we don’t have to take care of starting the database. Just remember to run the Docker daemon on your laptop. After annotating the test class with @QuarkusTest we can inject the gRPC client generated from the .proto manifest with @GrpcClient (1). Then, we can use the PersonsService interface to call our gRPCS methods. By default, Quarkus starts gRPC endpoints on 9000 port. The Quarkus gRPC client works in the reactive mode, so we will leverage the CompletableFuture class to obtain and verify results in the tests (2).

@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PersonsServiceTests {

    static Long newId;

    @GrpcClient // (1)
    PersonsService client;

    @Test
    @Order(1)
    void shouldAddNew() throws ExecutionException, InterruptedException, TimeoutException {
        CompletableFuture<Long> message = new CompletableFuture<>(); // (2)
        client.addPerson(PersonProto.Person.newBuilder()
                        .setName("Test")
                        .setAge(20)
                        .setGender(PersonProto.Gender.MALE)
                        .build())
                .subscribe().with(res -> message.complete(res.getId()));
        Long id = message.get(1, TimeUnit.SECONDS);
        assertNotNull(id);
        newId = id;
    }

    @Test
    @Order(2)
    void shouldFindAll() throws ExecutionException, InterruptedException, TimeoutException {
        CompletableFuture<List<PersonProto.Person>> message = new CompletableFuture<>();
        client.findAll(Empty.newBuilder().build())
                .subscribe().with(res -> message.complete(res.getPersonList()));
        List<PersonProto.Person> list = message.get(1, TimeUnit.SECONDS);
        assertNotNull(list);
        assertFalse(list.isEmpty());
    }

    @Test
    @Order(2)
    void shouldFindById() throws ExecutionException, InterruptedException, TimeoutException {
        CompletableFuture<PersonProto.Person> message = new CompletableFuture<>();
        client.findById(Int64Value.newBuilder().setValue(newId).build())
                .subscribe().with(message::complete);
        PersonProto.Person p = message.get(1, TimeUnit.SECONDS);
        assertNotNull(p);
        assertEquals("Test", p.getName());
        assertEquals(newId, p.getId());
    }

    @Test
    @Order(2)
    void shouldFindByAge() throws ExecutionException, InterruptedException, TimeoutException {
        CompletableFuture<PersonProto.Persons> message = new CompletableFuture<>();
        client.findByAge(Int32Value.newBuilder().setValue(20).build())
                .subscribe().with(message::complete);
        PersonProto.Persons p = message.get(1, TimeUnit.SECONDS);
        assertNotNull(p);
        assertEquals(1, p.getPersonCount());
    }

    @Test
    @Order(2)
    void shouldFindByName() throws ExecutionException, InterruptedException, TimeoutException {
        CompletableFuture<PersonProto.Persons> message = new CompletableFuture<>();
        client.findByName(StringValue.newBuilder().setValue("Test").build())
                .subscribe().with(message::complete);
        PersonProto.Persons p = message.get(1, TimeUnit.SECONDS);
        assertNotNull(p);
        assertEquals(1, p.getPersonCount());
    }
}

Running and Testing Quarkus Locally

Let’s run our Quarkus app locally in the dev mode:

$ mvn quarkus:dev

Quarkus will start the Postgres database and expose gRPC services on the port 9000:

We can go to the Quarkus Dev UI console. It is available under the address http://localhost:8080/q/dev-ui. Once you do it, you should see the gRPC tile as shown below:

quarkus-grpc-ui

Click the Services link inside that tile. You will be redirected to the site with a list of available gRPC services. After that, we may expand the row with the model.PersonsService name. It allows to perform a test call of the selected gRPC method. Let’s choose the AddPerson tab. Then we can insert the request in the JSON format and send it to the server by clicking the Send button.

quarkus-grpc-ui-send

If you don’t like UI interfaces you can use the grpcurl CLI instead. By default, the gRPC server is started on a port 9000 in the PLAINTEXT mode. In order to print a list of available services we need to execute the following command:

$ grpcurl --plaintext localhost:9000 list
grpc.health.v1.Health
model.PersonsService

Then, let’s print the list of methods exposed by the model.PersonsService:

$ grpcurl --plaintext localhost:9000 list model.PersonsService
model.PersonsService.AddPerson
model.PersonsService.FindAll
model.PersonsService.FindByAge
model.PersonsService.FindById
model.PersonsService.FindByName

We can also print the details about each method by using the describe keyword in the command:

$ grpcurl --plaintext localhost:9000 describe model.PersonsService.FindById
model.PersonsService.FindById is a method:
rpc FindById ( .google.protobuf.Int64Value ) returns ( .model.Person );

Finally, let’s call the endpoint described with the command visible above. We are going to find the previously added person (via UI) by the id field value.

$ grpcurl --plaintext -d '1' localhost:9000 model.PersonsService.FindById
{
  "id": "1",
  "name": "Test",
  "age": 20,
  "gender": "FEMALE"
}

Running Quarkus gRPC on OpenShift

In the last step, we will run our app on OpenShift and try to interact with the gRPC service through the OpenShift Route. Fortunately, we can leverage Quarkus extension for OpenShift. We can include the quarkus-openshift dependency in the optional Maven profile.

<profile>
  <id>openshift</id>
  <activation>
  <property>
    <name>openshift</name>
  </property>
  </activation>
  <dependencies>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-openshift</artifactId>
    </dependency>
  </dependencies>
  <properties>
    <quarkus.kubernetes.deploy>true</quarkus.kubernetes.deploy>
    <quarkus.profile>openshift</quarkus.profile>
  </properties>
</profile>

Once we run a Maven build with the option -Popenshift it will activate the profile. Thanks to that Quarkus will handle all things required for building image and running it on the target cluster.

$ mvn clean package -Popenshift -DskipTests

In order to test our gRPC service via the OpenShift Route we need to expose it over SSL/TLS. Here’s the secret that contains both the certificate and private key. It was issued by the cert-manager for the the Route hostname.

Quarkus Kubernetes Extension offers the ability to automatically generate Kubernetes resources based on the defaults and user-supplied configuration using dekorate. It currently supports generating resources for vanilla Kubernetes, OpenShift, and Knative.

We need to configure several things in the Quarkus app. We didn’t have to take care of it when running in the dev mode. First of all, we need to provide Postgres database connection settings (1). Thanks to the Quarkus OpenShift module we can generate YAML manifest using configuration properties. The database is available on OpenShift under the person-db address. The rest of the credentials can be taken from the person-db secret (2).

Then we will mount a secret with a TLS certificate and private key to the DeploymentConfig (3). It will be available inside the pod under the /mnt/app-secret path. Then, we can enable SSL for the gRPC service by setting the certificate and private key for the server (4). We should also enable the reflection service, to allow tools like grpcurl to interact with our services. Once the SSL/TLS for the service is configured, we can create the passthrough Route that exposes it outside the OpenShift cluster (5).

# (1)
%prod.quarkus.datasource.username = ${POSTGRES_USER}
%prod.quarkus.datasource.password = ${POSTGRES_PASSWORD}
%prod.quarkus.datasource.reactive.url = vertx-reactive:postgresql://person-db:5432/${POSTGRES_DB}

# (2)
quarkus.openshift.env.mapping.postgres_user.from-secret = person-db
quarkus.openshift.env.mapping.postgres_user.with-key = database-user
quarkus.openshift.env.mapping.postgres_password.from-secret = person-db
quarkus.openshift.env.mapping.postgres_password.with-key = database-password
quarkus.openshift.env.mapping.postgres_db.from-secret = person-db
quarkus.openshift.env.mapping.postgres_db.with-key = database-name

# (3)
quarkus.openshift.app-secret = secure-callme-cert

# (4)
%prod.quarkus.grpc.server.ssl.certificate = /mnt/app-secret/tls.crt
%prod.quarkus.grpc.server.ssl.key = /mnt/app-secret/tls.key
%prod.quarkus.grpc.server.enable-reflection-service = true

# (5)
quarkus.openshift.route.expose = true
quarkus.openshift.route.target-port = grpc
quarkus.openshift.route.tls.termination = passthrough

quarkus.kubernetes-client.trust-certs = true
%openshift.quarkus.container-image.group = demo-grpc
%openshift.quarkus.container-image.registry = image-registry.openshift-image-registry.svc:5000

Here are the logs of the Quarkus app after running on OpenShift. As you see the gRPC server enables reflection service and is exposed over SSL/TLS.

quarkus-grpc-logs

Let’s also display information about our app Route. We will interact with the person-grpc-service-demo-grpc.apps-crc.testing address using the grpcurl tool.

$ oc get route
NAME                  HOST/PORT                                        PATH   SERVICES              PORT   TERMINATION   WILDCARD
person-grpc-service   person-grpc-service-demo-grpc.apps-crc.testing          person-grpc-service   grpc   passthrough   None

Finally, we have to set the key and certificates used for securing the gRPC server as the parameters of grpcurl. It should work properly as shown below. You can also try other gRPC commands used previously for the app running locally in dev mode.

$ grpcurl -key grpc.key -cert grpc.crt -cacert ca.crt \
  person-grpc-service-demo-grpc.apps-crc.testing:443 list
grpc.health.v1.Health
model.PersonsService

Final Thoughts

Quarkus simplifies the development of gRPC services. For example, it allows us to easily generate Java classes from .proto files or configure SSL/TLS for the server with application.properties. No matter if we run it locally or remotely e.g. on the OpenShift cluster, we can easily achieve it by using Quarkus features like Dev Services or Kubernetes Extension. This article shows how to take advantage of Quarkus gRPC features and use gRPC services on OpenShift.

Leave a Reply