Tutoriel REST - premier développement d'une ressource
Pour ce premier TP REST, nous allons voir les principaux éléments du développement d'une ressource :
- POJO annoté
- Objets de transfert (Data Transfer Object - DTO)
- Gestion des représentations (JSON ou XML)
- Gestion du cache (ETag)
Mise en place de l'environnement
Le développement sera basé sur Jersey
qui fournit une implémentation de référence de JAX-RS. Vous trouverez dans la documentation l'utilisation des annotation standards ainsi que les aspects spécifiques de la plate-forme.
Le développement avec Jersey implique l'utilisation de Maven
pour la gestion des phases de développement. Maven va télécharger les librairies nécessaires depuis un dépôt extérieur. Il faut donc le configurer pour passer par le proxy quand vous êtes en salle de TP.
Dans le répertoire ~/.m2/
(a créer si nécessaire), créez le fichier 'settings.xml' avec le contenu suivant :
<settings>
<proxies>
<proxy>
<id>ulille-proxy</id>
<active>true</active>
<protocol>http</protocol>
<host>cache.univ-lille.fr</host>
<port>3128</port>
</proxy>
<proxy>
<id>lille1-proxy-sec</id>
<active>true</active>
<protocol>https</protocol>
<host>cache.univ-lille.fr</host>
<port>3128</port>
</proxy>
</proxies>
</settings>
Configuration de Maven
Le projet que vous avez récupéré contient le fichier de configuration pom.xml
. Le projet est actuellement configuré pour utiliser java 11 :
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<inherited>true</inherited>
<configuration>
<release>11</release>
</configuration>
</plugin>
Le code récupéré
L'arborescence de source contient les fichiers suivants :
src
├── main
│ └── java
│ └── fr
│ └── ulille
│ └── iut
│ └── tva
│ ├── DebugMapper.java
│ ├── dto
│ ├── Main.java
│ ├── ressource
│ │ └── TvaRessource.java
│ └── service
│ ├── CalculTva.java
│ └── TauxTva.java
└── test
└── java
└── fr
└── ulille
└── iut
└── tva
Pour ce TP, nous n'utiliserons pas l'arborescence de tests (ça sera pour la prochaine fois :-)). Il n'y aura pas de base de données à gérer afin de pouvoir nous concentrer sur la mise ne place d'une ressource REST.
Le service que nous allons exposer sous forme de ressource REST permet de faire des calculs de TVA. La définition des différents taux se trouve dans la classe TauxTva
et les différents calculs possibles dans la classe CalculTva
.
Le serveur Web qui hébergera notre ressource est lancé directement dans la classe Main
. Cette classe met également en place l'environnement Jersey qui va rechercher dans les paquetages définis les ressources disponibles.
Le développement de la ressource se fera dans la classe TvaRessource
.
Développement de la ressources
Une première personnalisation
Pour l'instant, les ressources que nous allons développer seront disponibles à partir de l'URI suivante : http://localhost:8080/myapp/
. Modifiez la classe Main
de manière à ce que l'URI utilisée soit http://localhost:8080/api/v1
correspondant à notre première version de l'API (Pour en savoir plus sur la façon de gérer les changements de version d'une API REST, vous pouvez consulter cet article).
Profiter en pour lire les commentaires dans le code de cette classe. Cela pourra vous servir plus tard...
La première méthode
Implémentation
Nous allons éditer la classe TvaRessource
. Cette ressource devra être accessible via l'URI suivante : http://localhost:8080/api/v1/tva
. Pour cela, vous devez annoter la classe avec @Path
:
@Path("tva")
public class TvaRessource {
}
Une classe annotée avec @Path
sera reconnue automatiquement comme une ressource REST par Jersey. Le chemin indiqué ici est relatif, il sera combiné avec l'URI définie dans la classe Main
.
Nous allons pouvoir fournir dans cette classe une première méthode Java accessible via une requête HTTP GET. Nous utiliserons pour cela l'annotation @GET
@GET
@Path("tauxpardefaut")
public double getValeurTauxParDefaut() {
return TauxTva.NORMAL.taux;
}
Test
Pour tester notre ressource, nous allons utiliser Maven pour compiler et lancer le serveur avec la commande mvn compile exec:java
dans un terminal.
Dans un autre terminal, utilisez curl
pour accéder à votre ressource et tester cette nouvelle méthode.
Vous devriez obtenir une réponse de ce type :
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 4
20.0
On peut remarquer que comme votre code a renvoyé un résultat sans lever d'erreur, Jersey a automatiquement :
- renvoyé le statut
200 OK
; - choisi le type MIME le plus adapté pour le résultat renvoyé dans le corps de la réponse :
Content-Type: text/plain
; - mis dans le corps de la réponse le résultat de votre méthode.
On joue avec les paramètres de requête
La requête HTTP peut varier notamment selon le chemin de l'URI (@PathParam) ou les paramètres de requête (@QueryParam)
La méthode ci-dessous renvoie la valeur du taux pour un niveau de TVA donné. Ce niveau de TVA (REDUIT, NORMAL, etc.) constitue une partie variable de l'URI qui sera notée entre {}
. Le paramètre de la méthode est associé à cette partie du chemin grâce à l'annotation @PathParam
.
@GET
@Path("valeur/{niveauTva}")
public double getValeurTaux(@PathParam("niveauTva") String niveau) {
return TauxTva.valueOf(niveau.toUpperCase()).taux;
}
Ajoutez cette méthode à votre classe puis testez la avec curl
.
Ajoutez (et testez) à votre ressource une méthode getMontantTotal
qui renverra le montant tva comprise pour un niveau de tva indiqué sur le chemin d'URI et une somme définie en paramètre de requête (par ex. http://localhost:8080/api/v1/tva/reduit?somme=100
).
Gérer les erreurs
Notre ressource renvoie correctement les informations demandées... tant que nos requêtes sont correctes. Essayez cette requête : curl -i http://localhost:8080/api/v1/tva/valeur/inexistant
.
Le résultat n'est pas terrible. Pour gérer cela proprement, nous allons devoir renvoyer une erreur qui a du sens pour le client. Nous allons modifier la méthode getValeurTaux
de manière à réagir à un niveau de TVA inconnu.
public float getValeurTaux(@PathParam("niveauTva") String niveau) {
try {
return TauxTva.valueOf(niveau.toUpperCase()).taux;
}
catch ( Exception ex ) {
throw new NiveauTvaInexistantException();
}
}
Il nous faut ensuite définir la nouvelle exception utilisée de cette façon :
package fr.ulille.iut.tva.ressource;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;
public class NiveauTvaInexistantException extends WebApplicationException {
private static final long serialVersionUID = 939875418210403804L;
public NiveauTvaInexistantException() {
super(Response.status(Response.Status.NOT_ACCEPTABLE).entity("Niveau de TVA inexistant").build());
}
On aurait pu se contenter de lever NotAcceptableException
directement dans la méthode getValeurTaux
, ce qui aurait renvoyé un code 406. La création de cette exception va nous permettre d'enrichir la réponse et notamment ici d'ajouter un message d'erreur dans le corps (entity) de la réponse.
Modifiez également la méthode getMontantTotal
de manière à gérer l'erreur.
Renvoyer des données plus complexes
Jusqu'à maintenant, nous avons transmis de simples valeurs numériques prises en charge directement par Jersey dans le corps de la réponse HTTP. Jersey peut faire la même chose avec des classes Java qui respectent les conventions JavaBean
, à minima :
- la classe possède un constructeur sans paramètres (constructeur par défaut);
- les propriétés de la classe sont associées à des accesseurs
get
/set
pour chaque attribut (par. ex pour un attributname
-->getName()
/setName()
).
Pour un objet de ce type, Jersey va renvoyer dans le corps de la requête un objet (JSON par défaut). Chaque propriété de l'objet java sera représenté par une propriété de l'objet JSON.
Ces objets Java destinés à représenter les données échangées s'appellent des Data Transfer Object
ou DTO
(ils peuvent bien sûr également être transmis par le client lors d'une requête POST
par exemple).
Nous allons ajouter une méthode permettant au client d'avoir la liste des niveaux de TVA avec le taux associé. Pour cela, nous allons d'abord définir un DTO pour représenter cela dans le paquetage fr.ulille.iut.tva.dto
.
package fr.ulille.iut.tva.dto;
public class InfoTauxDto {
private String label;
private double taux;
public InfoTauxDto() {}
public InfoTauxDto(String label, double taux) {
this.label = label;
this.taux = taux;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public double getTaux() {
return taux;
}
public void setTaux(double taux) {
this.taux = taux;
}
}
Nous pouvons maintenant utiliser ce DTO pour renvoyer la liste des taux existants au client :
@GET
@Path("lestaux")
public List<InfoTauxDto> getInfoTaux() {
ArrayList<InfoTauxDto> result = new ArrayList<InfoTauxDto>();
for ( TauxTva t : TauxTva.values() ) {
result.add(new InfoTauxDto(t.name(), t.taux));
}
return result;
}
Un test de cette nouvelle méthode devrait renvoyer le résultat suivant :
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 135
[{"label":"NORMAL","taux":20.0},{"label":"INTERMEDIAIRE","taux":10.0},{"label":"REDUIT","taux":5.5},{"label":"PARTICULIER","taux":2.1}]
On peut observer que :
- pour chaque propriété de la classe
InfoTauxDto
, on a une propriété équivalente dans les objets JSON - la liste renvoyée en Java a été convertie en tableau d'objets JSON
- Jersey a choisi le type MIME le plus adapté pour le résultat renvoyé dans le corps de la réponse :
Content-Type: application/json
Vous pouvez maintenant ajouter une méthode getDetail
qui répondra à une URI de ce type : http://localhost:8080/api/v1/tva/details/{taux}?somme={valeur}
où taux
correspond à un des niveaux de taux et valeur
correspond à la somme utilisée pour le calcul.
Le résultat d'une requête sur cette URI ressemblera à cela :
$ curl -i http://localhost:8080/api/v1/tva/details/normal?somme=100
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 92
{"montantTotal":120.0,"montantTva":20.0,"somme":100.0,"tauxLabel":"NORMAL","tauxValue":20.0}
Et les autres formats de données ?
Essayez de faire la requête suivante : curl -i -H "Accept: application/xml" http://localhost:8080/api/v1/tva/lestaux
. Quel résultat obtenez vous ?
Du côté du serveur, vous devriez observer l'erreur suivante :
GRAVE: MessageBodyWriter not found for media type=application/xml, type=class java.util.ArrayList, genericType=java.util.List<fr.ulille.iut.tva.dto.InfoTauxDto>
Ce message indique que Jersey ne sait pas transformer la liste Java dans une représentation application/xml
. MessageBodyWriter
est la classe qui devrait être chargée de faire cette transformation (Elle le fait pour JSON).
Pour gérer XML, il faut d'abord s'assurer d'avoir les bonnes libraires dans le projet maven (c'est déjà le cas, ici) :
<dependencies>
...
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>3.0.0</version>
<scope>runtime</scope>
</dependency>
...
</dependencies>
Ces librairies vont nous permettre d'utiliser JAXB (Java Architecture for XML Binding) qui offre les moyens de définir une transformation entre Java et XML. JAXB
utilise notamment un ensemble d'annotations qui permettent de spécifier comment les attributs d'une classe seront transformés en XML (et inversement).
Ici, nous allons utiliser tous les comportements par défaut de JAXB et nous n'auront besoin que d'une annotation sur notre DTO :
import jakarta.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class InfoTauxDto {
...
}
Nous pouvons maintenant ressayer notre requête précédente avec succès :
HTTP/1.1 200 OK
Content-Type: application/xml
Content-Length: 356
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><infoTauxDtoes><infoTauxDto><label>NORMAL</label><taux>20.0</taux></infoTauxDto><infoTauxDto><label>INTERMEDIAIRE</label><taux>10.0</taux></infoTauxDto><infoTauxDto><label>REDUIT</label><taux>5.5</taux></infoTauxDto><infoTauxDto><label>PARTICULIER</label><taux>2.1</taux></infoTauxDto></infoTauxDtoes>
Refaisons notre requête par défaut par acquis de conscience : curl -i http://localhost:8080/api/v1/tva/lestaux
. Que se passe-t-il ?
Il semble que du coup, notre ressource réponde en priorité en XML, vérifiez qu'elle sait toujours répondre en JSON.
Pour régler ce problème, nous allons spécifier les représentations possibles avec un ordre de préférence (plutôt du JSON) au moyen de l'annotation @Produces :
@GET
@Path("lestaux")
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public List<InfoTauxDto> getInfoTaux() {
...
}
Faites en sorte que la méthode getDetail
puisse également renvoyer du JSON (de préférence) ou de l'XML.