ARTICLES
Faire une API REST en Java avec Jersey et Grizzly
By Valentin Bremond
Hein ?
Le but est de mettre en place le plus simplement possible (comprendre : sans utiliser d’usine à gaz) une petite API REST en Java.
Oui bon ça va, je sais ce que vous allez me dire : le Java, de base, c’est une usine à gaz. C’est vrai, mais c’est aussi un language très propre (comment ça c’est mon point de vue ?) et surtout très performant (oui c’est à vous que je parle les fans de Python).
Bref, trève de trolls, passons au vif du sujet. Pour faire notre API, on va utiliser plusieurs choses :
- Java 8
- Jersey pour l’implémentation de JAX-RS (ce qui nous permet de faire du REST facile, du genre Flask en Python)
- Jackson pour la sérialisation / désérialisation JSON
- Log4j pour pouvoir récupérer tous les logs sur STDOUT / STDERR (incroyable de devoir utiliser une lib pour pouvoir faire ça…)
- JUnit pour les tests (oui, bon, moi j’en ai pas fait, mais il faut en faire !)
- Grizzly comme serveur Web embarqué (plus besoin de Tomcat !)
- Maven (pour les dépendances et la génération du JAR)
- la ligne de commande (au moins on comprend ce qu’on fait, pas comme quand on clique partout dans Eclipse…)
En avant Guingamp
Je considère que vous avez déjà installé Java 8, Maven et que vous avez un terminal sous la main.
Créer le projet Maven
1mvn archetype:generate -DgroupId=io.bremond.api -DartifactId=api -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
2cd apiConfigurer Maven
Éditer le fichier pom.xml :
1<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
2 <modelVersion>4.0.0</modelVersion>
3 <groupId>io.bremond.api</groupId>
4 <artifactId>api</artifactId>
5
6 <packaging>jar</packaging>
7 <version>1.0.0</version>
8
9 <name>${project.name}</name>
10
11
12 <!-- On créé des variables au lieu de hardcoder les trucs comme des cochons -->
13 <properties>
14 <project.name>api</project.name>
15 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
16 <maven.compiler.version>3.5.1</maven.compiler.version>
17 <maven.dependency.version>2.10</maven.dependency.version>
18
19 <java.target>1.8</java.target>
20
21 <jersey.version>2.22.2</jersey.version>
22 <jackson.version>2.8.2</jackson.version>
23 <log4j.version>2.6.2</log4j.version>
24 <junit.version>4.12</junit.version>
25 </properties>
26
27
28 <!-- On configure le build Maven pour qu'il nous fasse des JAR complets (avec les dépendances et le manifest), prêts à lancer -->
29 <build>
30 <plugins>
31 <plugin>
32 <groupId>org.apache.maven.plugins</groupId>
33 <artifactId>maven-compiler-plugin</artifactId>
34 <version>${maven.compiler.version}</version>
35 <inherited>true</inherited>
36 <configuration>
37 <source>${java.target}</source>
38 <target>${java.target}</target>
39 </configuration>
40 </plugin>
41 <plugin>
42 <groupId>org.apache.maven.plugins</groupId>
43 <artifactId>maven-assembly-plugin</artifactId>
44 <configuration>
45 <descriptorRefs>
46 <descriptorRef>jar-with-dependencies</descriptorRef>
47 </descriptorRefs>
48 <finalName>${project.name}-${project.version}</finalName>
49 <archive>
50 <manifest>
51 <mainClass>io.bremond.api.Main</mainClass>
52 </manifest>
53 </archive>
54 </configuration>
55 <executions>
56 <execution>
57 <phase>package</phase>
58 <goals>
59 <goal>single</goal>
60 </goals>
61 </execution>
62 </executions>
63 </plugin>
64 <plugin>
65 <groupId>org.apache.maven.plugins</groupId>
66 <artifactId>maven-dependency-plugin</artifactId>
67 <version>${maven.dependency.version}</version>
68 <executions>
69 <execution>
70 <id>copy-dependencies</id>
71 <phase>package</phase>
72 <goals><goal>copy-dependencies</goal></goals>
73 </execution>
74 </executions>
75 </plugin>
76 </plugins>
77 </build>
78
79
80 <!-- Enfin, on met en place les dépendances -->
81 <dependencies>
82 <!-- Jersey -->
83 <dependency>
84 <groupId>org.glassfish.jersey.containers</groupId>
85 <artifactId>jersey-container-grizzly2-http</artifactId>
86 <version>${jersey.version}</version>
87 </dependency>
88 <dependency>
89 <groupId>org.glassfish.jersey.media</groupId>
90 <artifactId>jersey-media-json-jackson</artifactId>
91 <version>${jersey.version}</version>
92 </dependency>
93
94 <!-- JUnit -->
95 <dependency>
96 <groupId>junit</groupId>
97 <artifactId>junit</artifactId>
98 <version>${junit.version}</version>
99 <scope>test</scope>
100 </dependency>
101
102 <!-- Log4j (on va s'en servir pour envoyer tous les logs sur STDOUT/STDERR) -->
103 <dependency>
104 <groupId>org.apache.logging.log4j</groupId>
105 <artifactId>log4j-api</artifactId>
106 <version>${log4j.version}</version>
107 </dependency>
108 <dependency>
109 <groupId>org.apache.logging.log4j</groupId>
110 <artifactId>log4j-core</artifactId>
111 <version>${log4j.version}</version>
112 </dependency>
113
114
115 <!-- Jackson -->
116 <dependency>
117 <groupId>com.fasterxml.jackson.core</groupId>
118 <artifactId>jackson-databind</artifactId>
119 <version>${jackson.version}</version>
120 </dependency>
121 <dependency>
122 <groupId>com.fasterxml.jackson.core</groupId>
123 <artifactId>jackson-annotations</artifactId>
124 <version>${jackson.version}</version>
125 </dependency>
126 <dependency>
127 <groupId>com.fasterxml.jackson.core</groupId>
128 <artifactId>jackson-core</artifactId>
129 <version>${jackson.version}</version>
130 </dependency>
131 </dependencies>
132</project>Oui, bon là OK : c’est un sacré merdier. Mais bon c’est le moyen le plus simple de gérer le bordel des libs Java et du packaging (ça ou Gradle - que je n’ai jamais utilisé par ailleurs).
Écrire la classe qui va bootstrapper l’API
On va créer une classe Main qui va configurer puis lancer le serveur Grizzly (c’est la classe définie dans le POM sous le tag mainClass) :
src/main/java/io/bremond/api/Main.java :
1package io.bremond.api;
2
3import java.io.IOException;
4import java.net.URI;
5import java.util.TimeZone;
6
7import org.glassfish.grizzly.http.server.HttpServer;
8import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
9import org.glassfish.jersey.server.ResourceConfig;
10import org.glassfish.jersey.jackson.JacksonFeature;
11
12
13public class Main
14{
15 // Configuration de l'URL et du port de Grizzly
16 public static final String BASE_URI = "http://localhost:8080/";
17
18 public static HttpServer startServer()
19 {
20 final ResourceConfig rc = new ResourceConfig().packages( "io.bremond.api" );
21
22 rc.register( JacksonFeature.class );
23
24 return GrizzlyHttpServerFactory.createHttpServer( URI.create( BASE_URI ), rc );
25 }
26
27 public static void main( String[] args ) throws IOException
28 {
29 // On fait en sorte de dire à la JVM d'utiliser UTC par défaut (pas utile ici mais ça pourrait être très utile par la suite)
30 TimeZone.setDefault( TimeZone.getTimeZone( "UTC" ) );
31
32 final HttpServer server = startServer();
33
34 // On attend qu'un CTRL-C termine l'application
35 while( true )
36 {
37 System.in.read();
38 }
39 }
40}Écrire nos routes
Une fois qu’on a ça, il ne nous reste plus qu’à écrire les routes (on créé d’abord le dossier routes) :
1mkdir src/main/java/io/bremond/api/routesLe fichier src/main/java/io/bremond/api/routes/Hello.java :
1package io.bremond.api.routes;
2
3import java.util.Map;
4import java.util.HashMap;
5
6import javax.ws.rs.GET;
7import javax.ws.rs.Path;
8import javax.ws.rs.PathParam;
9import javax.ws.rs.Produces;
10import javax.ws.rs.core.MediaType;
11import javax.ws.rs.core.Response;
12
13
14@Path( "/hello" )
15public class Hello
16{
17 public Hello() throws Exception
18 {}
19
20
21 @GET
22 @Produces( { MediaType.APPLICATION_JSON } )
23 public Response hello() throws Exception
24 {
25 Map<String,String> m = new HashMap<String,String>( 1 );
26 m.put( "message", "Coucou !" );
27
28 return Response.ok( m ).build();
29 }
30
31
32 @GET
33 @Path( "/{id}" )
34 @Produces( { MediaType.APPLICATION_JSON } )
35 public Response get( @PathParam( "id" ) long id ) throws Exception
36 {
37 Map<String,Object> m = new HashMap<String,Object>( 2 );
38 m.put( "message", "Coucou numéro" );
39 m.put( "id", id );
40
41 return Response.ok( m ).build();
42 }
43}L’annotation @Path définit la route liée avec la classe annotée. @GET vous avez compris ; par contre si vous mettez un autre @Path sur une méthode de la classe, les 2 s’ajoutent (dans notre cas, un GET sur /hello appellera la méthode hello() tandis qu’un GET sur /hello/12 appellera la méthode get()).
Configurer Log4j pour qu’il forward tous les logs sur STDOUT/STDERR
Cette étape est très utile si votre API va tourner dans un container (ou même si elle ne tourne pas dans un container mais est gérée par systemd et peut envoyer ses logs dans journald - pas besoin donc de s’emmerder la vie avec des fichiers en vrac et un logrotate).
On créé le dossier :
1mkdir src/main/resourcesDedans on met le fichier src/main/resources/log4j2.xml :
1<?xml version="1.0" encoding="UTF-8"?>
2<Configuration>
3 <Properties>
4 <Property name="PATTERN">%-5level %d [%t] %c:%M(%L): %m%n</Property>
5 </Properties>
6
7 <Appenders>
8 <Console name="STDOUT" target="SYSTEM_OUT">
9 <PatternLayout pattern="${PATTERN}"/>
10 <Filters>
11 <!-- Dans le filtre STDOUT, on interdit les messages "warn", "error" et "fatal" -->
12 <ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>
13 <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
14 <ThresholdFilter level="fatal" onMatch="DENY" onMismatch="NEUTRAL"/>
15
16 <!-- Mais on accepte tous les autres ("trace", "debug", "info") -->
17 <ThresholdFilter level="all" onMatch="ACCEPT" onMismatch="DENY"/>
18 </Filters>
19 </Console>
20 <Console name="STDERR" target="SYSTEM_ERR">
21 <PatternLayout pattern="${PATTERN}"/>
22 </Console>
23 </Appenders>
24
25 <Loggers>
26 <Root level="all">
27 <AppenderRef ref="STDOUT" level="all"/>
28 <AppenderRef ref="STDERR" level="warn"/>
29 </Root>
30 </Loggers>
31</Configuration>Builder le JAR
On va clean les éventuels builds (là normalement y’en a pas encore) et tout reconstruire :
1mvn clean packageLe JAR généré va se trouver à target/api-1.0.0-jar-with-dependencies.jar. Le numéro de version et le début du nom de fichier (ici, “api”) correspondent respectivement aux valeurs “version” et “name” dans le POM.
Tester
On lance le JAR :
1java -jar target/api-1.0.0-jar-with-dependencies.jarPuis on peut aller curler les URLs http://localhost:8080/hello et http://localhost:8080/hello/30, on retrouve bien nos JSON.
Conclusion
Voilà, vous voyez, c’est pas si bordélique que ça au final ! Vous avez plus qu’à mettre un petit ORM (ou pas hein) derrière ça et vous avez un truc qui marche bien, qui bombarde (on dira ce qu’on voudra, la JVM ça tabasse bien quand même) et qui est portable (juste un JAR pour toute votre API).
Liens
La doc de Jersey : https://jersey.java.net/documentation/latest/index.html
La doc de Maven : celle de 5 minutes, celle de 30 minutes
Un cookie, parce que vous avez été gentil : au chocolat en plus !