Il y a 25 ans, la société Sun créait le langage Java. La jvm n’a cessé de gagner en performance et s’enrichir en fonctionnalités. Une grande partie du succès du Java réside sur sa portabilité et sa facilité de gestion de la mémoire avec son garbage collector. Cependant, nous oublions souvent de citer son système de compilation à chaud, le Just In Time.

Mais c’est quoi la compilation à chaud ?

Il s’agit d’une technique visant à améliorer la performance de bytecode-compilés par une traduction en code machine natif au moment de l’exécution. La compilation à la volée se fonde sur deux anciennes idées : la compilation de bytecode et la compilation dynamique.

Dans un système dit bytecode-compilé, le code source est compilé à l’avance ou à la volée (lors de l’exécution) dans une représentation intermédiaire, le bytecode. C’est le cas par exemple des langages Limbo, Smalltalk, Perl, PHP, Python, Ruby, Lua, GNU Common Lisp ou encore Java, entre autres.

[baptiste@KEYWER GraalArticle]$ vi Greeting.java
[baptiste@KEYWER GraalArticle]$ javac Greeting.java 
[baptiste@KEYWER GraalArticle]$ ls -als
total 16
4 drwxr-xr-x. 2 baptiste baptiste 4096 14 mai   10:54 .
4 drwxr-xr-x. 3 baptiste baptiste 4096 14 mai   10:50 ..
4 -rw-rw-r--. 1 baptiste baptiste  461 14 mai   10:54 Greeting.class <= ByteCode
4 -rw-rw-r--. 1 baptiste baptiste  125 14 mai   10:53 Greeting.java
[baptiste@KEYWER GraalArticle]$ java Greeting 
Hello !

Le bytecode n’est pas un code machine, c’est-à-dire que ce n’est pas un code optimisé pour un type d’architecture d’ordinateur en particulier. On dit du bytecode qu’il est portable entre différentes architectures. Ce bytecode est ensuite interprété ou bien exécuté par une machine virtuelle.

La production de bytecode n’est que la première étape d’un processus d’exécution plus complexe. Le bytecode est ensuite déployé sur le système cible, lors de son execution le JIT, le traduit en code machine natif (ie. optimisé pour l’architecture de la machine exécutant le programme). Ceci peut être fait sur un fichier entier, ou spécifiquement sur une fonction du programme.

La compilation à la volée s’adapte dynamiquement à la charge de travail courante du logiciel, en compilant le code « chaud », c’est-à-dire le code le plus utilisé à un moment donné. Obtenir du code machine optimisé se fait beaucoup plus rapidement depuis du bytecode que depuis du code source. Comme le bytecode déployé est portable, la compilation à la volée est envisageable pour tout type d’architecture, à la condition d’avoir un compilateur JIT pour cette architecture.

LES SOURCES DE L’OPENJDK SONT CONSULTABLES SUR GITHUB.

Vous remarquerez qu’il est composé de sources de plusieurs langages.

EN PARCOURANT LES SOURCES, NOUS TROUVONS DU CODE C++ SPÉCIFIQUE À DE NOMBREUSES ARCHITECTURES :

[baptiste@KEYWER hotspot]$ tree . -d
.
├── cpu
│   ├── aarch64
│   ├── arm
│   ├── ppc
│   ├── s390
│   ├── sparc
│   ├── x86
│   └── zero
├── os
│   ├── aix
│   ├── bsd
│   ├── linux
│   ├── posix
│   ├── solaris
│   └── windows
└── os_cpu
    ├── aix_ppc
    ├── bsd_x86
    ├── bsd_zero
    ├── linux_aarch64
    ├── linux_arm
    ├── linux_ppc
    ├── linux_s390
    ├── linux_sparc
    ├── linux_x86
    ├── linux_zero
    ├── solaris_sparc
    ├── solaris_x86
    └── windows_x86

Le JIT est composé de deux compilateurs :

LA JVM COMBINE L’USAGE DE CES DEUX COMPILATEURS.

Lors de la première exécution du code, la Jvm utilisera le C1. La compilation C2 se déclenche selon une table de statistiques maintenue par la Jvm, c’est elle qui décidera de son lancement.

L’usage de ce mécanisme a permis de faire des gains énormes en performance, mais C2 est devenu de plus en plus complexe et difficile à maintenir. C’est en ayant conscience des limitations de cette technique que la société Oracle publia dans sa version 9 de Java une API permettant de fournir sa propre implémentation de compilation de code (JEP-243). C’est là qu’intervient GraalVM !


GraalVM est une machine virtuelle développée par Oracle. Bien que basée sur la HotSpot nous allons voir en quoi cette JVM en est différente.

Mise en place :

GraalVm est divisée en deux éditions la Community et l’Entreprise Edition. Pour un usage personnel il est tout à fait possible de télécharger la version Entreprise. Deux versions de HotSpot sont proposées la 8 et la 11.

[baptiste@KEYWER latest]$ $GRAALVM_HOME/bin/java -version
java version "11.0.7" 2020-04-14 LTS
Java(TM) SE Runtime Environment GraalVM EE 20.0.1 (build 11.0.7+8-LTS-jvmci-20.0-b04)
Java HotSpot(TM) 64-Bit Server VM GraalVM EE 20.0.1 (build 11.0.7+8-LTS-jvmci-20.0-b04, mixed mode, sharing)

UNE NOUVELLE IMPLÉMENTATION DU JIT

Son implémentation a totalement été revue, le C++ a été remplacé par du Java, plus facile à maintenir.

Pour le tester nous suivrons un exemple proposé sur le site de GraalVM (n’hésitez pas à y faire un tour).

Voici un programme permettant de compter le nombre de caractères en majuscule dans un texte, exécuté de nombreuses fois.

public class CountUppercase {
    static final int ITERATIONS = Math.max(Integer.getInteger("iterations", 1), 1);
    public static void main(String[] args) {
        String sentence = String.join(" ", args);
        for (int iter = 0; iter < ITERATIONS; iter++) {
            if (ITERATIONS != 1) System.out.println("-- iteration " + (iter + 1) + " --");
            long total = 0, start = System.currentTimeMillis(), last = start;
            for (int i = 1; i < 10_000_000; i++) {
                total += sentence.chars().filter(Character::isUpperCase).count();
                if (i % 1_000_000 == 0) {
                    long now = System.currentTimeMillis();
                    System.out.printf("%d (%d ms)%n", i / 1_000_000, now - last);
                    last = now;
                }
            }
            System.out.printf("total: %d (%d ms)%n", total, System.currentTimeMillis() - start);
        }
    }
}

Nous allons exécuter ce code en désactivant la nouvelle version du JIT avec l’argument -XX:-UseJVMCICompiler

[baptiste@KEYWER tuto_graalvm]$ $GRAALVM_HOME/bin/javac CountUppercase.java
[baptiste@KEYWER tuto_graalvm]$ $GRAALVM_HOME/bin/java -XX:-UseJVMCICompiler CountUppercase In 2020 I would like to run ALL languages in one VM.
1 (497 ms)
2 (433 ms)
3 (415 ms)
4 (410 ms)
5 (415 ms)
6 (414 ms)
7 (415 ms)
8 (414 ms)
9 (413 ms)
total: 69999993 (4239 ms)

Le traitement a pris 4239 ms.

En aparté, le – désactive (-XX:-UseJVMCICompiler) et le + active (-XX:+UseJVMCICompiler).

Nous allons relancer la même exécution en laissant le JVMCICompiler activé par défaut.

[baptiste@KEYWER tuto_graalvm]$ $GRAALVM_HOME/bin/java CountUppercase In 2020 I would like to run ALL languages in one VM.
1 (152 ms)
2 (152 ms)
3 (79 ms)
4 (77 ms)
5 (79 ms)
6 (78 ms)
7 (77 ms)
8 (80 ms)
9 (79 ms)
total: 69999993 (932 ms)

Cette fois-ci l’exécution a pris 920 ms, soit 4 fois moins de temps !

LA COMPILATION NATIVE

Aussi appelé AOT (Ahead Of Time), cette fonctionnalité a été introduite dans Java 9 via la JEP 295. L’idée est de considérer que tout ce qui est fait par le JIT en runtime peut être réalisé en phase de compilation.

Prenons une classe dont une fonction est appelée plusieurs fois d’affilé :

public class MultipleCall {

  public int f() throws Exception {
    int a = 5;
    return a;
  }

  public static void main(String[] args) throws Exception {
    for (int i = 1; i <= 10; i++) {
      System.out.println("call " + Integer.valueOf(i));
      long a = System.nanoTime();
      new Test().f();
      long b = System.nanoTime();
      System.out.println("elapsed= " + (b-a));
    }

  }
}

Commençons par expérimenter ce programme via une HotSpot :

[baptiste@KEYWER tuto_graalvm]$ $JAVA_HOME/bin/java -version
openjdk version "11.0.5" 2019-10-15
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.5+10)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.5+10, mixed mode)
[baptiste@KEYWER native-list-dir]$ $JAVA_HOME/bin/javac MultipleCall.java 
[baptiste@KEYWER native-list-dir]$ time $JAVA_HOME/bin/java MultipleCall 
[baptiste@DESKTOP-FUI7H3K 02_AOT_HOTSPOT]$ $JAVA_HOME/bin/java Test
call 1
elapsed= 3740
call 2
elapsed= 709
call 3
elapsed= 352
call 4
elapsed= 317
call 5
elapsed= 349
call 6
elapsed= 306
call 7
elapsed= 281
call 8
elapsed= 297
call 9
elapsed= 295
call 10
elapsed= 322

Pour utiliser l’AOT il nous faut utiliser l’exécutable jaotc

[baptiste@KEYWER native-list-dir]$ $JAVA_HOME/bin/jaotc --output MultipleCall.so MultipleCall

Cette commande va convertir notre fichier Java en une librairie dans un format appelé SharedObjects (.so).

[baptiste@KEYWER tuto_graalvm]$ ls -alhs
total 8,1M
4,0K drwxr-xr-x.  3 baptiste baptiste 4,0K 14 mai   17:58  .
4,0K drwxr-xr-x.  9 baptiste baptiste 4,0K 14 mai   10:49  ..
4,0K -rw-rw-r--.  1 baptiste baptiste 2,4K 14 mai   17:57  MultipleCall.class
4,0K -rw-rw-r--.  1 baptiste baptiste  929  2 mai   11:44  MultipleCall.java
144K -rw-rw-r--.  1 baptiste baptiste 141K 14 mai   17:58  MultipleCall.so

Nous l’executerons de la manière suivante:

[baptiste@KEYWER tuto_graalvm]$ $JAVA_HOME/bin/java -XX:AOTLibrary=./MultipleCall.so MultipleCall 
call 1
elapsed= 2227
call 2
elapsed= 945
call 3
elapsed= 368
call 4
elapsed= 287
call 5
elapsed= 296
call 6
elapsed= 227
call 7
elapsed= 263
call 8
elapsed= 263
call 9
elapsed= 296
call 10
elapsed= 298

On constate de meilleurs performances au démarrage, les résultats finissent ensuite par tendre vers les mêmes résultats. Il est faux de se dire que l’AOT permet d’avoir un programme bien plus performant, en revanche il permet d’accéder plus rapidement à un code optimal, là ou le JIT aurait nécessité plusieurs itérations.  A noter qu’il est également possible d’effectuer cette manipulation sur un module notamment ceux du JDK.

Expérimentons cette fonctionnalité avec GraalVM

Prenons le code suivant trouvé sur le site officiel de GraalVM:

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

public class ListDir {
        public static void main(String[] args) throws java.io.IOException {

                String root = ".";
                if(args.length > 0) {
                        root = args[0];
                }
                System.out.println("Walking path: " + Paths.get(root));

                long[] size = {0};
                long[] count = {0};

                try (Stream<Path> paths = Files.walk(Paths.get(root))) {
                        paths.filter(Files::isRegularFile).forEach((Path p) -> {
                                File f = p.toFile();
                                size[0] += f.length();
                                count[0] += 1;
                        });
                }

                System.out.println("Total: " + count[0] + " files, total size = " + size[0] + " bytes");
        }
}

Voici le résultat d’une éxecution classique:

[baptiste@KEYWER native-list-dir]$ $GRAALVM_HOME/bin/javac ListDir.java
[baptiste@KEYWER native-list-dir]$ time $GRAALVM_HOME/bin/java ListDir ..
Walking path: ..
Total: 611 files, total size = 55309806 bytes

real 0m0,197s
user 0m0,242s
sys 0m0,041s

L’exécutable de compilation native n’est pas présent par défaut. Pour l’édition entreprise il va falloir le télécharger.

[baptiste@KEYWER ~]$ cd $GRAALVM_HOME/bin
[baptiste@KEYWER bin]$ ./gu -L install native-image-installable-svm-svmee-java11-linux-amd64-20.0.1.jar 
Processing Component archive: native-image-installable-svm-svmee-java11-linux-amd64-20.0.1.jar
Installing new component: Native Image (org.graalvm.native-image, version 20.0.1)
[baptiste@KEYWER bin]$ ls -alhs
0 lrwxrwxrwx.  1 baptiste baptiste        27 14 mai   19:09 native-image -> ../lib/svm/bin/native-image

L’exécutable gu ou Graal Updater vous permet d’installer des composants en ligne de commande.

[baptiste@KEYWER native-list-dir]$ $GRAALVM_HOME/bin/javac ListDir.java
[baptiste@KEYWER native-list-dir]$ $GRAALVM_HOME/bin/native-image ListDir
Build on Server(pid: 20294, port: 43927)
[listdir:20294]    classlist:     119.83 ms,  1.00 GB
[listdir:20294]        (cap):     743.81 ms,  1.00 GB
[listdir:20294]        setup:   1,945.27 ms,  1.00 GB
[listdir:20294]   (typeflow):   5,436.31 ms,  1.20 GB
[listdir:20294]    (objects):   3,881.34 ms,  1.20 GB
[listdir:20294]   (features):     227.98 ms,  1.20 GB
[listdir:20294]     analysis:   9,894.44 ms,  1.20 GB
[listdir:20294]     (clinit):     189.02 ms,  1.20 GB
[listdir:20294]     universe:     490.97 ms,  1.20 GB
[listdir:20294]      (parse):   1,228.73 ms,  1.20 GB
[listdir:20294]     (inline):   1,114.86 ms,  1.20 GB
[listdir:20294]    (compile):   8,336.15 ms,  1.50 GB
[listdir:20294]      compile:  11,062.47 ms,  1.50 GB
[listdir:20294]        image:     686.83 ms,  1.50 GB
[listdir:20294]        write:     137.18 ms,  1.50 GB
[listdir:20294]      [total]:  24,546.82 ms,  1.50 GB

La première chose que l’on constate est que le temps de création de l’image est très long ! 25 secondes pour compiler une classe.

Mais que fait GraalVM pendant tout ce temps ?

GraalVM va effectuer tout un tas d’optimisations, notamment établir un graph du code appelé, pour ensuite enlever le code inutile.

[baptiste@KEYWER native-list-dir]$ ls -alsh
total 6,8M
4,0K drwxrwxr-x.  2 baptiste baptiste 4,0K 15 mai   10:46 .
4,0K drwxrwxr-x. 16 baptiste baptiste 4,0K  2 mai   12:15 ..
6,7M -rwxrwxr-x.  1 baptiste baptiste 6,7M 15 mai   10:46 listdir

On constate que l’exécutable pèse seulement 7M ! Pour le lancer, inutile de posséder un JRE puisqu’il s’agit de code natif.

IMAGINEZ LE GAIN POUR UNE IMAGE DOCKER !!!

[baptiste@KEYWER native-list-dir]$ time ./listdir ..
Walking path: ..
Total: 611 files, total size = 55309806 bytes

real 0m0,034s
user 0m0,014s
sys 0m0,020s

On constate un gain supplémentaire par rapport à l’AOT de la HotSpot. A noter que les applications natives s’exécutent sur machine virtuelle appelée SubstrateVM.

Ce n’est pas terminé, Graal propose une fonctionnalité (dans sa version entreprise), permettant de créer un fichier de profiling lors de l’exécution. Ce fichier peut être utilisé pour générer une image encore plus performante.

[baptiste@KEYWER native-list-dir]$ $GRAALVMEE_HOME/bin/native-image --pgo-instrument ListDir
...
[baptiste@KEYWER native-list-dir]$ time ./listdir ..
Walking path: ..
Total: 611 files, total size = 55309806 bytes

real 0m0,034s
user 0m0,014s
sys 0m0,020s

[baptiste@KEYWER native-list-dir]$ ls -als
total 46188
    4 drwxrwxr-x.  2 baptiste baptiste     4096 15 mai   11:19 .
    4 drwxrwxr-x. 16 baptiste baptiste     4096  2 mai   12:15 ..
 1840 -rw-------.  1 baptiste baptiste  1880350 15 mai   11:19 default.iprof
 
[baptiste@KEYWER native-list-dir]$ $GRAALVMEE_HOME/bin/native-image --pgo ListDir
[baptiste@KEYWER native-list-dir]$ time ./listdir ..
Walking path: ..
Total: 611 files, total size = 55309806 bytes

real 0m0,018s
user 0m0,006s
sys 0m0,011s
HotSpot 11GraalVM EEGraalVM EE Native ImageGraalVM EE Native Image + pgo
real 0m0,197suser 0m0,242ssys 0m0,041sreal 0m0,161suser 0m0,277ssys 0m0,037sreal 0m0,034suser 0m0,014ssys 0m0,020sreal 0m0,018suser 0m0,006ssys 0m0,011s

Résultat des courses, la taille de l’exécutable pèse seulement 7 Mo, plutôt que 200 Mo de JRE + 5 Mo de Jar. Les performances sont bien meilleures, plus de 10 fois plus rapide. Parfait pour un contexte cloud !

MAIS TOUT ÇA N’EST-T-IL PAS TROP BEAU ?

Eh oui, je ne vous ai pas tout dit. Ces avantages ont un prix, certaines fonctionnalités Java ne sont pas disponibles pour les images natives ! Je vous ai expliqué plus haut que lors de la création de l’image, Graal effectuait un graph permettant de retirer le code inutile et cela lors de la phase de compilation.

MAIS QU’EN EST-IL DES INSTRUCTIONS EXÉCUTÉES EN RUNTIME TELLES QUE LA RÉFLEXION ? COMMENT LES INITIALISATIONS STATIQUES SONT-ELLES RÉALISÉES ?

Commençons par nous intéresser à la réflexion

LA CONFIGURATION PAR FICHIER

Il suffit de lister les éléments pouvant être la cible de réflexion via un argument au moment de la génération de l’image.

$GRAALVM_HOME/bin/native-image -H:ReflectionConfigurationFiles=/path/to/reflectconfig -jar build/libs/aot-reflection-test.jar 

Voici à quoi ressemble le fichier, la déclaration peut être fine et aller jusqu’au niveau d’un attribut ou méthode d’une classe.

[
  {
    "name" : "java.lang.Class",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
    "allDeclaredClasses" : true,
    "allPublicClasses" : true
  },
  {
    "name" : "java.lang.String",
    "fields" : [
      { "name" : "value", "allowWrite" : true },
      { "name" : "hash" }
    ],
    "methods" : [
      { "name" : "<init>", "parameterTypes" : [] },
      { "name" : "<init>", "parameterTypes" : ["char[]"] },
      { "name" : "charAt" },
      { "name" : "format", "parameterTypes" : ["java.lang.String", "java.lang.Object[]"] }
    ]
  },
  {
    "name" : "java.lang.String$CaseInsensitiveComparator",
    "methods" : [
      { "name" : "compare" }
    ]
  }
]

Je vous joins les sources d’un projet expérimentant certains comportements de réflexion avec Graal.

UTILISER LES FEATURES GRAAL :

@AutomaticFeature
class RuntimeReflectionRegistrationFeature implements Feature {
  public void beforeAnalysis(BeforeAnalysisAccess access) {
    try {
      RuntimeReflection.register(String.class);
      RuntimeReflection.register(/* finalIsWritable: */ true, String.class.getDeclaredField("value"));
      RuntimeReflection.register(String.class.getDeclaredField("hash"));
      RuntimeReflection.register(String.class.getDeclaredConstructor(char[].class));
      RuntimeReflection.register(String.class.getDeclaredMethod("charAt", int.class));
      RuntimeReflection.register(String.class.getDeclaredMethod("format", String.class, Object[].class));
      RuntimeReflection.register(String.CaseInsensitiveComparator.class);
      RuntimeReflection.register(String.CaseInsensitiveComparator.class.getDeclaredMethod("compare", String.class, String.class));
    } catch (NoSuchMethodException | NoSuchFieldException e) { ... }
  }
}

Graal propose une API Feature permettant de configurer programmatiquement son image en buildTime.

SOLUTION 1:

Utiliser l’annotation @AutomaticFeature

SOLUTION 2:

L’activer via un argument lors de la génération de l’image : –features=<fqcn>

SOLUTION 3:

L’activer via un fichier de configuration dans le répertoire Resources/META-INF/GROUP_ID/ARTIFACT_ID/native-image.properties

Args = --features=com.keywer.graalvm.reflection.ConfigureAtBuildTimeFeature

Les initialisations de code static :

Par défaut elle sont dorénavant réalisées en phase de runtime. Pour gagner en temps de lancement il est possible d’effectuer ces initialisations lors de la génération de l’image.

Il vous faudra jouer avec ces deux arguments pour distinguer les classes a initialiser en :

Pour les applications complexes, cette opération peut se révéler être un véritable casse-tête.

Vous aurez alors la possibilité de tracer les dépendances via l’argument : -H:+TraceClassInitialization

Et ce n’est pas tout !

[baptiste@KEYWER bin]$ ls
bundle       javac      jhsdb       js            node            rmic
bundler      javadoc    jimage      jshell        npm             rmid
gem          javap      jinfo       jstack        pack200         rmiregistry
graalpython  jcmd       jjs         jstat         polyglot        Rscript
gu           jconsole   jlink       jstatd        R               ruby
irb          jdb        jmap        jvisualvm     rake            serialver
jar          jdeprscan  jmod        keytool       rdoc            truffleruby
jarsigner    jdeps      jps         lli           rebuild-images  unpack200
java         jfr        jrunscript  native-image  ri

Et oui vous ne rêvez pas, c’est bien l’exécutable Node que vous voyez. Je ne m’étendrai pas sur le sujet mais Graal est aussi une JVM dite polyglote. Elle est capable d’exécuter du code de plusieurs langages ! Oracle fournit une API appelée Truffle permettant à un langage d’exploiter le système de gestion de la mémoire de sa machine virtuelle.

Les outils

Est-il possible de faire du remote débug d’une application native ?

Pour les applications node JS oui, via devTool de Chrome.

$ node --inspect --jvm HelloWorld.js
Debugger listening on port 9229.
To start debugging, open the following URL in Chrome:
    chrome-devtools://devtools/bundled/js_app.html?ws=127.0.1.1:9229/76fcb6dd-35267eb09c3
Server running at http://localhost:8000/

Peut-on utiliser VisualVM avec une application native ?

Oui pour la version Entreprise, en ajoutant l’argument -H:+AllowVMInspection lors de la génération de l’image. Attention vous n’aurez pas accès aux Threads ou aux Beans JMX.

Existe-t-il un plugin maven ?

<plugin>
    <groupId>com.oracle.substratevm</groupId>
    <artifactId>native-image-maven-plugin</artifactId>
    <version>${graalvm.plugin.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>native-image</goal>
            </goals>
            <phase>package</phase>
        </execution>
    </executions>
</plugin>

Quels sont les frameworks Java compatibles avec GraalVM ?

Un gros travail est actuellement mené sur Spring pour le rendre totalement compatible.

Pour conclure

Oracle frappe fort et crée une petite révolution en proposant 3 évolutions majeures.

GraalVM a longtemps été considérée comme une VM expérimentale, ce n’est clairement plus le cas. Les grands frameworks ne s’y sont pas trompés et se rendent petit à petit compatible. RedHat a largement participé à la popularisation de GraalVM via Quarkus.

De manière globale, Oracle a adopté ces derniers temps une démarche beaucoup plus conquérante en terme de développement de son langage. Que ce soit par ses travaux sur Graal ou la publication de release tous les 6 mois. Dans un monde ou des langages performants émergent régulièrement tels que Rust ou Go, il est clair qu’il s’agit d’un enjeu stratégique pour Oracle de rester leader dans ce domaine.

Sources:

https://www.graalvm.org
https://developer.okta.com/blog/2019/11/27/graalvm-java-binaries
https://rieckpil.de/whatis-graalvm/
https://fr.wikipedia.org/wiki/Compilation_%C3%A0_la_vol%C3%A9e
https://www.baeldung.com/graal-java-jit-compiler
https://www.infoq.com/fr/articles/Graal-Java-JIT-Compiler/
https://www.baeldung.com/ahead-of-time-compilation
http://macias.info/entry/202001051700_graal_reflection.md
https://vertx.io/
https://helidon.io/#/
https://spark.apache.org/
https://quarkus.io/
https://micronaut.io/
https://medium.com/graalvm/simplifying-native-image-generation-with-maven-plugin-and-embeddable-configuration-d5b283b92f57

PRÉ-REQUIS:

Vous devrez disposer d’un jdk 8, d’un IDE et si possible du plugin lombok.

Vous trouverez l’intégralité du code sur la branche master. Chacune des étapes de ce tutoriel dispose d’une branche que vous pourrez checkouter en cas de souci.

Depuis tout petit j’ai toujours été passionné d’aquariophilie. J’ai implémenté une petite application me permettant de stocker quelques informations sur mes poissons.

Étape 1 : L’état des lieux

A) PRISE DE CONNAISSANCE DE LA STRUCTURE DE L’APPLICATION :

Il s’agit d’une application Spring boot exposant une api Rest.

A noter que les données sont servies par une base de données H2.

Vous y trouverez un certain nombre de fonctionnalités déjà implémentées, l’idée de ce tutoriel est de se focaliser sur GraphQL.

B) EXAMINONS L’API :

Vous trouverez dans le répertoire doc des exports de configuration Postman vous permettant interroger l’api.

Rest se base sur le protocole HTTP et repose sur un concept de découpage des données en ressource. Dans notre cas nous en avons deux.

Lancez l’application et appelez les deux services REST.

{
    "id": 1,
    "name": "Cichlidae",
    "waterType": "FRESH"
}

Étape 2 : Setup GraphQL

Je souhaite faire évoluer mon application et mettre en place GraphQL.

Il existe plusieurs librairies java :
– https://www.graphql-java.com/
– https://www.graphql-java-kickstart.com/spring-boot/
– https://download.eclipse.org/microprofile/microprofile-graphql-1.0/microprofile-graphql.html

Pour ce tutoriel je choisis GraphQL Kickstart pour sa bonne intégration à Spring Boot.

A) DÉPENDANCES MAVEN :

<!-- GraphQL -->
   <dependency>
     <groupId>com.graphql-java-kickstart</groupId>
     <artifactId>graphql-spring-boot-starter</artifactId>
     <version>${graphql.version}</version>
   </dependency>
   <!-- to embed GraphiQL tool -->
   <dependency>
     <groupId>com.graphql-java-kickstart</groupId>
     <artifactId>graphiql-spring-boot-starter</artifactId>
     <version>${graphql.version}</version>
     <scope>runtime</scope>
   </dependency>

La première contient l’implémentation, GraphiQL quant à lui est un client web dont nous parlerons plus tard.

B) CONFIGURATION DE L’APPLICATION SPRING BOOT :

graphql:
  servlet:
    mapping: /graphql
    enabled: true
    corsEnabled: true
    # if you want to @ExceptionHandler annotation for custom GraphQLErrors
    exception-handlers-enabled: true
    contextSetting: PER_REQUEST_WITH_INSTRUMENTATION
graphiql:
  mapping: /graphiql
  endpoint:
    graphql: /graphql
    subscriptions: /subscriptions
  subscriptions:
    timeout: 30
    reconnect: false
  static:
    basePath: /
  enabled: true

application.yaml

C) DÉCLARATION DU SCHÉMA GRAPHQL :

MASTERCLASS-GRAPHQL
│   .gitignore
│   pom.xml
│   README.md
└───src
    └───main
        ├───java
        │
        └───resources
            │   application.yaml
            │   data.sql
            │
            └───graphql
                    fish.graphqls

Le Schéma est l’élément central de l’application. GraphQL est dit déclaratif, c’est à l’intérieur de ce fichier que sont déclarés  l’ensemble des objets exposés.

Définissons ensemble l’objet Family :

enum WaterType {
    SEA,
    FRESH
}

type Family {
    id: ID!
    # Name of the family
    name: String
    # Type of water
    waterType: WaterType
    " Fish inner the family"
    fishs: [Fish!]
}

Il existe un certain nombre de champs de base appelés Scalar (ID, String, Int, …). Les types tableau sont indiqués par des crochets []. Il est aussi possible de définir des validations simples, telles que la notion de champs obligatoires avec le signe « ! ». Il est aussi possible de documenter votre API en utilisant « # » ou les guillemets.

D) EN VOUS INSPIRANT DE CE QUI A ÉTÉ FAIT PRÉCÉDEMMENT ET EN VOUS AIDANT DE CETTE DOCUMENTATION, DÉCLAREZ LE SCHÉMA DE L’OBJET FISH.

Étape 3 : Les Query

L’une des grandes forces de GraphQL, c’est qu’il impose un découpage de ses fonctionnalités d’interrogations et de mutations (pattern CQRS).

A) CRÉATION D’UNE QUERY SIMPLE :

L’objectif de cette tâche est d’exposer en GraphQL la fonctionnalité getMostExpensiveFish présente dans la classe MostExpensiveFish.

Tout ajout de fonctionnalité GrapQL doit faire l’objet de déclaration dans le Schéma.

type Query {
    # The most Expensive Fish
    mostExpensiveFish: Fish
}

Créez un nouveau package par exemple « graphql » et ajoutez la classe FishQueryResolver implémentant l’interface GraphQLQueryResolver.


import com.coxautodev.graphql.tools.GraphQLQueryResolver;

@Component
public class FishQueryResolver implements GraphQLQueryResolver {

    private FishDatabaseService fishDatabaseService;

    @Autowired
    public FishQueryResolver(FishDatabaseService fishDatabaseService) {
        this.fishDatabaseService = fishDatabaseService;
    }

    public Fish getMostExpensiveFish() {
        return fishDatabaseService.getMostExpensiveFish();
    }

B) GRAPHIQL

Ce client web est automatiquement lancé par SpringBoot. Lancez l’application et rendez-vous sur localhost:8080/graphiql


Exécutez la query :

query{
  mostExpensiveFish {
    id,
    name,
    family {
      waterType
    }
  }
}

En GraphQL le client est un acteur tout aussi important que la partie serveur. C’est lui qui détient la responsabilité de définir les champs dont vous souhaitez récupérer les valeurs ! Il est tout à fait possible de le faire en Rest, mais il s’agit d’une implémentation à réaliser coté serveur, ici il s’agit du comportement natif du client! Cela permet de réduire considérablement la taille du payload, imaginez les gains en terme réseau !

C) QUERY AVEC PARAMÈTRE :

Ce type de query se déclare de la même façon, il suffit tout simplement de faire attention au mapping des arguments schéma / QueryResolver.

Déclarez dans le schéma la méthode findFishByName située dans la classe FishDatabaseService en suivant les conseils de la documentation officielle.

query{
 fishByName(name:"Discus") {
   id,
  name
 }
}

D) QUERY AVEC PARAMÈTRES PLUS COMPLEXE

Pour les requêtes nécessitant d’avantage de paramètres, GraphQL met à disposition du développeur la notion d’Input. Il se déclare de la même façon qu’un objet classique à la différence près que cet objet ne peut être situé qu’en paramètre.

Prenons l’exemple de la pagination:

Commençons par créer l’objet java :


package com.keywer.masterclass.spring.graphql.graphql.input;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Positive;
import javax.validation.constraints.PositiveOrZero;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PaginationInput {
    @Positive
    private int first;
    @PositiveOrZero
    private int offset;
}

Puis la déclaration dans le schéma :


input PaginationInput {
    # Number of element returned
    first: Int
    # Index where element will be returned
    offset: Int
}
...
type Query {
    # Fish with pagination
    fishWithPagination(pagination: PaginationInput): [Fish]
}

Et dans le QueryResolver :

public List<Fish> fishWithPagination(PaginationInput paginationInput) {
     Fish first = fishDatabaseService.findByOffset(paginationInput.getOffset());
     return fishDatabaseService.findFish(first.getId(), paginationInput.getFirst());
 }
query{
 fishWithPagination(pagination: {first: 2, offset: 1}) {
  id,
  name
 }
}

E) METTEZ EN PLACE LA MÊME STRUCTURE POUR L’INPUT CURSORINPUT.

Étape 4 : Query coté client

A) LES FRAGMENTS :

Nous avons vu plus haut que le client était responsable de la récupération des champs et qu’il fallait énumérer un à un les champs récupérables. Il est possible grâce aux Fragments de définir un groupement de champs que l’on souhaite récupérer.

Lancez l’application et connectez-vous à l’interface de GraphiQL.

fragment standard on Fish {
 id,
 name
}

query{
 fishWithCursor(cursor: { first: 2,after: 3}) {
  ...standard,
  price
 }
}

Déclarez un fragment pour l’entity family et utilisez le sur la query mostExpensiveFish.

B) CLIENT PARAMÉTRABLE :

Dans GraphiQL se cache un petit onglet appelé « Variables »


Dans la zone query:

query Fish($name: String){ 
    fishByName(name: $name) { id, name } 
}

Puis dans la zone query variables:

{"name": "Discus"}

Utilisez cette technique sur la méthode fishWithPagination.

ÉTAPE 5 : LES RESOLVERS

Dans GraphQL kick start la notion de Resolvers est extrêmement importante. Le framework tente de résoudre le mapping Schéma/Code via le FieldResolver qui tentera d’effectuer un mapping par le nom de champ, puis par les méthodes commençant par getNAME, puis isNAME et enfin getNAME.

Il est aussi possible de créer son propre Resolver !

A) CRÉER UN RESOLVER PERSONNALISÉ :

Mettez le schéma à jour avec le type Purchase:

type Purchase {
    id:ID!
    shopName: String
}

type Fish {
    id: ID!
    "Fish name"
    name: String
    "Optimal temperature accept by the fish"
    temperature: Int
    "Price of the fish"
    price: Float
    "Family of the fish"
    family: Family
    "Information on the purchase"
    purchase: Purchase
}

Voici la classe Purchase :

package com.keywer.masterclass.spring.graphql.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Purchase {
    private long id;
    private String shopName;
}

Notre source de données sera un fichier json à mettre dans le répertoire resources:

[
  { "id": 1, "shopName": "BricoFish" },
  { "id": 2,  "shopName": "AquaFish" },
  { "id": 3,  "shopName": "FisherShop" }
]

Le code permettant de charger les données du fichier est le suivant :

public Purchase getPurchase(Fish fish) throws IOException {

   File purchasesDatasource = new File(getClass().getClassLoader().getResource("purchases.json").getFile());

   Purchase[] purchaseList = new ObjectMapper().readValue(purchasesDatasource, Purchase[].class);

   int randomIndex = new Random().nextInt(purchaseList.length);

   return purchaseList[randomIndex];
}

Déclarez une nouvelle classe appelé FishResolver :

@Component
public class FishResolver implements GraphQLResolver<Fish> {}

Complétez le code de la classe pour charger aléatoirement un achat.

Cet exercice, en agrégeant des données venant de deux sources différentes (sql et fichier json), de mettre en lumière un cas d’utilisation de GraphQL, la Gateway.

Étape 6 : Les Scalars personnalisés

Je souhaite maintenant préciser la date à laquelle j’ai effectué mon achat.

Le type Date ne fait pas parti des types scalar de base de GraphQL. Heureusement pour nous, GraphQL nous laisse la main pour créer nos propres types.

Enrichissons d’abord le schéma :

scalar Date

type Purchase {
    id:ID!
    shopName: String
    date: Date
}

Mettons à jour le fichier purchase.json avec des dates :

[
  { "id": 1, "shopName": "BricoFish", "date": "2009-01-12"},
  { "id": 2, "shopName": "AquaFish", "date": "2020-03-02"  },
  { "id": 3, "shopName": "FisherShop", "date": "1985-12-24"}
]

Cette fois-ci utilisons les configurators Spring pour déclarer notre nouveau type :

@Configuration
public class FishConfiguration {

    private static final String DATE_FORMAT = "yyyy-MM-dd";

    private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(DATE_FORMAT);

    @Bean
    public GraphQLScalarType getCustomDate() {
	GraphQLScalarType.newScalar().name("Date").description("A date with format " + DATE_FORMAT).coercing(new Coercing<LocalDate, String>() {
		 public String serialize(Object input) throws CoercingSerializeException {}
		@Override
		public LocalDate parseValue(Object input) throws CoercingParseValueException {}
		@Override
		public LocalDate parseLiteral(Object input) throws CoercingParseLiteralException {}

		}
	}
}

L’élément le plus important de la création d’un Scalar est le Coercing, c’est lui qui est en charge de serializer/deserializer le champ.

Voici le code (un peu complexe) du type Date :

@Bean
  public GraphQLScalarType getCustomDate() {
      return GraphQLScalarType.newScalar().name("Date").description("A date with format " + DATE_FORMAT).coercing(new Coercing<LocalDate, String>() {

          private LocalDate parseLocalDate(String s, Function<String, RuntimeException> exceptionMaker) {
              try {
                  TemporalAccessor temporalAccessor = dateFormatter.parse(s);
                  return LocalDate.from(temporalAccessor);
              } catch (DateTimeParseException e) {
                  throw exceptionMaker.apply("Invalid RFC3339 full date value : '" + s + "'. because of : '" + e.getMessage() + "'");
              }
          }

          @Override
          public String serialize(Object input) throws CoercingSerializeException {
              TemporalAccessor temporalAccessor;
              if (input instanceof TemporalAccessor) {
                  temporalAccessor = (TemporalAccessor) input;
              } else if (input instanceof String) {
                  temporalAccessor = parseLocalDate(input.toString(), CoercingSerializeException::new);
              } else {
                  throw new CoercingSerializeException(
                          "Expected a 'String' or 'java.time.temporal.TemporalAccessor' but was '" + input + "'."
                  );
              }
              try {
                  return dateFormatter.format(temporalAccessor);
              } catch (DateTimeException e) {
                  throw new CoercingSerializeException(
                          "Unable to turn TemporalAccessor into full date because of : '" + e.getMessage() + "'."
                  );
              }
          }

          @Override
          public LocalDate parseValue(Object input) throws CoercingParseValueException {
              TemporalAccessor temporalAccessor;
              if (input instanceof TemporalAccessor) {
                  temporalAccessor = (TemporalAccessor) input;
              } else if (input instanceof String) {
                  temporalAccessor = parseLocalDate(input.toString(), CoercingParseValueException::new);
              } else {
                  throw new CoercingParseValueException(
                          "Expected a 'String' or 'java.time.temporal.TemporalAccessor' but was '" + input + "'."
                  );
              }
              try {
                  return LocalDate.from(temporalAccessor);
              } catch (DateTimeException e) {
                  throw new CoercingParseValueException(
                          "Unable to turn TemporalAccessor into full date because of : '" + e.getMessage() + "'."
                  );
              }
          }

          @Override
          public LocalDate parseLiteral(Object input) throws CoercingParseLiteralException {
              if (!(input instanceof StringValue)) {
                  throw new CoercingParseLiteralException(
                          "Expected AST type 'StringValue' but was '" + input + "'."
                  );
              }
              return parseLocalDate(((StringValue) input).getValue(), CoercingParseLiteralException::new);
          }

      }).build();
  }

Ajoutez un champ date de type string dans la classe Purchase. Nous aurions pu mettre un type LocalDate, mais il aurait fallu implémenter un serialiser/deserialiser json ce qui n’est pas l’objectif de la formation.

Étape 7 : Mutation

A) ET SI ON INSÉRAIT DES DONNÉES ?

Pour cela implémentez l’interface MutationResolver dans une classe appelée FishMutationResolver.

package com.keywer.masterclass.spring.graphql.graphql;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.keywer.masterclass.spring.graphql.model.Family;
import com.keywer.masterclass.spring.graphql.model.WaterType;
import com.keywer.masterclass.spring.graphql.repository.FamilyRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class FishMutationResolver implements GraphQLMutationResolver {

    private final FamilyRepository familyRepository;

    @Autowired
    public FishMutationResolver(FamilyRepository familyRepository) {
        this.familyRepository = familyRepository;
    }

    @Transactional
    public Family createFamily(String name, WaterType waterType) {
        return this.familyRepository.save(Family.builder().name(name).waterType(waterType).build());
    }
}

Ajoutez dans le schéma une nouvelle section Mutation :

type Mutation {
  createFamily(name: String, waterType: WaterType): Family
}
package com.keywer.masterclass.spring.graphql.graphql;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.keywer.masterclass.spring.graphql.model.Family;
import com.keywer.masterclass.spring.graphql.model.WaterType;
import com.keywer.masterclass.spring.graphql.repository.FamilyRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class FishMutationResolver implements GraphQLMutationResolver {

    private final FamilyRepository familyRepository;

    @Autowired
    public FishMutationResolver(FamilyRepository familyRepository) {
        this.familyRepository = familyRepository;
    }

    @Transactional
    public Family createFamily(String name, WaterType waterType) {
        return this.familyRepository.save(Family.builder().name(name).waterType(waterType).build());
    }
}

Rien de bien différent de la query, voici l’appel coté client :

mutation{
 createFamily(name: "Galeaspida",waterType: SEA){ name }
}

B) AJOUTEZ UNE FONCTIONNALITÉ DE MUTATION PERMETTANT D’AJOUTER UN POISSON PRENANT UN NOM DE FAMILLE. A CELA JETEZ UNE ERREUR DE TYPE NOSUCHELEMENTEXCEPTION LORSQU’AUCUNE FAMILLE N’EST TROUVÉE.

createFish(name: String, temperature: Int, price : Float, familyName: String): Fish

Voici un exemple de retour d’erreur :

{
  "errors": [
    {
      "message": "Impossible to find Family azert"
    }
  ],
  "data": {
    "createFish": null
  }
}

A noter qu’en cas d’erreur, GraphQL renvoie tout de même les attributs qu’il a réussi à récupérer, tout en énumérant les erreurs qu’il a rencontré.

ÉTAPE 8 : SUBSCRIPTIONS

GraphQL propose un système d’abonnement. Je souhaite implémenter un système pouvant m’alerter lorsqu’une nouvelle famille de poisson est ajoutée.

Nous allons utiliser les EventPublisher Spring :

package com.keywer.fmasterclass.spring.graphql.event;

import com.keywer.masterclass.spring.graphql.model.Family;
import org.springframework.context.ApplicationEvent;

public class FamilyCreationEvent extends ApplicationEvent {
    private Family family;

    public FamilyCreationEvent(Object source, Family family) {
        super(source);
        this.family = family;
    }
    public Family getFamily() {
        return family;
    }
}
private ApplicationEventPublisher applicationEventPublisher;
  private final FamilyRepository familyRepository;
  private final FishRepository fishRepository;

  @Autowired
  public FishMutationResolver(ApplicationEventPublisher applicationEventPublisher,
                              FamilyRepository familyRepository,
                              FishRepository fishRepository) {
      this.applicationEventPublisher = applicationEventPublisher;
      this.familyRepository = familyRepository;
      this.fishRepository = fishRepository;
  }

  @Transactional
  public Family createFamily(String name, WaterType waterType) {
      Family family = Family.builder().name(name).waterType(waterType).build();
      FamilyCreationEvent customSpringEvent = new FamilyCreationEvent(this, family);
      applicationEventPublisher.publishEvent(customSpringEvent);
      return this.familyRepository.save(family);
  }

Nous allons ensuite créer un publicateur d’événements sous forme de flowable :

package com.keywer.masterclass.spring.graphql.service;

import com.keywer.masterclass.spring.graphql.event.FamilyCreationEvent;
import com.keywer.masterclass.spricng.graphql.model.Family;
import com.keywer.masterclass.spring.graphql.model.Fish;
import com.keywer.masterclass.spring.graphql.model.WaterType;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.observables.ConnectableObservable;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import java.util.Queue;
import java.util.concurrent.*;

@Component
public class FamilyPublisher {

    private Flowable<Family> publisher;

    private Queue<Family> events= new ArrayBlockingQueue<>(30);

    @Autowired
    public FamilyPublisher() {
        this.initSubscriber();
    }

    private void initSubscriber() {
        Observable<Family> familyObservable = Observable.create(observableEmitter -> {
                    ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
                    executorService.scheduleAtFixedRate(newFamilyTick(observableEmitter), 0, 2, TimeUnit.SECONDS);
                }
        );
        ConnectableObservable<Family> connectableObservable = familyObservable.share().publish();
        connectableObservable.connect();

        this.publisher = connectableObservable.toFlowable(BackpressureStrategy.BUFFER);
    }

    private Runnable newFamilyTick(ObservableEmitter<Family> observableEmitter) {
        return () -> {
            while(!events.isEmpty()) {
                observableEmitter.onNext(events.poll());
            }
        };
    }

    @EventListener
    public void handleSuccessful(FamilyCreationEvent event) {
        events.add(event.getFamily());
    }

    public Publisher<Family> getPublisher() {
        return publisher;
    }
}

Coté GraphQL commençons par mettre à jour le schéma :

type Subscription {
	lastFamily: Family!
}

Puis comme pour les query/mutation implémenter l’interface SubscriptionResolver :

package com.keywer.masterclass.spring.graphql.graphql;

import com.coxautodev.graphql.tools.GraphQLSubscriptionResolver;
import com.keywer.masterclass.spring.graphql.model.Family;
import com.keywer.masterclass.spring.graphql.service.FamilyPublisher;
import org.reactivestreams.Publisher;t
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class FishSubscriptionResolver implements GraphQLSubscriptionResolver {

    private final FamilyPublisher familyPublisher;

    @Autowired
    public FishSubscriptionResolver(FamilyPublisher familyPublisher) {
        this.familyPublisher = familyPublisher;
    }

    public Publisher<Family> lastFamily(){
        return familyPublisher.getPublisher();
    }
}

Pour vérifier son fonctionnement nous allons implémenter un client Javascript à placer dans le répertoire resources/public/index.html.

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Subscriptions over Web Sockets</title>
    <style>
        body {
            font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
            font-weight: 300;
        }

        .networking {
            display: none;
        }

        .stockTicker {
            border: solid black 1px;
            margin: 2px;
            min-height: 400px
        }

        .stockWrapper {
            display: block;
            padding: 20px;
            font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
            font-weight: 300;
        }

        .stockSymbol {
            font-weight: 600;
        }

        .stockPrice {
            font-weight: 600;
            color: red;
        }

        .stockUp {
            font-weight: 600;
            color: green;
        }
    </style>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

    <script>

        function networkBlip() {

            var $networking = $('.networking');
            if (!$networking.is(":visible")) {
                $networking.show(100, function () {
                    var $that = $(this);
                    setTimeout(function () {
                        $that.hide();
                    }, 500)
                });
            }
        }

        function subscribeToStocks() {
            var exampleSocket = new WebSocket("ws://localhost:8080/subscriptions");
            networkBlip();

            exampleSocket.onopen = function () {
                networkBlip();
                console.log("web socket opened");

                var query = 'subscription LastFamilySubscription \{ \n' +
                    '    lastFamily {' +
                    '       id ' +
                    '       name ' +
                    '     }' +
                    '}';
                var graphqlMsg = {
                    query: query,
                    variables: {}
                };
                exampleSocket.send(JSON.stringify(graphqlMsg));
            };

            var STOCK_CODES_UPDATES = {};

            exampleSocket.onmessage = function (event) {
                networkBlip();

                var data = JSON.parse(event.data);
                console.log(data);
            };
        }

        window.addEventListener("load", subscribeToStocks);
    </script>
</head>
<body>

<div class="jumbotron text-center">
    <h2>graphql-java Subscriptions</h2>
    <p>An example of graphql-java subscriptions sending continuous updates over websockets</p>
</div>


<div class="container">
    <div class="row">
        <div class="col-sm-1">
            <img src="https://avatars1.githubusercontent.com/u/14289921?s=200&v=4"
                 class="img-thumbnail" alt="graphql-java" width="400" height="400">
        </div>
        <div class="col-sm-3">
            <h3>Explanation</h3>
            <p>This demonstrates the use of graphql subscriptions and web sockets to send a stream of imagined stock price
                updates to this page.</p>
            <p>The updates are continuously sent from a server side publish and subscribe system (RxJava in this case) and
                pushed
                down to the browser client while applying graphql shapes to the subscription data</p>
            <p>The graphql query used in this example is :</p>
            <pre>

            </pre>
        </div>
        <div class="col-sm-8">
            <h3>Stock Price Updates</h3>
            <div class="stockTicker">Pending subscription...</div>
            <div class="networking">📡</div>
        </div>
    </div>
</div>
</body>
</html>

La connexion à l’adresse localhost:8080/index.html initialisera le client. Lancez la console de développement pour y voir plus clair. Il s’agit d’une communication web-socket standard avec initialisation de la conversation avec une requête http 101. En parallèle lancez GraphiQL et ajoutez une famille. 

Étape 9 : Les outils

Kick start facilite l’intégration d’outils de l’écosystème GraphQL.

ALTAIR ET PLAYGROUND

Il s’agit de sur-couche à GraphiQL, ils ajoutent un certain nombre de fonctionnalités telles que la gestion des onglets, une meilleure intégration des subscriptions ou encore une personnalisation de l’interface. Ces outils sont également disponibles en client lourd.

VOYAGER

Et pour finir un outil de consultation du Graph.