Skip to content

jufab/graphql-grpc-helidon

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GraphQL or gRPC in Java

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.
  • 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 :

schema

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,

schema

view schema information,

schema

and do some requests.

schema

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

You can see example here

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,

schema

load protobuf file,

schema

select your service to test after adding url,

schema

and use it.

schema

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.

Releases

No releases published

Packages

No packages published

Languages