Mise en place d'une API Rest avec Spring

Les services WEB

Nous avons maintenant besoin de généraliser cette connexion et d'améliorer l'hétérogénéité de nos solutions. Pour ce faire, nous allons introduire la notion de Services WEB (Web Services) qui consiste à utiliser des documents XML pour coder les requêtes et les réponses qui transitent entre le client et serveur via le protocole HTTP :

  +--------------------+                          +--------------------+
  |                    |   ----> Requêtes ---->   |                    |
  |       Client       |                          |      Serveur       |
  |                    |   <---- Réponses <----   |                    |
  +--------------------+                          +--------------------+

Ces WS peuvent publier des données et/ou permettre aux clients de modifier les données métiers. Ces WS échangent souvent des documents XML sur la base du protocole Soap. Le protocole SOAP est géré dans JEE par l'API JAX-WS.

Les services REST

Depuis quelques années, les WS se démocratisent et se simplifient. Afin d'éviter le recours à XML, des auteurs ont suggéré d'utiliser toute la puissance du protocole HTTP pour coder les requêtes en utilisant à la fois :

Préalables

À faire :

Un premier service WEB

Commencons par un service REST très simple :

package myboot.app4.web;

import java.util.Arrays;
import java.util.List;

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

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

    @GetMapping("/hello")
    public String hello() {
        return "Hello";
    }

    @GetMapping(value = "/list")
    public List<Integer> list() {
        return Arrays.asList(10, 20, 30);
    }

}

À faire :

Tester une API REST en créant un client

Tester une API REST avec MockMvc

Nous ne sommes pas obligé de lancer le serveur pour tester une API et nous pouvons utiliser la classe MockMvc. Essayez l'exemple ci-dessous :

package myboot.app4.test;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
public class TestHelloRestApiMockMvc {

    @Autowired
    private MockMvc mvc;

    @Test
    public void testHello() throws Exception {
        mvc.perform(get("/api/hello")).andDo(print())//
                .andExpect(status().isOk())//
                .andExpect(content().string("Hello"));
    }

    @Test
    public void testHelloJohn() throws Exception {
        mvc.perform(get("/api/hello/john")).andDo(print())//
                .andExpect(status().isOk())//
                .andExpect(content().string("Hello john"));
    }

    @Test
    public void testList() throws Exception {
        mvc.perform(get("/api/list")).andDo(print())//
                .andExpect(status().isOk())//
                .andExpect(content().json("[10,20,30]"));
    }

}

Utiliser les headers

Voila un exemple de traitement des entêtes dans des requêtes REST :

@GetMapping(value = "/headers")
public ResponseEntity<String> headers(@RequestHeader String myHeader) {
    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("resultHeader", myHeader.toUpperCase());
    var res = ResponseEntity.ok()//
            .headers(responseHeaders)//
            .header("xx", "yy")//
            .body("HEADER " + myHeader);
    return res;
}

Travail à faire : Préparez un test unitaire avec la méthode exchange d'un RestTemplate et le code ci-dessous pour préparer, envoyer et récupérer des entêtes :

    HttpHeaders headers = new HttpHeaders();
    headers.add("myHeader", "myHeaderValue");
    HttpEntity entity = new HttpEntity(headers);
    ResponseEntity<String> response = restTemplate.exchange(url,
                HttpMethod.GET, entity, String.class);

Utiliser des javaBeans

Nous allons maintenant construire une API REST autour des l'entité Movie utilisée dans le premier TP :

package myboot.app4.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import myboot.app1.dao.MovieRepository;
import myboot.app1.model.Movie;

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

    @Autowired
    MovieRepository repo;

    @GetMapping("/movies")
    public Iterable<Movie> getMovies() {
        return repo.findAll();
    }

    @GetMapping("/movies/{id}")
    public Movie getMovie(@PathVariable int id) {
        return repo.findById(id).get();
    }

}

Travail à faire :

Contrôler les données renvoyés

Travail à faire :

Supprimer des données

Ajoutez la nouvelle route ci-dessous et vérifiez par un test unitaire son bon fonctionnement.

@DeleteMapping("/movies/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void deleteMovie(@PathVariable int id) {
    repo.deleteById(id);
}

Ajouter des données

Ajoutez la nouvelle route ci-dessous et vérifiez par un test unitaire son bon fonctionnement.

@PostMapping("/movies")
public Movie postMovie(@RequestBody Movie m) {
    repo.save(m);
    return m;
}

Travail à faire :

Modifier des données

En respectant la même forme, traitez la requête PUT /movies qui va mettre à jour un film. Il faut traiter le cas de la mise à jour d'un film qui n'existe pas en BD (je vous conseille d'utiliser la méthode orElseThrow du résultat Optional renvoyé par la méthode Spring Boot findById - plus d'informations).

Travail à faire : Ajoutez un test unitaire pour valider cette modification.

Améliorer le traitement des erreurs

Pour l'instant, les requêtes GET /movies/ID et PUT /movies sur des ID erronés provoquent une erreur interne qui n'est pas très lisible. Définissez l'exception suivante :

@ResponseStatus(value = HttpStatus.NOT_FOUND)
class MovieNotFoundException extends RuntimeException {
    private static final long serialVersionUID = 1L;
}

et utilisez-là (avec la clause orElseThrow) pour que ces deux routes provoquent des erreurs NOT_FOUND. Modifiez en conséquence les tests unitaires.

Filtrer des données

Modifiez le traitement de la route GET /movies afin d'ajouter des fonctions de filtrage sur des paramètres optionnels comme

GET /movies?name=Fred&year=1999

Traitement des relations

Nous allons nous baser sur les entités User, Post et Comment du TP sur les graphes d'entités. Faites en sorte de créer des données qui comportent des boucles (User1 -> Post1 -> Comment1 -> User1) et d'autres pas (User2 -> Post2 -> Comment2 -> User3).

package myboot.app4.model;

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;

import lombok.Data;
import lombok.NoArgsConstructor;

@JsonIdentityInfo(//
        generator = ObjectIdGenerators.PropertyGenerator.class, //
        property = "name"//
)
@Data
@NoArgsConstructor
public class Loop {

    private String name = "";

    private Loop loop;

    public Loop(String name) {
        super();
        this.name = name;
    }

}