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: id
, name
, 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:
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.
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.
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