I. Introduction▲
Créer une API en Java est un sujet déjà largement traité. Si vous utilisez Jersey et ses annotations, vous savez que c’est très simple et rapide à mettre en place. Dans cet article, je voudrais simplement vous montrer ce qu’on peut faire avec Vert.x en revenant aux fondamentaux. Et en vérité, la création d’une API sera avant tout un prétexte.
II. Domaine▲
Si vous lisez régulièrement mes articles, vous savez qu’ils parlent tous de chiens. C’est un domaine simple qui ne nécessite pas de longues explications. Le bean Dog suivant sera suffisant pour cette chronique.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
public
class
Dog {
private
final
String id;
private
final
String name;
private
final
String race;
private
final
int
age;
// Constructeur
public
Dog
(
final
String id, final
String name, final
String race, final
int
age) {
super
(
);
this
.id =
id;
this
.name =
name;
this
.race =
race;
this
.age =
age;
}
//
// + getters
Je vous propose également DogService qui fera office de service, avec les opérations classiques. Un code complet naïf est donné en annexes.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
public
class
DogService {
// Charcher la liste des chiens
public
List<
Dog>
findAll
(
) {
...
}
// Chercher un seul chien
public
Dog findById
(
final
String id) {
...
}
// Mettre à jour un chien
public
Dog update
(
final
Dog dog) {
...
}
// Effacer un chien
public
void
remove
(
final
String id) {
...
}
// Ajouter un chien
public
Dog add
(
final
Dog dog) {
...
}
Et finalement, je vous propose d’initialiser une liste d’exemples composée des chiens célèbres.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
public
class
DogService {
private
final
Map<
String, Dog>
dogs =
new
HashMap<
String, Dog>(
);
// Constructeur
public
DogService
(
) {
super
(
);
initDogs
(
);
}
// Initialisation des chiens fictifs
private
void
initDogs
(
) {
// Création de chiens fictifs
final
Dog lassie =
new
Dog
(
"12345fh"
, "Lassie"
, "colley"
, 12
);
final
Dog milou =
new
Dog
(
"e7654"
, "Milou"
, "fox-terrier"
, 11
);
final
Dog scoobydoo =
new
Dog
(
"s93d78"
, "Scooby-Doo"
, "Danois"
, 7
);
final
Dog idefix =
new
Dog
(
"6222mk9p"
, "Idefix"
, "Bichon"
, 17
);
// Ajouts dans la map
dogs.put
(
lassie.getId
(
), lassie);
dogs.put
(
milou.getId
(
), milou);
dogs.put
(
scoobydoo.getId
(
), scoobydoo);
dogs.put
(
idefix.getId
(
), idefix);
}
III. Créer un serveur▲
III-A. Installation▲
Comme souvent, le plus simple va être de passer par Maven, Gradle, etc. Dans cet article, nous allons utiliser Maven, mais sentez-vous libre d’utiliser votre système préféré.
Nous allons donc ajouter une dépendance à Vert.x dans le fichier pom.xml. Vert.x est modulaire. Nous pouvons donc indiquer précisément les dépendances qui nous intéressent, à commencer par vertx-core. Et puisque nous allons développer pour le web, nous ajouterons également vertx-web, le module dédié.
2.
3.
4.
5.
6.
7.
8.
9.
10.
<dependency>
<groupId>
io.vertx</groupId>
<artifactId>
vertx-core</artifactId>
<version>
${vertx.version}</version>
</dependency>
<dependency>
<groupId>
io.vertx</groupId>
<artifactId>
vertx-web</artifactId>
<version>
${vertx.version}</version>
</dependency>
III-B. Le verticle en charge de l'API▲
Contrairement à ce que vous verrez dans d’autres tutoriels, je préfère séparer mes verticles de mon main. Je vous encourage à faire de même dans vos applications. On va donc créer MyApiVerticle. Pour cela, il suffit d’hériter de AbstractVerticle et de réécrire les methodes start() et stop(). Demandez à votre IDE de le faire pour vous.
2.
3.
4.
5.
6.
7.
8.
9.
10.
import
io.vertx.core.AbstractVerticle;
public
class
MyApiVerticle extends
AbstractVerticle {
// Quand le verticle se lance
@Override
public
void
start
(
) throws
Exception {
}
// Quand le verticle s'arrête
@Override
public
void
stop
(
) throws
Exception {
}
On va en profiter pour ajouter des logs. Vert.x fournit ses propres classes, mais rien ne vous empêche d’utiliser une autre bibliothèque, comme Flogger dont je parlais dans un précédent article, sobrement intitulé « Tutoriel pour logguer facilement, en Java à l'aide de Flogger, en 5 minutes ».
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
import
io.vertx.core.logging.Logger;
import
io.vertx.core.logging.LoggerFactory;
public
class
MyApiVerticle extends
AbstractVerticle {
private
static
final
Logger LOGGER =
LoggerFactory.getLogger
(
MyApiVerticle.class
);
// Quand le verticle se lance
@Override
public
void
start
(
) throws
Exception {
LOGGER.info
(
"Dans le start..."
);
}
// Quand le verticle s'arrête
@Override
public
void
stop
(
) throws
Exception {
LOGGER.info
(
"Dans le stop..."
);
}
Il ne reste plus qu’à lancer le verticle depuis le main, de la même façon que dans mes autres articles dédiés à Vert.x.
2.
3.
4.
5.
6.
7.
import
io.vertx.core.Vertx;
public
class
App {
public
static
void
main
(
String[] args) {
System.out.println
(
"App..."
);
final
Vertx vertx =
Vertx.vertx
(
);
vertx.deployVerticle
(
new
MyApiVerticle
(
));
}
Il faut ensuite exécuter le programme et regarder les logs dans la console.
2.
3.
App...
sept. 1
, 2019
10
:10
:49
AM com.ice.articlevertxapi.MyApiVerticle
INFOS: Dans le start...
III-C. Ecouter un port▲
On y est presque. Avant d’entrer dans le vif du sujet, je vous propose d’écrire un simple Hello World accessible depuis le navigateur. Il faut simplement créer un serveur, depuis la variable vertx fournie par AbstractVerticle. Dans le cas du Hello World, la lambda fournie au RequestHandler est très simple. Enfin, je vous propose d’écouter sur le port 8080.
2.
3.
4.
5.
6.
7.
@Override
public
void
start
(
) throws
Exception {
LOGGER.info
(
"Dans le start..."
);
vertx.createHttpServer
(
)
.requestHandler
(
routingContext ->
routingContext.response
(
).end
(
"Hello World!"
))
.listen
(
8080
);
}
Il ne reste plus à rechercher l'adresse http://localhost:8080/ dans votre navigateur.
IV. Notre API naïve▲
IV-A. Charger une liste d'objets▲
On va maintenant écrire notre API. Dans un premier temps, commençons par l’opération qui me semble la plus simple : charger la liste avec tous les chiens.
Pour cela, je vous propose de définir la route « /api/v1/dogs » qui est composée de « api » pour préciser qu’on est sur l’URL de notre API, de « v1 » pour dire qu’on est en version 1, et de « dogs » au pluriel pour indiquer qu’on veut la liste des chiens.
Le code associé est relativement simple.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
@Override
public
void
start
(
) throws
Exception {
LOGGER.info
(
"Dans le start..."
);
// Création du routeur
Router router =
Router.router
(
vertx);
// Définition de la route
router.get
(
"/api/v1/dogs"
)
.handler
(
this
::getAllDogs);
// Lancement du serveur
vertx.createHttpServer
(
)
.requestHandler
(
router)
.listen
(
8080
);
Note : les tutoriels un peu plus vieux proposaient de faire appel à router::accept comme RequestHandler. Mais la méthode accept est désormais dépréciée. Dans les nouvelles versions, on peut passer directement le router, ce qui est plus simple.
A ce stade, le plus simple est de définir le code propre au traitement de la liste dans la méthode séparée getAllDogs, pour que ça reste lisible.
2.
3.
private
void
getAllDogs
(
RoutingContext routingContext) {
LOGGER.info
(
"Dans getAllDogs..."
);
}
J’aimerais attirer votre attention sur le fait que la méthode getAllDogs renvoie void et non la liste de chiens. Cela peut vous déstabiliser en première approche, mais n’oubliez pas qu’on développe une API REST asynchrone. C’est Vert.x qui se chargera d’envoyer la réponse au client.
On peut relancer le programme se demander l’URL « http://localhost:8080/api/v1/dogs » depuis le navigateur. Celui-ci va tourner en boucle, puisqu’on n’a pas encore programmé l’envoi de la liste de chiens, mais on peut vérifier que la log s’affiche dans la console, signe que le routage a bien fonctionné.
App...
sept. 1
, 2019
10
:10
:19
AM com.ice.articlevertxapi.MyApiVerticle
INFOS
:
Dans le start...
sept. 1
, 2019
10
:10
:33
AM com.ice.articlevertxapi.MyApiVerticle
INFOS
:
Dans getAllDogs...
Avant d’aller plus loin, je vous propose de compléter le code afin d’avoir naïvement accès au service de chiens présenté plus haut. Ici, je vais simplement utiliser l'instruction new, mais il est bien entendu possible de faire de l'injection. Pensez juste à choisir une bibliothèque rapide.
2.
3.
4.
5.
6.
7.
public
class
MyApiVerticle extends
AbstractVerticle {
private
final
DogService dogService =
new
DogService
(
);
...
private
void
getAllDogs
(
RoutingContext routingContext) {
LOGGER.info
(
"Dans getAllDogs..."
);
final
List<
Dog>
dogs =
dogService.findAll
(
);
}
On souhaite répondre au format JSON. On va donc créer l’objet correspondant et on y placera la liste de chiens. Et bien entendu, on peut ajouter autant d’attributs qu’on le souhaite.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
import
io.vertx.core.json.JsonObject;
...
private
void
getAllDogs
(
RoutingContext routingContext) {
LOGGER.info
(
"Dans getAllDogs..."
);
// Recherche des chiens
final
List<
Dog>
dogs =
dogService.findAll
(
);
// Création et remplissage de la réponse
final
JsonObject jsonResponse =
new
JsonObject
(
);
jsonResponse.put
(
"dogs"
, dogs);
jsonResponse.put
(
"my-name"
, "Thierry"
);
}
Et finalement, on peut demander à Vert.x d’envoyer la réponse. Ici, je précise le statut 200 pour indiquer que tout s’est bien déroulé. En cas d’erreur, j’aurais indiqué un statut 500 par exemple. J’ajoute aussi un entête (header) pour préciser le content-type et indiquer qu’on renvoie du JSON. Et bien entendu, j’envoie la réponse.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
import
io.vertx.core.json.Json;
...
private
void
getAllDogs
(
RoutingContext routingContext) {
LOGGER.info
(
"Dans getAllDogs..."
);
// Recherche des chiens
final
List<
Dog>
dogs =
dogService.findAll
(
);
// Création et remplissage de la réponse
final
JsonObject jsonResponse =
new
JsonObject
(
);
jsonResponse.put
(
"dogs"
, dogs);
jsonResponse.put
(
"my-name"
, "Thierry"
);
// Envoi de la réponse
routingContext.response
(
)
.setStatusCode
(
200
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
Json.encodePrettily
(
jsonResponse));
}
Vous pouvez rafraîchir votre navigateur sur l’adresse « http://localhost:8080/api/v1/dogs » et constater que vous recevez bien la liste des chiens au format JSON.
Note : Dans l’exemple, l’utilisation de encodePrettily est facultative. Je l’utilise uniquement pour que la réponse soit facile à lire. La plupart des clients proposent également cette facilité.
Pour la suite, je vous propose d’utiliser le logiciel Postman à la place du navigateur. Cela nous donnera plus de possibilités, en particulier pour les autres opérations CRUD.
Lancez donc une requête GET sur l'adresse « http://localhost:8080/api/v1/dogs » depuis Postman.
Vous constatez que la barre d'onglets vous permet de formater la réponse pour qu'elle soit lisible. Il n'est donc plus nécessaire de faire appel à encodePrettily.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
private
void
getAllDogs
(
RoutingContext routingContext) {
LOGGER.info
(
"Dans getAllDogs..."
);
// Recherche des chiens
final
List<
Dog>
dogs =
dogService.findAll
(
);
// Création et remplissage de la réponse
final
JsonObject jsonResponse =
new
JsonObject
(
);
jsonResponse.put
(
"dogs"
, dogs);
jsonResponse.put
(
"my-name"
, "Thierry"
);
// Envoi de la réponse
routingContext.response
(
)
.setStatusCode
(
200
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
Json.encode
(
jsonResponse));
}
L'affichage en RAW n'est pas très lisible.
Contrairement à l'affichage « pretty ».
Mais je préfère que ce soit le client qui réalise la mise en forme, s'il en a vraiment besoin. Ce sera toujours quelques nanosecondes de gagnées sur le serveur et durant le transfert.
IV-B. Charger un seul objet▲
L’opération CRUD qu’on va naturellement développer ensuite est celle qui consiste à charger un chien, en particulier grâce à son id. Disons qu’on veuille charger les informations de Milou. Dans ce cas, il faudra appeler l’URL « http://localhost:8080/api/v1/dogs/e7654 » dans laquelle dogs est toujours écrit au pluriel, et où « e7654 » est l'id de Milou.
La syntaxe pour préciser un paramètre dans le routage de l’URL est :param
2.
3.
4.
5.
6.
7.
8.
@Override
public
void
start
(
) throws
Exception {
LOGGER.info
(
"Dans le start..."
);
// Création du routeur
Router router =
Router.router
(
vertx);
// Définition des routes
router.get
(
"/api/v1/dogs"
).handler
(
this
::getAllDogs);
router.get
(
"/api/v1/dogs/:id"
).handler
(
this
::getOneDog);
Il faudra bien entendu récupérer cet id depuis la requête dans la méthode.
2.
3.
private
void
getOneDog
(
RoutingContext routingContext) {
LOGGER.info
(
"Dans getOneDog..."
);
final
String id =
routingContext.request
(
).getParam
(
"id"
);
La suite est relativement simple. Il faut récupérer le chien à l’aide du service et l’envoyer au client.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
private
void
getOneDog
(
RoutingContext routingContext) {
LOGGER.info
(
"Dans getOneDog..."
);
// Param id de la requête
final
String id =
routingContext.request
(
).getParam
(
"id"
);
// Recherche du chien correspondant à l'id
final
Dog dog =
dogService.findById
(
id);
// Envoi de la réponse
routingContext.response
(
)
.setStatusCode
(
200
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
Json.encode
(
dog));
}
Note : Ici je n’utilise pas d’objet JSON intermédiaire, car je souhaite renvoyer uniquement le chien, sans information complémentaire.
Il ne reste plus qu'à tester l'adresse http://localhost:8080/api/v1/dogs/e7654 dans Postman.
IV-C. Envoyer une erreur▲
La gestion d’erreur devrait monopoliser une grande partie de votre temps. Dans ce tutoriel, on va se concentrer sur le cas simple, correspondant aux identifiants non trouvés, mais on pourrait parler de nombreux types d’erreurs : sécurité, restrictions, problème de connexion aux ressources (comme la base de données), bugs, etc.
Dans l'exemple, on pourrait renvoyer une erreur si le chien n'est pas trouvé.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
private
void
getOneDog
(
RoutingContext routingContext) {
LOGGER.info
(
"Dans getOneDog..."
);
// Param id de la requête
final
String id =
routingContext.request
(
).getParam
(
"id"
);
// Recherche du chien correspondant à l'id
final
Dog dog =
dogService.findById
(
id);
// Gestion d'erreur si le chien n'est pas trouvé...
if
(
dog ==
null
) {
// TODO chien non trouvé...
}
// Envoi de la réponse
routingContext.response
(
)
.setStatusCode
(
200
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
Json.encode
(
dog));
}
Il suffit de créer un message JSON contenant les informations que vous jugerez utiles et de préciser un statut 404 indiquant que la ressource n’a pas été trouvée.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
if
(
dog ==
null
) {
final
JsonObject errorJsonResponse =
new
JsonObject
(
);
errorJsonResponse.put
(
"error"
, "No dog can be found for the specified id:"
+
id);
errorJsonResponse.put
(
"id"
, id);
// Envoi de la réponse avec erreur 404
routingContext.response
(
)
.setStatusCode
(
404
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
Json.encode
(
errorJsonResponse));
return
;
}
Pour pouvez tester que vous recevez bien un message d’erreur quand vous appelez l’adresse http://localhost:8080/api/v1/dogs/abc123 depuis Postman, sachant qu’aucun chien n’a l’id « abc123 ». Vous pouvez vérifier (sur la droite dans l'interface de Postman) que le statut est bien 404 Not Found.
IV-D. Créer un objet▲
Habituellement, on utilise le verbe POST pour créer un nouvel objet, ce qui est très simple à configurer dans le router.
2.
3.
4.
5.
6.
7.
@Override
public
void
start
(
) throws
Exception {
...
// Définition des routes
router.get
(
"/api/v1/dogs"
).handler
(
this
::getAllDogs);
router.get
(
"/api/v1/dogs/:id"
).handler
(
this
::getOneDog);
router.post
(
"/api/v1/dogs"
).handler
(
this
::createOneDog);
Il faut ensuite récupérer les informations transmises dans la requête. Pour charger un objet par son id en GET, on avait récupéré la valeur de l’id dans l’URL. En POST, on trouvera les informations dans le corps (body). Je peux ensuite créer un nouveau chien à partir de ces informations et demander au service de l'ajouter à la liste.
2.
3.
4.
5.
6.
private
void
createOneDog
(
RoutingContext routingContext) {
LOGGER.info
(
"Dans createOneDog..."
);
final
JsonObject body =
routingContext.getBodyAsJson
(
);
final
String name =
body.getString
(
"name"
);
final
String race =
body.getString
(
"race"
);
final
Integer age =
body.getInteger
(
"age"
);
Pour pouvoir récupérer le corps (body), on doit spécifier un BodyHandler sur les routes "/api/v1/dogs*", où l'étoile sert de joker, car il n'y en a pas par défaut.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
import
io.vertx.ext.web.handler.BodyHandler;
...
public
class
MyApiVerticle extends
AbstractVerticle {
...
@Override
public
void
start
(
) throws
Exception {
LOGGER.info
(
"Dans le start..."
);
// Création du routeur
Router router =
Router.router
(
vertx);
// Body handler
router.route
(
"/api/v1/dogs*"
).handler
(
BodyHandler.create
(
));
// Définition des routes
router.get
(
"/api/v1/dogs"
).handler
(
this
::getAllDogs);
router.get
(
"/api/v1/dogs/:id"
).handler
(
this
::getOneDog);
router.post
(
"/api/v1/dogs"
).handler
(
this
::createOneDog);
Il est temps d'ajouter un nouveau chien dans la liste à partir des informations reçues. Je crée donc d'abord (immutable) un chien sans id. C'est le service qui aura la charge de le créer. En retour, le service me renvoie le chien créé, avec maintenant un id.
2.
3.
4.
private
void
createOneDog
(
RoutingContext routingContext) {
...
final
Dog dog =
new
Dog
(
null
, name, race, age);
final
Dog createdDog =
dogService.add
(
dog);
Il ne reste plus qu'à notifier Vert.x que la réponse peut être envoyée. Elle contient le nouveau chien. Et bien entendu, puisqu'il s'agit d'une création, on indique le statut 201.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
private
void
createOneDog
(
RoutingContext routingContext) {
...
// TODO Vérification des champs...
...
// Création d'un chien
final
Dog dog =
new
Dog
(
null
, name, race, age);
final
Dog createdDog =
dogService.add
(
dog);
// Envoi de la réponse
routingContext.response
(
)
.setStatusCode
(
201
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
Json.encode
(
createdDog));
}
Dans Postman, on peut maintenant appeler l'URL http://localhost:8080/api/v1/dogs en POST en précisant un body en JSON. On peut vérifier que le statut de retour est bien 201 created et qu'un id a été choisi pour notre nouveau chien.
Quand on redemande la liste des chiens, on voit que celui que nous venons d'ajouter est bien présent.
IV-E. Modifier un objet▲
La mise à jour d’un chien va maintenant être relativement simple, car on a déjà tous les éléments. On va donc aller plus vite. Habituellement, on utilise le verbe PUT et on précise l’id dans l’URL.
2.
3.
4.
5.
6.
7.
8.
@Override
public
void
start
(
) throws
Exception {
...
// Définition des routes
router.get
(
"/api/v1/dogs"
).handler
(
this
::getAllDogs);
router.get
(
"/api/v1/dogs/:id"
).handler
(
this
::getOneDog);
router.post
(
"/api/v1/dogs"
).handler
(
this
::createOneDog);
router.put
(
"/api/v1/dogs/:id"
).handler
(
this
::updateOneDog);
On prend donc l'id dans l'URL et les champs dans le body.
2.
3.
4.
5.
6.
7.
8.
9.
private
void
updateOneDog
(
RoutingContext routingContext) {
LOGGER.info
(
"Dans updateOneDog..."
);
// Param id de la requête
final
String id =
routingContext.request
(
).getParam
(
"id"
);
// Params pris dans le body
final
JsonObject body =
routingContext.getBodyAsJson
(
);
final
String name =
body.getString
(
"name"
);
final
String race =
body.getString
(
"race"
);
final
Integer age =
body.getInteger
(
"age"
);
Il ne reste plus qu'à faire appel au service et renvoyer l'objet mis à jour.
2.
3.
4.
5.
6.
7.
8.
9.
10.
private
void
updateOneDog
(
RoutingContext routingContext) {
...
// Création/maj d'un chien
final
Dog dog =
new
Dog
(
id, name, race, age);
final
Dog updatedDog =
dogService.update
(
dog);
// Envoi de la réponse
routingContext.response
(
)
.setStatusCode
(
200
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
Json.encode
(
updatedDog));
Dans Postman, il faudra simplement choisir le verbe PUT sur l'URL http://localhost:8080/api/v1/dogs/s93d78, où « s93d78 » est l'id de Scooby-Doo.
On peut vérifier que Scooby-Doo a bien été changé.
IV-F. Supprimer un objet▲
On arrive à la dernière opération CRUD, consistant à supprimer un chien. À ce stade, cela devrait être une formalité. On commence en indiquant une route sur le verbe DELETE.
2.
3.
4.
5.
6.
7.
8.
9.
@Override
public
void
start
(
) throws
Exception {
...
// Définition des routes
router.get
(
"/api/v1/dogs"
).handler
(
this
::getAllDogs);
router.get
(
"/api/v1/dogs/:id"
).handler
(
this
::getOneDog);
router.post
(
"/api/v1/dogs"
).handler
(
this
::createOneDog);
router.put
(
"/api/v1/dogs/:id"
).handler
(
this
::updateOneDog);
router.delete
(
"/api/v1/dogs/:id"
).handler
(
this
::deleteOneDog);
Le contenu de la méthode est ensuite très simple.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
private
void
deleteOneDog
(
RoutingContext routingContext) {
LOGGER.info
(
"Dans deleteOneDog..."
);
// Param id de la requête
final
String id =
routingContext.request
(
).getParam
(
"id"
);
// Suppression du chien
dogService.remove
(
id);
// Envoi de la réponse
routingContext.response
(
)
.setStatusCode
(
200
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
);
}
Dans Postman, on va supprimer Scooby-Doo en appelant http://localhost:8080/api/v1/dogs/s93d78 sur le verbe DELETE, où « s93d78 » est l'id de Scooby-Doo.
Et bien entendu, on vérifie que Scooby-Doo a bien été retiré de la liste.
V. Réorganiser le routeur▲
Maintenant qu’on a fait l’essentiel de l’API pour nos chiens, on constate que la partie routage commence à être en pagaille. Il faut imaginer que vous aurez probablement plusieurs domaines complexes dans votre programme. Je vous propose donc d’organiser le code par ressource.
On commence par créer la classe DogResource et on y déplace tout le code relatif à l’API chien.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
public
class
DogResource {
...
private
void
getAllDogs
(
final
RoutingContext routingContext) {
...
}
private
void
getOneDog
(
final
RoutingContext routingContext) {
...
}
private
void
createOneDog
(
final
RoutingContext routingContext) {
...
}
private
void
createOneDog
(
final
RoutingContext routingContext) {
...
}
private
void
deleteOneDog
(
final
RoutingContext routingContext) {
...
}
Je voudrais également que toute la logique de routage spécifique aux chiens soit déportée vers cette classe. Et je voudrais que le routage se fasse plus particulièrement sur la fin de "/api/v1/dogs/".
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
public
Router getSubRouter
(
final
Vertx vertx) {
// Création du sous-routeur
final
Router subRouter =
Router.router
(
vertx);
// Body handler
subRouter.route
(
"/*"
).handler
(
BodyHandler.create
(
));
// Définition des routes
subRouter.get
(
"/"
).handler
(
this
::getAllDogs);
subRouter.get
(
"/:id"
).handler
(
this
::getOneDog);
subRouter.post
(
"/"
).handler
(
this
::createOneDog);
subRouter.put
(
"/:id"
).handler
(
this
::updateOneDog);
subRouter.delete
(
"/:id"
).handler
(
this
::deleteOneDog);
return
subRouter;
}
Au niveau du verticle, qui a été nettoyé, je peux définir un SubRouter et mapper toutes les URL en "/api/v1/dogs" dessus.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
@Override
public
void
start
(
) throws
Exception {
LOGGER.info
(
"Dans le start..."
);
// Création du routeur
final
Router router =
Router.router
(
vertx);
// Création de la ressource
final
DogResource dogResource =
new
DogResource
(
);
// Définition des routes et sous-routes
final
Router dogSubRouter =
dogResource.getSubRouter
(
vertx);
router.mountSubRouter
(
"/api/v1/dogs"
, dogSubRouter);
// Lancement du serveur
vertx.createHttpServer
(
)
.requestHandler
(
router)
.listen
(
8080
);
}
Bien entendu, je peux décomposer "/api/v1/dogs" en trois sous routers « imbriqués ». L'étape d'après serait de créer la ressource CatResource et les sous-routeurs associés.
VI. Conclusions▲
Aurons-nous réussi à écrire une API naïve avec Vert.x en 5 minutes chrono ? On ne doit pas en être loin. On a pu voir que c’est relativement simple.
On a tout de même fait l’impasse sur un ensemble de préoccupations, notamment en ce qui concerne la sécurité.
Pour aller plus loin, je vous recommande le module vertx-web-api-service qui va grandement simplifier la création d’une API standardisée. Ce module vous permet notamment de définir les routes et les contrats en Yaml.
Vos retours et remarques nous aident à améliorer les publications de Developpez.com. N'hésitez donc pas à commenter cet article. Commentez
VII. Remerciements▲
D'abord, j'adresse mes remerciements à l'équipe Vert.x et aux créateurs de modules complémentaires, pour avoir développé une bibliothèque aussi utile et pour la maintenir. Je n'oublie pas tous les contributeurs qui participent, notamment sur le forum.
Plus spécifiquement en ce qui concerne cet article, je tiens à remercier l'équipe de Developpez.com et plus particulièrement à Mickael Baron et Bruno Barthel.
VIII. Annexes▲
VIII-A. Liens▲
Vert.x : https://vertx.io/
Documentation du module vertx-web : https://vertx.io/docs/vertx-web/java/
Documentation du module vertx-web-api-service : https://vertx.io/docs/vertx-web-api-service/java/
Postman : https://www.getpostman.com/
Série dédiée à Vert.x : https://thierry-leriche-dessirier.developpez.com/tutoriels/java/vertx/creer-lancer-tester-verticle/
Autres articles de la série « en 5 minutes » : https://thierry-leriche-dessirier.developpez.com/#5min
VIII-B. Codes complets▲
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
<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/xsd/maven-4.0.0.xsd"
>
<modelVersion>
4.0.0</modelVersion>
<groupId>
com.ice</groupId>
<artifactId>
article-vertx-api</artifactId>
<version>
0.0.1-SNAPSHOT</version>
<packaging>
jar</packaging>
<name>
article-vertx-api</name>
<url>
http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>
UTF-8</project.build.sourceEncoding>
<maven.compiler.source>
1.8</maven.compiler.source>
<maven.compiler.target>
1.8</maven.compiler.target>
<vertx.version>
3.8.1</vertx.version>
</properties>
<dependencies>
<dependency>
<groupId>
io.vertx</groupId>
<artifactId>
vertx-core</artifactId>
<version>
${vertx.version}</version>
</dependency>
<dependency>
<groupId>
io.vertx</groupId>
<artifactId>
vertx-web</artifactId>
<version>
${vertx.version}</version>
</dependency>
</dependencies>
</project>
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
package
com.ice.articlevertxapi.bean;
public
class
Dog {
private
final
String id;
private
final
String name;
private
final
String race;
private
final
int
age;
public
Dog
(
final
String id, final
String name, final
String race, final
int
age) {
super
(
);
this
.id =
id;
this
.name =
name;
this
.race =
race;
this
.age =
age;
}
public
String getId
(
) {
return
id;
}
public
String getName
(
) {
return
name;
}
public
String getRace
(
) {
return
race;
}
public
int
getAge
(
) {
return
age;
}
@Override
public
String toString
(
) {
return
"Dog [id="
+
id +
", name="
+
name +
", race="
+
race +
", age="
+
age +
"]"
;
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
package
com.ice.articlevertxapi.service;
import
java.util.HashMap;
import
java.util.List;
import
java.util.Map;
import
java.util.stream.Collectors;
import
com.ice.articlevertxapi.bean.Dog;
public
class
DogService {
private
final
Map<
String, Dog>
dogs =
new
HashMap<
String, Dog>(
);
public
DogService
(
) {
super
(
);
initDogs
(
);
}
private
void
initDogs
(
) {
final
Dog lassie =
new
Dog
(
"12345fh"
, "Lassie"
, "colley"
, 12
);
final
Dog milou =
new
Dog
(
"e7654"
, "Milou"
, "fox-terrier"
, 11
);
final
Dog scoobydoo =
new
Dog
(
"s93d78"
, "Scooby-Doo"
, "Danois"
, 7
);
final
Dog idefix =
new
Dog
(
"6222mk9p"
, "Idefix"
, "Bichon"
, 17
);
dogs.put
(
lassie.getId
(
), lassie);
dogs.put
(
milou.getId
(
), milou);
dogs.put
(
scoobydoo.getId
(
), scoobydoo);
dogs.put
(
idefix.getId
(
), idefix);
}
public
List<
Dog>
findAll
(
) {
return
dogs.values
(
).stream
(
)
.collect
(
Collectors.toList
(
));
}
public
Dog findById
(
final
String id) {
return
dogs.get
(
id);
}
public
Dog update
(
final
Dog dog) {
dogs.put
(
dog.getId
(
), dog);
return
dog;
}
public
void
remove
(
final
String id) {
dogs.remove
(
id);
}
public
Dog add
(
final
Dog dog) {
final
String id =
"fr"
+
System.currentTimeMillis
(
) +
"d"
;
final
Dog newdog =
new
Dog
(
id,
dog.getName
(
),
dog.getRace
(
),
dog.getAge
(
));
dogs.put
(
id, newdog);
return
newdog;
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
package
com.ice.articlevertxapi;
import
com.ice.articlevertxapi.resource.DogResource;
import
io.vertx.core.AbstractVerticle;
import
io.vertx.core.logging.Logger;
import
io.vertx.core.logging.LoggerFactory;
import
io.vertx.ext.web.Router;
public
class
MyApiVerticle extends
AbstractVerticle {
private
static
final
Logger LOGGER =
LoggerFactory.getLogger
(
MyApiVerticle.class
);
@Override
public
void
start
(
) throws
Exception {
LOGGER.info
(
"Dans le start..."
);
final
Router router =
Router.router
(
vertx);
final
DogResource dogResource =
new
DogResource
(
);
final
Router dogSubRouter =
dogResource.getSubRouter
(
vertx);
router.mountSubRouter
(
"/api/v1/dogs"
, dogSubRouter);
vertx.createHttpServer
(
)
.requestHandler
(
router)
.listen
(
8080
);
}
@Override
public
void
stop
(
) throws
Exception {
LOGGER.info
(
"Dans le stop..."
);
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
package
com.ice.articlevertxapi.resource;
import
java.util.List;
import
com.ice.articlevertxapi.bean.Dog;
import
com.ice.articlevertxapi.service.DogService;
import
io.vertx.core.Vertx;
import
io.vertx.core.json.Json;
import
io.vertx.core.json.JsonObject;
import
io.vertx.core.logging.Logger;
import
io.vertx.core.logging.LoggerFactory;
import
io.vertx.ext.web.Router;
import
io.vertx.ext.web.RoutingContext;
import
io.vertx.ext.web.handler.BodyHandler;
public
class
DogResource {
private
static
final
Logger LOGGER =
LoggerFactory.getLogger
(
DogResource.class
);
private
final
DogService dogService =
new
DogService
(
);
public
Router getSubRouter
(
final
Vertx vertx) {
final
Router subRouter =
Router.router
(
vertx);
// Body handler
subRouter.route
(
"/*"
).handler
(
BodyHandler.create
(
));
// Routes
subRouter.get
(
"/"
).handler
(
this
::getAllDogs);
subRouter.get
(
"/:id"
).handler
(
this
::getOneDog);
subRouter.post
(
"/"
).handler
(
this
::createOneDog);
subRouter.put
(
"/:id"
).handler
(
this
::updateOneDog);
subRouter.delete
(
"/:id"
).handler
(
this
::deleteOneDog);
return
subRouter;
}
private
void
getAllDogs
(
final
RoutingContext routingContext) {
LOGGER.info
(
"Dans getAllDogs..."
);
final
List<
Dog>
dogs =
dogService.findAll
(
);
final
JsonObject jsonResponse =
new
JsonObject
(
);
jsonResponse.put
(
"dogs"
, dogs);
jsonResponse.put
(
"my-name"
, "Thierry"
);
routingContext.response
(
)
.setStatusCode
(
200
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
Json.encode
(
jsonResponse));
}
private
void
getOneDog
(
final
RoutingContext routingContext) {
LOGGER.info
(
"Dans getOneDog..."
);
final
String id =
routingContext.request
(
).getParam
(
"id"
);
final
Dog dog =
dogService.findById
(
id);
if
(
dog ==
null
) {
final
JsonObject errorJsonResponse =
new
JsonObject
(
);
errorJsonResponse.put
(
"error"
, "No dog can be found for the specified id:"
+
id);
errorJsonResponse.put
(
"id"
, id);
routingContext.response
(
)
.setStatusCode
(
404
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
Json.encode
(
errorJsonResponse));
return
;
}
routingContext.response
(
)
.setStatusCode
(
200
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
Json.encode
(
dog));
}
private
void
createOneDog
(
final
RoutingContext routingContext) {
LOGGER.info
(
"Dans createOneDog..."
);
final
JsonObject body =
routingContext.getBodyAsJson
(
);
final
String name =
body.getString
(
"name"
);
final
String race =
body.getString
(
"race"
);
final
Integer age =
body.getInteger
(
"age"
);
// TODO Vérification des champs...
final
Dog dog =
new
Dog
(
null
, name, race, age);
final
Dog createdDog =
dogService.add
(
dog);
routingContext.response
(
)
.setStatusCode
(
201
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
Json.encode
(
createdDog));
}
private
void
updateOneDog
(
final
RoutingContext routingContext) {
LOGGER.info
(
"Dans updateOneDog..."
);
final
String id =
routingContext.request
(
).getParam
(
"id"
);
final
JsonObject body =
routingContext.getBodyAsJson
(
);
final
String name =
body.getString
(
"name"
);
final
String race =
body.getString
(
"race"
);
final
Integer age =
body.getInteger
(
"age"
);
// TODO Vérification des champs...
final
Dog dog =
new
Dog
(
id, name, race, age);
final
Dog updatedDog =
dogService.update
(
dog);
routingContext.response
(
)
.setStatusCode
(
200
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
Json.encode
(
updatedDog));
}
private
void
deleteOneDog
(
final
RoutingContext routingContext) {
LOGGER.info
(
"Dans deleteOneDog..."
);
final
String id =
routingContext.request
(
).getParam
(
"id"
);
dogService.remove
(
id);
routingContext.response
(
)
.setStatusCode
(
200
)
.putHeader
(
"content-type"
, "application/json"
)
.end
(
);
}
}
2.
3.
4.
5.
6.
7.
8.
9.
package
com.ice.articlevertxapi;
import
io.vertx.core.Vertx;
public
class
App {
public
static
void
main
(
String[] args) {
System.out.println
(
"App..."
);
final
Vertx vertx =
Vertx.vertx
(
);
vertx.deployVerticle
(
new
MyApiVerticle
(
));
}
}