Skip to content
Snippets Groups Projects
Readme.md 11.5 KiB
Newer Older
Yvan Peter's avatar
Yvan Peter committed
# Exemple de développement REST avec Spring Boot

## Spring Boot
Spring est un framework Java qui cherche à simplifier et automatiser l'instanciation et la configuration des différents composants par une configuration externe (en XML) et de l'injection (des références de composants tiers) au niveau des classes.

Spring boot vise a simplifier la mise en œuvre de spring en cachant une partie de la complexité avec des choix par défaut sur la configuration des composants et l'injection.

## Mise en place du développement
Le projet a été généré via le site [Spring Initialzr](https://start.spring.io/) qui permet de définir les paquetages et surtout les dépendances du projet.

![Copie d'écran du site](Spring_Initialzr.png)

Les dépendances choisies permette de générer le fichier pom.xml du projet afin que vous ayez toutes les libraires nécessaire pour chaque type de fonctionnalité.

## Le développemnet de la ressource

### Vision globale des éléments à mettre en place

Le schéma ci-dessous présente les principaux éléments de la solution :
  
Yvan Peter's avatar
Yvan Peter committed
- Le _Controller_ est la classe qui traite les requêtes HTTP. C'est cette classe qui sera annotée pour associer les verbes HTTP et les URI à des méthodes.
- Le _Service_ correspond au composant qui implémente le comportement métier. Ce composant est utilisé par le _Controller_ pour réaliser les opérations demandées.
- La classe `Ingredient`est un javaBean qui représente le modèle de données. Ici il servira directement de DTO (Data Trasnfert Object). Je n'ai pas fait de DTO spécifiques. Il est également annoté comme entité JPA pour la persistence (DAO (Data Access Object)).
- Le _Repository_ correspond au méthodes de manipulation de la base de données, ici on se contentera d'hériter d'une classe de Spring qui fournit les opérations de base (CRUD, findById...).
![Schéma d'architecture](architecture_spring.png)
Les noms des classes et leurs fonctions correspondent à l'organisation et aux patrons préconisés par Spring / Spring boot.
### Les annotations pour associer les méthodes aux verbes HTTP et URI
Il existe une annotation générique [`@RequestMapping`](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html) qui permet de définir le `path` (URI) et la `method` (verbe HTTP) mais également les types MIME produits et consommés ainsi que d'autres éléments. Cette annotation peut être utilisée au niveau de la classe ou des méthodes.

Vous pouvez également utiliser directement des annotations plus spécifiques au niveau des méthodes qui correpondent au verbe HTTP associé : ` @GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping` ou `@PatchMapping`. Ces annotation utilisent les même paramètres que `@RequestMapping` (à l'exception de la méthode évidemment).

Voici un extrait de la classe `IngredientController` qui vise à traiter les requêtes HTTP :

``` java
@RestController
@RequestMapping("ingredients")
public class IngredientController {

    @GetMapping
    public List<Ingredient> getIngredients() {
    }

    @GetMapping("{id}")
    public Ingredient getOneIngredient(@PathVariable Long id) {
    }

    @PostMapping
    public ResponseEntity<Ingredient> createIngredient(@RequestBody @Valid Ingredient ingredient) {
    }

    @DeleteMapping("{id}")
    public ResponseEntity deleteIngredient(@PathVariable long id) {
    }
}
```

### La persistence des données
La persistence des données est réalisée avec JPA (_Java Persistence Architecture_) et une base de données H2. Elle repose sur deux éléments :

- La classe `Ingredient` est un JavaBean annoté avec JPA de manière à ce que ses propriétés correspondent aux colonnes de la table de la base de données (en utilisant essentiellemnet les valeurs par défaut de JPA. Tout est paramétrable avec des annotations JPA pour aller plus loin). J'ai ajouté des annotations pour identifier la clé primaire et poser des contraintes sur la propriété `name` :

``` java
@Entity
public class Ingredient {
    @Id @GeneratedValue
    private long id;
    @NotNull @Column(unique = true)
    private String name;

}
```

- Le composant `IngredientRepository` assure l'accès à la base de données et fournit les opérations de manipulation. Ici, on se contente d'hériter de [`JpaRepository`](https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/JpaRepository.html) qui nous fournit les méthodes essentielles. Comme on le voit ci-après, on défini l'entité gérée ainsi que le type de la clé primaire :

``` java
public interface IngredientRepository extends JpaRepository<Ingredient, Long> {
}
```

### L'auto-configuration
Spring boot facilite l'auto-configuration et l'injection des composants sur la base d'annotation des classes. Voici quelques éléments mis en place :

- La classe AppConfig sert de point d'entrée pour la configuration (annotation `@Configuration`) et permet d'indiquer le(s) paquatage(s) à scanner pour trouver les composants à instancier et injecter (annotation `@ComponentScan`).

``` java
@Configuration
@ComponentScan("fr.ulille.iut.pizzaland")
public class AppConfig {}
```

- Les composants sont des classes qui peuvent être découvertes, instanciées et injectés par Spring. Afin d'être reconnue, ces composants doivent être annotés avec `@Component` ou avec une annotation plus spécifique qui précise sont rôle dans l'architecture Spring :

``` java
@Service
public class IngredientService {}

@Repository
public interface IngredientRepository extends JpaRepository<Ingredient, Long> {}

@ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {}
```

Les composants découverts par Spring peuvent ainsi être instanciés et injectés dans d'autres classes grâce à l'annotation `@Autowired` comme dans les exemples ci-dessous :

``` java
public class IngredientController {
    private IngredientService ingredientService;

    @Autowired
    IngredientController(IngredientService service) {
        this.ingredientService = service;
    }

}

public class IngredientService {
    private IngredientRepository ingredientRepository;

    @Autowired
    public IngredientService(final IngredientRepository ingredientRepository) {
        this.ingredientRepository = ingredientRepository;
    }

```

### Gestion des codes de retour et des erreurs pour les opérations REST
Par défaut Spring renvoie des codes 200/400/500 sans rentrer dans le détail des différents codes de retour possibles. Voici quelques éléments mis en place pour aller plus loin :

#### Gestion du POST
Lors d'une création, la convention est de renvoyer `201 Created` et un entête HTTP `location` avec l'URI de la nouvelle ressource. Voici le code permettant de faire cela :

``` java
    @PostMapping
    public ResponseEntity<Ingredient> createIngredient(@RequestBody @Valid Ingredient ingredient) {
        Ingredient created = ingredientService.createIngredient(ingredient);
        URI location = ServletUriComponentsBuilder.fromCurrentRequestUri().path("/" + created.getId())
                .build().toUri();
        
        return ResponseEntity.created(location).body(created);
    }
```

On peut par ailleurs noter l'annotation `@RequestBody` qui permet d'associer l'objet JSON envoyé par le client avec notre classe `Ingredient` qui sert ici de DTO.

#### Gestion des codes de retour en cas d'erreur : données invalides
 La gestion de la soumission de données invalides repose sur plusieurs points :

- L'utilisation de l'annotation `@Valid` déclenche ici la validation des propriété du bean ingrédient. Cette annotation n'est pas spécifique à Spring mais vient du [standard de validation de java](https://beanvalidation.org/). Cela utilise des annotations qui sont sur la classe `Ingredient`, ici l'annotation `@NotNull` pour la propriété `name`.

``` java
    @NotNull
    private String name;
```

Le non respect des contraintes levera une exception `ConstraintViolationException`.

- Par ailleurs, sur la classe `Ingredient`, nous avons appliqué une contrainte sur l'unicité du champ `name`. Cette contrainte est une contrainte JPA qui sera traitée par la couche de persistence :

``` java
    @Column(unique = true)
    private String name;
```

En cas de violation de cette contrainte, c'est donc la couche de persitence qui va lever une exception.

- Nous devons indiquer à Spring comment réagir à ces exceptions. Pour cela, nous utilisons une classe annotée avec [`@ControllerAdvice`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RestControllerAdvice.html) et qui hérite de [`ResponseEntityExceptionHandler`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.html) dont les méthodes permettent de gérer différents types d'exceptions.

Dans l'extrait de code ci-dessous, l'annotation `@ExceptionHandler` permet de spécifier l'exception associée à la méthode de traitement dans le cas des exception qui ne sont pas directement liées à la gestion de  la requête REST par Spring :

``` java
@ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
    @ExceptionHandler(DuplicateIngredientException.class)
    public void handleDuplicateIngredientError(HttpServletResponse response) throws IOException {
        response.sendError(HttpStatus.NOT_ACCEPTABLE.value());
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public void constraintViolationException(HttpServletResponse response) throws IOException {
        response.sendError(HttpStatus.NOT_ACCEPTABLE.value());
    }
```

  La première méthode repose sur une exception applicative (`DuplicateIngredientException`) levée par la classe `IngredientService`.

Dans le cas de données non valides (ici, pas de nom) Spring lève directement une exception sans aucune gestion dans notre code. Nous utilisons alors une des méthodes prédéfinies de la classe `ResponseEntityExceptionHandler` (qui ici récupère les éléments de la réponse générée par Spring et fixe le code d'erreur HTTP) :

``` java
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
                                                                  HttpHeaders headers,
                                                                  HttpStatus status, WebRequest request) {

    }
```

#### Gestion des codes de retour en cas d'erreur : not found
Yvan Peter's avatar
Yvan Peter committed
Dans le code ci-dessous, la méthode de recherche renvoie un container [`Optional<T>`](https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html) apparu dans Java 8 qui peut contenir un résultat ou une valeur nulle est qui fournit des méthodes pour traiter cela :

``` java
    @GetMapping("{id}")
    public Ingredient getOneIngredient(@PathVariable Long id) {
        Optional<Ingredient> result = ingredientService.getOne(id);

        return result.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "ingredient non trouvé"));
    }
```

Dans le cas du _delete_ qui ne renvoie rien, on s'attend plutôt à la levée d'une exception de la couche de persistence si l'identifiant n'existe pas :

``` java
    @DeleteMapping("{id}")
    public ResponseEntity deleteIngredient(@PathVariable long id) {
        try {
            ingredientService.delete(id);
        }
        catch ( Exception ex ) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "l'ingrédient n'existe pas");
        }
        return ResponseEntity.noContent().build();
    }
```