Les API Web avec Spring Web MVC

Une API Web suit les mêmes règles qu’un site Web traditionnel. La différence est avant tout une différence d’usage. Une API Web fonctionne selon un échange de requêtes/réponses HTTP mais elle n’est pas utilisée pour fournir un contenu directement affichable à un individu. La différence porte donc le plus souvent sur le format des représentations échangées entre le client et le serveur : pour un site Web, le serveur fournira principalement des pages HTML alors que pour une API Web, il s’agira d’utiliser des formats de représentation permettant de traiter directement les données par le dispositif client. Pour ce dernier cas, on privilégie généralement les formats JSON et XML, plus simples à analyser par des programmes.

Une API Web peut ainsi être utilisée dans des contextes très divers :

  • pour un échange M2M (Machine to Machine) dans lequel deux systèmes d’information s’échangent des données.

  • plus récemment pour l’Internet des Objets (IoT) afin de permettre à différents dispositifs de communiquer entre eux.

  • pour un site Web riche. Le client Web est capable de générer des requêtes HTTP asynchrones (ajax) pour récupérer des données sur le serveur et mettre à jour une partie du contenu de la page.

  • pour des applications mobiles (Android / iOS) afin de récupérer des données applicatives sur Internet.

Support pour les formats JSON et XML

Nous allons voir que le développement d’API Web suppose de produire des réponses dans des formats comme le JSON ou le XML.

Pour une application avec Spring Boot, la configuration par défaut inclue une prise en charge de ces formats de représentation. Il n’y a donc aucune configuration particulière à réaliser.

Pour une application sans Spring Boot, vous devez ajouter des dépendances pour chaque format que votre application doit supporter. Pour un projet géré avec Maven, vous devez ajouter une dépendance à jackson-databind pour le support du JSON et à jackson-dataformat-xml pour le support du XML dans le fichier pom.xml :

Dépendances pour le support JSON et XML
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.4</version>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.9.4</version>
</dependency>

L’annotation @RestController

Une classe annotée avec @RestController indique qu’il s’agit d’un contrôleur spécialisé pour le développement d’API Web. @RestController est simplement une annotation qui regroupe @Controller et @ResponseBody. Il s’agit donc d’un contrôleur dont les méthodes retournent par défaut les données à renvoyer au client plutôt qu’un identifiant de vue.

Note

Le qualificatif de REST pour ce contrôleur est malheureusement le résultat d’une confusion (courante) des développeurs qui mélangent REST avec API Web (et API Web avec JSON).

REST est l’ensemble des contraintes sur lesquelles sont basés le Web et le protocole HTTP. Donc tous les contrôleurs développés avec Spring Web MVC doivent se conformer au modèle REST.

Il aurait été plus juste d’avoir une annotation nommée @WebApiController pour ce cas particulier de type de contrôleurs.

Un contrôleur pour une API Web

À titre d’exemple, imaginons que nous disposions d’une classe Item pour représenter des données de notre modèle :

La classe Item
package dev.gayerie;

public class Item {

    private String name;

    private String code;

    private int quantity;

    // Getters/setters omis

}

Nous pouvons très facilement déclarer un contrôleur pour obtenir une représentation JSON d’une instance de la classe Item :

Un contrôleur pour une API Web
package dev.gayerie;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class ItemController {

    @GetMapping(path="/item", produces= "application/json")
    public Item getItem() {
        Item item = new Item();
        item.setCode("XV-32");
        item.setName("Weird stuff");
        item.setQuantity(10);
        return item;
    }

}

Une classes annotée avec @RestController fonctionne de la même façon qu’une classe annotée avec @Controller. Notez cependant que l’attribut produces de l’annotation @GetMapping a été positionné à "application/json". Cela signifie que Spring Web MVC va convertir l’instance de la classe Item retournée par la méthode en une représentation JSON avant de l’envoyer au client.

Note

La sérialisation JSON sera réalisée par la bibliothèque Jackson.

Si nous déployons sur notre serveur local notre application dans le contexte myapp, nous pouvons utiliser le programme cURL pour interroger notre API :

curl http://localhost:8080/myapp/api/item

{"name":"Weird stuff","code":"XV-32","quantity":10}

La négociation de contenu

HTTP permet la négociation de contenu proactive. Cela signifie qu’un client peut envoyer ses préférences au serveur. Ce dernier doit répondre au mieux en fonction des préférences reçues et de ses capacités. Une négociation possible porte sur le format de représentation. Cela peut s’avérer utile pour une API Web destinée à des clients très divers. Par exemple, certains clients peuvent privilégier le XML et d’autres le JSON.

La négociation proactive pour le type de représentation est réalisée par le client qui envoie dans sa requête un en-tête Accept donnant la liste des types MIME qu’il préfère. Avec Spring Web MVC, cette négociation est automatiquement gérée par le contrôleur.

Nous pouvons maintenant faire évoluer notre contrôleur pour indiquer qu’il peut produire du JSON ou du XML en déclarant un tableau de types MIME dans l’attribut produces :

Un contrôleur pour une API Web
package dev.gayerie;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class ItemController {

    @GetMapping(path="/item", produces= {"application/json", "application/xml"})
    public Item getItem() {
        Item item = new Item();
        item.setCode("XV-32");
        item.setName("Weird stuff");
        item.setQuantity(10);
        return item;
    }

}

Par défaut, ce contrôleur produit toujours du JSON (car le type MIME JSON est placé en premier dans la liste) mais un client peut indiquer qu’il préfère une représentation XML grâce à l’en-tête Accept :

curl -H "Accept: application/xml" http://localhost:8080/myapp/api/item

<Item><name>Weird stuff</name><code>XV-32</code><quantity>10</quantity></Item>

L’envoi de données

Les API Web sont souvent utilisées pour effectuer des opérations modifiant l’état du serveur (création, modification, suppression). Pour ces cas, il est toujours possible d’envoyer au serveur des paramètres d’URI et/ou des données comme un formulaire HTML. Cependant, comme les formats JSON et XML sont souvent utilisés comme représentation dans les réponses du serveur, il paraît cohérent de permettre à un client d’envoyer des données au serveur dans un de ces formats.

Pour autoriser cela, il suffit d’utiliser l’attribut consumes pour les annotations de type @RequestMapping conjointement avec l’annotation @RequestBody.

Un contrôleur acceptant des requêtes au format JSON
package dev.gayerie;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class ItemController {

    @PostMapping(path="/items", consumes="application/json")
    @ResponseStatus(code=HttpStatus.CREATED)
    public void createItem(@RequestBody Item item) {
        // ...
    }

}

Dans l’exemple ci-dessus, le contrôleur accepte une requête POST pour le chemin /api/items contenant des données au format JSON. Le paramètre item dispose de l’annotation @RequestBody. Donc Spring Web MVC va considérer que ce paramètre représente le corps de la requête. Il va donc tenter de convertir le document JSON en une instance de la classe Item.

Note

La désérialisation du document JSON vers l’objet Java sera réalisée par la bibliothèque Jackson.

Si nous déployons sur notre serveur local notre application dans le contexte myapp, nous pouvons utiliser le programme cURL pour envoyer des données à notre API :

curl -H "Content-type: application/json" -d '{"name":"mon item","code":"1337","quantity":1}' http://localhost:8080/myapp/api/items

Comme pour le contenu d’une réponse, nous pouvons autoriser plusieurs formats de représentation dans un contrôleur en fournissant une liste à l’attribut consumes :

@PostMapping(path="/items", consumes={"application/json", "application/xml"})
@ResponseStatus(code=HttpStatus.CREATED)
public void createItem(@RequestBody Item item) {
    // ...
}

La réponse

Par défaut, un contrôleur pour une API Web retourne un code statut HTTP 200 si la méthode retourne un objet ou 204 (No Content) si la méthode retourne void. Si on désire positionner un code statut particulier, il est possible d’utiliser l’annotation @ResponseStatus avec un code particulier parmi l’énumération HttpStatus.

Changement du code statut de la réponse avec une annotation
@PostMapping(path="/items", consumes={"application/json", "application/xml"})
@ResponseStatus(code=HttpStatus.CREATED)
public void createItem(@RequestBody Item item) {
    // ...
}

Si on désire contrôler plus finement le contenu de la réponse, il est possible de retourner un objet de type ResponseEntity<T>.

package dev.gayerie;

import java.net.URI;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;

@RestController
@RequestMapping("/api")
public class ItemController {

    @PostMapping(path="/items", consumes="application/json", produces="application/json")
    public ResponseEntity<Item> createItem(@RequestBody Item item,
                                           UriComponentsBuilder uriBuilder) {

        // ...

        URI uri = uriBuilder.path("/api/items/{code}").buildAndExpand(item.getCode()).toUri();
        return ResponseEntity.created(uri).body(item);
    }

}

Un objet ResponseEntity<T> peut être créé à partir de méthodes statiques correspondant aux cas d’utilisation les plus courants en HTTP. Dans l’exemple ci-dessus, la méthode ResponseEntity<T>.created permet de créer une réponse avec un code statut 201 (Created) et un en-tête Location contenant le lien vers la ressource créée sur le serveur. Ainsi la méthode ResponseEntity<T>.created attend en paramètre l’URI de la ressource. Dans l’exemple ci-dessus, on accède à une instance de UriComponentsBuilder qui est fournie par Spring Web MVC afin de nous aider à construire une URI pour une ressource du serveur.

Si nous déployons sur notre serveur local notre application dans le contexte myapp, nous pouvons utiliser le programme cURL pour envoyer des données à notre API :

curl -i -H "Content-type: application/json" -d '{"name":"mon item","code":"1337","quantity":1}' http://localhost:8080/myapp/api/items

HTTP/1.1 201
Location: http://localhost:8080/myapp/api/items/1337
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 06 Mar 2018 10:00:00 GMT

{"name":"mon item","code":"1337","quantity":1}

RestControllerAdvice

Comme pour les contrôleurs de base, il est possible d’ajouter dans un contrôleur pour une API Web des méthodes pour gérer les exceptions (annotées avec @ExceptionHandler), des méthodes de binder (annotées avec @InitBinder) et des méthodes de modèle (annotées avec @ModelAttribute).

Afin de réutiliser ces méthodes à travers plusieurs contrôleurs, il est aussi possible de les regrouper dans une classe annotée avec @RestControllerAdvice. Comme pour l’annotation @RestController, @RestControllerAdvice est une annotation composée de @ControllerAdvice et de @ResponseBody. Concrètement, elle change l’interprétation par défaut des méthodes de gestion des exceptions, en considérant que la valeur de retour correspond à la réponse à sérialiser directement dans la représentation de la réponse (le plus souvent pour créer un document XML ou JSON).

Les annotations Jackson

Si vous utilisez la bibliothèque Jackson pour convertir les objets Java en XML ou JSON, vous pouvez utiliser des annotations dans la déclaration des objets afin de modifier le comportement par défaut de la sérialisation/désérialisation.

Astuce

Pour tester la conversion d’un objet Java en JSON via Jackson vous pouvez écrire un programme (ou un test) en utilisant une instance de la classe ObjectMapper fournie par Jackson :

Un programme qui produit un document JSON sur la sortie standard
package dev.gayerie;

import com.fasterxml.jackson.databind.ObjectMapper;

public class JacksonSerialisation {

    public static void main(String[] args) throws Exception {
        Object obj = new Item();

        ObjectMapper objectMapper = new ObjectMapper();
        System.out.println(objectMapper.writeValueAsString(obj));
    }

}

Pour tester la conversion d’un objet Java en XML via Jackson, il faut utiliser la classe XmlMapper fournie par Jackson :

Un programme qui produit un document XML sur la sortie standard
package dev.gayerie;

import com.fasterxml.jackson.dataformat.xml.XmlMapper;

public class JacksonSerialisation {

    public static void main(String[] args) throws Exception {
        Object obj = new Item();

        XmlMapper xmlMapper = new XmlMapper();
        System.out.println(xmlMapper.writeValueAsString(obj));
    }

}

Parmi les annotations utiles, on peut citer :

@JsonProperty

Cette annotation ajoutée à un attribut permet de spécifier le nom de la propriété dans le document JSON ou le nom de l’élément dans un document XML.

@JsonIgnore

Cette annotation ajoutée à un attribut permet d’exclure cet attribut de la sérialisation/désérialisation.

@JsonRootName

Cette annotation ajoutée sur une classe permet de spécifier le nom de l’élément s’il doit apparaître à la racine du document. Cette annotation est surtout utile pour la génération de document XML afin de changer le nom de l’élément racine.

@JsonPropertyOrder

Cette annotation ajoutée à une classe permet de fixer l’ordre des éléments dans le document.

@JsonView

Cette annotation ajoutée à un attribut permet de définir une ou des vues JSON pour lesquelles cet attribut doit apparaître (Cf exemple ci-dessous).

Note

Pour la liste complète des annotations disponibles avec Jackson, vous pouvez vous reporter à la documentation officielle.

Reprenons notre classe Item en ajoutant des annotations Jackson :

package dev.gayerie;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonRootName;

@JsonRootName("item")
@JsonPropertyOrder({"nom", "code", "quantite"})
public class Item {

    @JsonProperty("nom")
    private String name;

    private String code;

    @JsonProperty("quantite")
    private int quantity;

    // Getters/setters omis

}

La sérialisation avec Jackson d’un objet de la classe Item donnera les documents suivants :

sérialisation en JSON
{"nom":"Weird stuff","code":"XV-35","quantite":1}
sérialisation en XML
<item><nom>Weird stuff</nom><code>XV-35</code><quantite>1</quantite></item>

Les vues JSON

L’utilisation de vues JSON permet de ne convertir qu’une partie de l’objet. Pour cela, nous créons des interfaces qui servent à désigner des vues. Pour notre exemple, nous allons créer les interfaces ItemViewWithoutQuantity et ItemViewWithQuantity :

les interfaces représentant les vues JSON
package dev.gayerie;

public interface ItemViewWithoutQuantity {

}

package dev.gayerie;

public interface ItemViewWithQuantity extends ItemViewWithoutQuantity {

}

Notez que ItemViewWithQuantity hérite de ItemViewWithoutQuantity car dans notre exemple nous voulons simplement exclure dans certains cas l’attribut quantity de la sérialisation. Nous pouvons revoir la définition de la classe Item en ajoutant des annotations @JsonView pour attribuer une vue à chaque attribut :

déclaration des vues JSON
package dev.gayerie;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.annotation.JsonView;

@JsonRootName("item")
@JsonPropertyOrder({"nom", "code", "quantite"})
public class Item {

    @JsonProperty("nom")
    @JsonView(ItemViewWithoutQuantity.class)
    private String name;

    @JsonView(ItemViewWithoutQuantity.class)
    private String code;

    @JsonProperty("quantite")
    @JsonView(ItemViewWithQuantity.class)
    private int quantity;

    // Getters/setters omis

}

Les vues JSON son facilement utilisables dans un contrôleur Spring car on peut préciser la vue grâce à l’annotation @JsonView sur la valeur de retour d’une méthode :

package dev.gayerie;

import java.net.URI;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.util.UriComponentsBuilder;

import com.fasterxml.jackson.annotation.JsonView;

@RestController
@RequestMapping("/api")
public class ItemController extends ResponseEntityExceptionHandler{

    @PostMapping(path="/items", consumes="application/json", produces="application/json")
    @JsonView(ItemViewWithoutQuantity.class)
    public ResponseEntity<Item> createItem(@RequestBody Item item, UriComponentsBuilder uriBuilder) {
        System.out.println(item.getCode());
        URI uri = uriBuilder.path("/api/item/{code}").buildAndExpand(item.getCode()).toUri();
        return ResponseEntity.created(uri).body(item);
    }

}

L’appel à cette API produira le document :

sérialisation en JSON
{"nom":"Nom de l'item","code":"Code de l'item"}

L’attribut quantite n’est pas présent dans le document JSON car l’annotation @JsonView limite la sérialisation à la vue ItemViewWithoutQuantity.

Astuce

Pour tester les vues JSON directement via Jackson, vous pouvez modifier les appels à l’instance de ObjectMapper pour lui indiquer la vue à utiliser :

programme de sérialisation JSON en précisant une vue
package dev.gayerie;

import com.fasterxml.jackson.databind.ObjectMapper;

public class JacksonSerialisation {

    public static void main(String[] args) throws Exception {
        Item obj = new Item();
        obj.setCode("XV-35");
        obj.setName("Weird stuff");
        obj.setQuantity(1);

        ObjectMapper objectMapper = new ObjectMapper();
        System.out.println(objectMapper.writerWithView(ItemViewWithoutQuantity.class)
                                       .writeValueAsString(obj));
    }

}

Implémentation d’un client

Spring Web MVC fournit la classe RestTemplate permettant d’effectuer des requêtes HTTP. Cette classe permet de convertir les objets Java au format JSON ou XML pour une requête (et inversement de transformer un réponse du serveur au format JSON ou XML en instance d’un objet Java).

En reprenant notre exemple précédent pour la création d’un Item, on peut écrire l’application client suivante :

un exemple d’application cliente avec RestTemplate
package dev.gayerie;

import java.net.URI;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

public class WebApiClient {

    public static void main(String[] args) throws Exception {
        RestTemplate client = new RestTemplate();
        URI uri = new URI("http://localhost:8080/myapp/api/items");

        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.set("Content-type", "application/json");

        Item item = new Item();
        item.setCode("1337");
        item.setName("weird stuff");
        item.setQuantity(1);

        HttpEntity<Item> entity = new HttpEntity<Item>(item, requestHeaders);
        ResponseEntity<Item> responseEntity = client.postForEntity(uri, entity, Item.class);

        System.out.println(responseEntity.getHeaders().getLocation());
        Item itemResultat = responseEntity.getBody();
        System.out.println(itemResultat.getCode());
    }

}

Note

Spring 5 a introduit un nouvelle classe pour implémenter un client HTTP : WebClient