Julien Fabre
Posted on March 9, 2021
All this article is available in my repository
jufab
/
graphql-grpc-helidon
Lab around GraphQL and gRPC with Helidon
This article presents GraphQL and gRPC implemented with Java.
With the same use case, I'm going to show you how to expose and to implement these protocols.
I use a simple use case but not just a one table use case...Ok, just a 2 tables with a relation one-to-one.
To make this article, I use Helidon.
Why Helidon? Why not! ...seriously, because this framework offers the possibility to implement these protocols ( and of course, for me, to discover this framework ;-) )
Table of contents
About Helidon
Helidon is a java framework with 2 versions :
- Helidon SE : A Reactive Microframework.
- Tiny Footprint
- Functional Style
- Simple and Transparent
- GraalVM Native Image
- More information here
- Helidon MP : A MicroProfile implementation.
- Support MP 3.3
- Small Footprint
- Declarative Style
- Dependency Injection
- CDI, JAX-RS, JSON-P/B
- More information here
In these 2 versions, Helidon offers a lot of facilities around GraphQL or gRPC implementations (that's a good reason to use it for this article)
There is a lot of other possibilities like reactive streams, reactive messaging or predefined health-check or metrics...
And gives facility to build it with docker or GraalVM and to deploy it with Kubernetes.
So, try it for fun and why not for your project ;-)
Use case
Definition
Just a simple use case : A person with an address => Address is in a separate table for this article.
So we have :
Helidon DB Client
For the database and my use case, I used an H2 in-memory Database and Helidon DB-Client.
Helidon DB Client is a reactive API for access database. You can access with JDBC Driver (H2, Oracle, MySQL...) or directly with MongoDB.
For this article, I used a JDBC. So I added these dependencies :
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.dbclient</groupId>
<artifactId>helidon-dbclient-jdbc</artifactId>
</dependency>
All the database implementation is here in this maven module
It contains 2 important files in resources :
- db.yaml : contains configuration for h2 Database
- statements.yaml : contains all statements like tables creation or sequences...
Init Project
Use archetype from documentation
mvn -U archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=io.helidon.archetypes \
-DarchetypeArtifactId=helidon-quickstart-se \
-DarchetypeVersion=2.2.1 \
-DgroupId=fr.jufab \
-DartifactId=graphql-helidon \
-Dpackage=fr.jufab.graphql
and
mvn -U archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=io.helidon.archetypes \
-DarchetypeArtifactId=helidon-quickstart-se \
-DarchetypeVersion=2.2.1 \
-DgroupId=fr.jufab \
-DartifactId=grpc-helidon \
-Dpackage=fr.jufab.grpc
I transformed projects into 3 maven modules and deleted all the GraalVM or Docker builder for reused database in graphQL or gRPC module.
Or, you can use helidon cli to manage your project.
https://helidon.io/docs/latest/#/about/05_cli
GraphQL
So now, let's talk about GraphQL.
GraphQL is a query language and operates on a single endpoint. it talks with JSON language. It defines with a schema, describes valid attributes, operations, etc...
And GraphQL is a specification
Schema
To define GraphQL, we need to use a schema with this information :
- Object : composition of an object response
- Query : query to request objects or array's object.
- Mutation : to save/modify objects.
- Subscription : to establish a bi-directional communication channel using WebSocket
With the use case, the schema, available here, is :
# Objects
type Person {
id: ID!
firstname: String!
lastname: String!
age: Int
gender: Gender!
address: Address
}
enum Gender {
WOMAN,
MAN
}
type Address {
id: ID!
street: String!
zipCode: String!
city: String!
}
# Query
type Query {
personById(id: ID!): Person
personsByFirstName(firstname: String!): [Person]
persons:[Person]
}
# Mutation
type Mutation {
createPersonWithAddress(firstname: String!, lastname: String!, age: Int, gender: Gender, street: String!, zipCode: String!, city: String!):Person
createPerson(firstname: String!, lastname: String!, age: Int, gender: Gender, idAddress: ID!):Person
createAddress(street: String!, zipCode: String!, city: String!): Address
}
Maven
In helidon SE, there is a facility to use GraphQL with a helidon GraphQL library.
More information about this integration in the documentation
For the moment, this feature is experimental...but it works for this project :)
To insert GraphQL in the project, I added the dependency in the pom.xml file
<dependency>
<groupId>io.helidon.graphql</groupId>
<artifactId>helidon-graphql-server</artifactId>
</dependency>
This dependency uses GraphQL java version 15.0.
Implementation
Server
GraphQL must be registered to the webserver.
In the Main class, it was registered here :
WebServer server = WebServer.builder()
.routing(Routing.builder()
.register(health) // Health at "/health"
.register(MetricsSupport.create()) // Metrics at "/metrics"
.register(GraphQlSupport.create(buildSchema(dbClient)))
.build())
.config(config.get("server"))
.build();
Exactly the line :
register(GraphQlSupport.create(buildSchema(dbClient)))
The GraphQlSupport class takes a GraphQLSchema. Method buildSchema() creates a configured GraphQLSchema.
private static GraphQLSchema buildSchema(DbClient dbClient) {
SchemaParser schemaParser = new SchemaParser();
Resource schemaResource = Resource.create(PERSON_GRAPHQLS);
TypeDefinitionRegistry typeDefinitionRegistry =
schemaParser.parse(schemaResource.string(StandardCharsets.UTF_8));
SchemaGenerator schemaGenerator = new SchemaGenerator();
return schemaGenerator.makeExecutableSchema(typeDefinitionRegistry,
buildRuntimeWiring(dbClient));
}
it requires:
- schema : person.graphqls
- parser : to instantiate a typeDefinitionRegistry
- RuntimeWiring : to connect data with datafetcher
Method buildRuntimeWiring connects Object (Person, Address) with datafetcher.
private static RuntimeWiring buildRuntimeWiring(DbClient dbClient) {
AddressRepository addressRepository = new AddressRepository(dbClient);
AddressDataFetcher addressDataFetcher = new AddressDataFetcher(addressRepository);
PersonRepository personRepository = new PersonRepository(dbClient);
PersonDataFetcher personDataFetcher = new PersonDataFetcher(personRepository);
return RuntimeWiring.newRuntimeWiring()
.type(TypeRuntimeWiring.newTypeWiring("Query")
.dataFetcher("persons", personDataFetcher.getPersons()))
.type(TypeRuntimeWiring.newTypeWiring("Query")
.dataFetcher("personById", personDataFetcher.getPersonById()))
.type(TypeRuntimeWiring.newTypeWiring("Query")
.dataFetcher("personsByFirstName", personDataFetcher.getPersonsByFirstName()))
.type(TypeRuntimeWiring.newTypeWiring("Person")
.dataFetcher("address", addressDataFetcher.getAddressById()))
.type(TypeRuntimeWiring.newTypeWiring("Mutation")
.dataFetcher("createPersonWithAddress", personDataFetcher.createPersonWithAddress()))
.type(TypeRuntimeWiring.newTypeWiring("Mutation").dataFetcher("createAddress",
addressDataFetcher.createAddress()))
.build();
}
There are all Query or Mutation in this method and all of them are link with a DataFetcher
DataFetcher
All Objects will be associated to a DataFetcher object. DataFetcher has the responsibility to load objects for Query or to save objects for Mutation.
How it works :
You define a DataFetcher with a DataFetchingEnvironment object. This object contains all arguments or fields to be fetched.
like this (PersonDataFetcher):
// To get all Persons... no arguments
public DataFetcher<List<Person>> getPersons() {
return environment -> personRepository.getPersons().collectList().get();
}
//To get all by FirstName
public DataFetcher<List<Person>> getPersonsByFirstName() {
return environment -> personRepository.getPersonsByFirstName(environment.getArgument("firstname")).collectList().get();
}
// Or to create a person
public DataFetcher<Person> createPersonWithAddress() {
return environment -> this.personRepository.createPerson(
new Person(environment.getArgument("firstname"), environment.getArgument("lastname"),
environment.getArgument("age"),
new Address(environment.getArgument("street"), environment.getArgument("zipCode"),
environment.getArgument("city")),
Gender.valueOf(environment.getArgument("gender")))).get();
}
More information about DataFetching => https://www.graphql-java.com/documentation/v16/data-fetching/
Test
For testing resources, I use insomnia.
You can access GraphQL requests like that,
view schema information,
and do some requests.
And that's it for GraphQL...
gRPC
gRPC helps you to build web services. It's cross-language but this project is a Java project so... :)
To define and describe service, gRPC uses a simple definition file and uses protocol buffers format.
For more informations : https://grpc.io/
Protocol buffers
gRPC uses protocol buffers : https://developers.google.com/protocol-buffers
Protocol buffers is a language for serializing structured data.
You can define some options for every language: "package" for java or "objc_class_prefix" for Objective-C to prefix generated classes.
For this lab, I used the syntax version "proto3"
Protobuf file contains :
- message : Object Data
- enum : define enumeration
- service : Service to query, modify or save data
With the use cases, proto file available here has this definition
syntax = "proto3";
option java_package = "fr.jufab.grpc.proto";
option java_multiple_files = true;
option java_generic_services = true;
option java_outer_classname = "Helidon";
enum Gender {
WOMAN = 0;
MAN = 1;
}
message Person {
int32 id = 1;
string firstname = 2;
string lastname = 3;
int32 age = 4;
Gender gender = 5;
Address address = 6;
}
message Address {
int32 id = 1;
string street = 2;
string zipCode = 3;
string city = 4;
}
message Persons {
repeated Person persons=1;
}
message QueryPerson {
int32 id=1;
string firstname=2;
}
message PersonWithAddressToSave {
string firstname = 1;
string lastname = 2;
int32 age = 3;
Gender gender = 4;
string street = 5;
string zipCode = 6;
string city = 7;
}
message PersonToSave{
string firstname = 1;
string lastname = 2;
int32 age = 3;
Gender gender = 4;
int32 idAddress = 5;
}
message AddressToSave{
string street = 1;
string zipCode = 2;
string city = 3;
}
message QueryAddress {
int32 id = 1;
}
service PersonService {
rpc persons(QueryPerson) returns (Persons);
rpc personById(QueryPerson) returns (Person);
rpc personsByFirstName(QueryPerson) returns (Persons);
rpc createPersonWithAddress(PersonWithAddressToSave) returns (Person);
rpc createPerson(PersonToSave) returns (Person);
}
service AddressService {
rpc createAddress(AddressToSave) returns (Address);
rpc addressById(QueryAddress) returns (Address);
}
Maven
Helidon SE offers a maven dependency to deploy gRPC server.
More information about this integration in the documentation
Like GraphQL, gRPC feature is experimental for the moment...but it works for this project :)
To insert gRPC in the project, I added the dependency in the pom.xml
<dependency>
<groupId>io.helidon.grpc</groupId>
<artifactId>helidon-grpc-server</artifactId>
</dependency>
But to use gRPC and specially protobuf, that's not enough...
You must generate data from protobuf file with a special maven plugin. This plugin use Protocol Buffer Compiler.
In this project, to generate classes, there is this maven definition in pom.xml.
<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>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
more info about this plugin here https://www.xolstice.org/protobuf-maven-plugin/
All classes are generated in "generated-sources".
Implementation
Server
To start gRPC server, Helidon offers a specific server on this purpose.
You need to start a GrpcServer like this
GrpcServer grpcServer = GrpcServer
.create(GrpcServerConfiguration.create(config.get("grpcserver")),GrpcRouting.builder()
.register(buildPersonServiceGrpc(dbClient)) //See after for this
.register(buildAddressServiceGrpc(dbClient)) //See after for this
.build())
.start()
.toCompletableFuture()
.get(10, TimeUnit.SECONDS);
By default, port is on 1408 but you can redefine it in your yaml like this :
grpcserver:
port: 3333
Service
You can see in server, the service registered "PersonServiceGrpc" or "AddressServiceGrpc".
These 2 services available here and here
These services implement service supplied by Protobuf generated classes. So, in "PersonGrpcService", this class implements "PersonServiceGrpc.PersonServiceImplBase".
By default, you can't see this class "PersonServiceGrpc" because you need to generate class with the protobuf file. Why my package is in "fr.jufab.grpc.proto"? Because I used an option in protobuf :
option java_package = "fr.jufab.grpc.proto";
For example, PersonServiceGrpc gives me all methods define in protobuf.
Remember service in protbuf :
service PersonService {
rpc persons(QueryPerson) returns (Persons);
rpc personById(QueryPerson) returns (Person);
rpc personsByFirstName(QueryPerson) returns (Persons);
rpc createPersonWithAddress(PersonWithAddressToSave) returns (Person);
rpc createPerson(PersonToSave) returns (Person);
}
and PersonServiceGrpc gives you :
public static abstract class PersonServiceImplBase implements io.grpc.BindableService {
/**
*/
public void persons(fr.jufab.grpc.proto.QueryPerson request,
io.grpc.stub.StreamObserver<fr.jufab.grpc.proto.Persons> responseObserver) {
asyncUnimplementedUnaryCall(METHOD_PERSONS, responseObserver);
}
/**
*/
public void personById(fr.jufab.grpc.proto.QueryPerson request,
io.grpc.stub.StreamObserver<fr.jufab.grpc.proto.Person> responseObserver) {
asyncUnimplementedUnaryCall(METHOD_PERSON_BY_ID, responseObserver);
}
/**
*/
public void personsByFirstName(fr.jufab.grpc.proto.QueryPerson request,
io.grpc.stub.StreamObserver<fr.jufab.grpc.proto.Persons> responseObserver) {
asyncUnimplementedUnaryCall(METHOD_PERSONS_BY_FIRST_NAME, responseObserver);
}
/**
*/
public void createPersonWithAddress(fr.jufab.grpc.proto.PersonWithAddressToSave request,
io.grpc.stub.StreamObserver<fr.jufab.grpc.proto.Person> responseObserver) {
asyncUnimplementedUnaryCall(METHOD_CREATE_PERSON_WITH_ADDRESS, responseObserver);
}
/**
*/
public void createPerson(fr.jufab.grpc.proto.PersonToSave request,
io.grpc.stub.StreamObserver<fr.jufab.grpc.proto.Person> responseObserver) {
asyncUnimplementedUnaryCall(METHOD_CREATE_PERSON, responseObserver);
}
}
Now, just implements these methods with your repository or DB access.
For method "persons" in PersonGrpcService, I implemented it like that :
@Override public void persons(QueryPerson request,
StreamObserver<fr.jufab.grpc.proto.Persons> responseObserver) {
try {
complete(responseObserver, buildPersonsGrpc(personRepository.getPersons()
.collectList()
.get()));
} catch (InterruptedException e) {
LOGGER.log(Level.SEVERE, "Error", e);
} catch (ExecutionException e) {
LOGGER.log(Level.SEVERE, "Error", e);
}
}
You can see all implementation in PersonGrpcService and AddressGrpcService
Test
For testing gRPC services, I use insomnia too.
You can access gRPC services like that,
load protobuf file,
select your service to test after adding url,
That's it for gRPC.
Conclusion
GraphQL and gRPC offer a same approach : to use a schema and to generate resources from description.
GraphQL is an HTTP protocol with schema definition. So you can access to this resource with any language who accepts HTTP request.
gRPC communicates over his protocol (HTTP/2) and you need to generate your service from the protobuf.
In my opinion :
- GraphQL may be better for external exposition like for mobile, open API, surely other case.
- gRPC may be better for internal communication like in cloud, with defined client, in K8s Cluster, in information system...
Of course, it can depend on your use. Maybe this article can help you to choose.
Posted on March 9, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.