DEV Community

Cover image for GraphQL or gRPC in Java
Julien Fabre
Julien Fabre

Posted on • Edited on

GraphQL or gRPC in Java

All this article is available in my repository

GitHub logo 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.
  • 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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Exactly the line :

register(GraphQlSupport.create(buildSchema(dbClient)))
Enter fullscreen mode Exit fullscreen mode

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));
  }
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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,

insomnia-graphql

view schema information,

insomnia-graphql-schema

and do some requests.

insomnia-graphql-request

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);
}

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

By default, port is on 1408 but you can redefine it in your yaml like this :

grpcserver:
  port: 3333
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
Enter fullscreen mode Exit fullscreen mode

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,

insomnia-grpc

load protobuf file,

Load protobuf

select your service to test after adding url,

insomnia-grpc-service

and use it.
insomnia-grpc-create

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.

Top comments (2)

Collapse
 
anaktas profile image
Anastasios • Edited

From my experience while using both of them, in the company in which I currently work, we use GraphQL and gRPC exactly as you concluded in your article. We use gRPC for the communication between API gateways and microservices (or even between microservices in some cases) and GraphQL as the backend for the mobile platforms. We try to utilize gRPC on the mobile platforms for some functionalities, but unfortunately there is no related swift library for gRPC, but on android it is available both for java and kotlin. Moreover, HTTP/2 is not widely adopted yet, thus trying to use it for fronted communication will not always leverage the pros of the HTTP/2.

Just a small correction, GraphQL is not based on REST at all. Every request is a POST request in a single endpoint. This is a major drawback since web caching doesn't work (although, there are plenty other ways to cache, either on client side, or in server side).

Collapse
 
jufab profile image
Julien Fabre

Thanks for your comment and thanks to confirm my conclusion :-)

You right about REST... I wanted to talk about schema not REST! There is only one ressource with one verb (POST) as you say (and as i say operate on a single endpoint...)

I'm going to change that, it's so wrong :-)