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 qui permet de définir les paquetages et surtout les dépendances du projet.
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 :
- 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...).
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
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 :
@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
:
@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 deJpaRepository
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 :
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
).
@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 :
@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 :
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 :
@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. Cela utilise des annotations qui sont sur la classeIngredient
, ici l'annotation@NotNull
pour la propriéténame
.
@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 champname
. Cette contrainte est une contrainte JPA qui sera traitée par la couche de persistence :
@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
et qui hérite deResponseEntityExceptionHandler
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 :
@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) :
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status, WebRequest request) {
}
Gestion des codes de retour en cas d'erreur : not found
Dans le code ci-dessous, la méthode de recherche renvoie un container Optional<T>
apparu dans Java 8 qui peut contenir un résultat ou une valeur nulle est qui fournit des méthodes pour traiter cela :
@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 :
@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();
}