IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Tutoriel pour apprendre à développer les Microcservices REST avec Spring Boot et Spring RestTemplate

Projet Spring Boot Image non disponible


précédentsommairesuivant

I. Première partie : Le serveur

Il est question dans cette partie de développer un service REST, et ensuite de le consommer dans la seconde partie de ce tutoriel.

I-A. Introduction

La plupart des applications d'entreprises sont des applications client/serveur. Pour développer ces applications, les deux solutions les plus dominantes sur le marché sont les web services REST (Representational State Transfer) et web services SOAP (Simple Object Access Protocol). SOAP est un protocole très normé et plus ancien que REST. L'objectif de cet article n'est pas de faire une étude comparée des deux technologies, mais avant de passer des heures à choisir entre SOAP et REST, il faut prendre en compte le fait que certains services web supportent l'un et pas l'autre. Par exemple, SOAP fonctionne en environnement distribué (Point à multipoint) alors que REST nécessite une communication directe point à point, basée sur l'utilisation des URI. De plus en plus d'entreprises préfèrent les services REST pour leur simplicité de mise en œuvre, et c'est l'une des raisons pour lesquelles l'idée m'est venue de rédiger cet article et surtout de montrer comment mettre en œuvre une architecture service REST-API. Pour plus d'informations sur la technologie SOAP et son implémentation, je vous conseille d'excellents tutoriels de Mickael Baron.

À la fin de cet article, vous devrez être capable de développer des web services REST en utilisant Spring Boot, savoir automatiser les tests unitaires et les tests d'intégration, être capable de mieux gérer les exceptions dans une application utilisant l'architecture Rest-API et le tout en Java8.

Pour ceux qui ont déjà les connaissances avancées sur les services REST, vous pouvez passer directement à la pratique.Création de l'application

Toutes les sources (client et serveur) sont disponibles en téléchargementTutoSpringBoot_et_REST.zip

I-A-1. Cahier des charges

Il s'agit de développer un portail web d'inscription et de connexion en utilisant les services RESTFul côté serveur et Spring RestTemplate côté client.

Le serveur sera développé dans une application à part et le client dans une autre. Je vais ensuite mettre en relation les deux services grâce à l'URL du serveur et le Framework Spring RestTemplate. Ce choix d'architecture permet de bien mettre en évidence la séparation des services REST et leur consommation par les clients.

Dans cette partie, il n'y aura aucun fichier JSP (Java Server Page) à écrire. Ça se fera côté client. Mais, je peux déjà vous montrer à quoi va ressembler l'écran d'accueil de l'application à travers l'image ci-dessous :

Voici à quoi ressemblera la page de connexion.

Ecran de connexion
Écran de création du compte et de connexion

I-A-2. Technologies utilisées

Technologies à implémenter dans ce projet

  • Spring Boot-2.2.6-RELEASE.
  • Java-1.8.
  • Frameworks : Maven, SpringMVC, Spring-RestTemplate, Mockito, JSON, Boomerang.
  • Base de données embarquée: H2.
  • Tomcat-9x (embarqué). Vous pouvez aussi utiliser Tomcat-8.
  • IDE : Eclipse-03-2019.
  • cargo-maven2-plugin pour le déploiement et l'automatisation des tests d'intégration.
  • La plupart des technologies sont embarquées par Spring Boot.

I-A-3. Pourquoi utiliser Spring Boot ?

Définition : Spring est un Framework de développement d'applications Java, qui apporte plusieurs fonctionnalités comme Spring Security, SpringMVC, Spring Batch, Spring Ioc, Spring Data, etc. Ces Frameworks ont pour objectif de faciliter la tâche aux développeurs. Malheureusement, leurs mises en œuvre deviennent très complexes à travers les fichiers de configuration XML qui ne cessent de grossir, et une gestion des dépendances fastidieuse. C'est pour répondre à cette inquiétude que le projet Spring Boot a vu le jour.

Définition : Spring Boot est un sous projet de Spring qui vise à rendre Spring plus facile d'utilisation en élimant plusieurs étapes de configuration. L'objectif de Spring Boot est de permettre aux développeurs de se concentrer sur des tâches techniques et non des tâches de configurations, de déploiements, etc. Ce qui a pour conséquences un gain de temps et de productivité (avec Spring Boot, il est très facile de démarrer un projet n-tiers).

Spring Boot apporte à Spring une très grande simplicité d'utilisation :

  1. Il facilite notamment la création, la configuration et le déploiement d'une application complète. On n'a plus besoin des fichiers XML à configurer (pas besoin du fichier du descripteur de déploiement web.xml dans le cas d'une application web). Nous verrons plus bas comment cela est possible.
  2. Spring Boot permet de déployer très facilement une application dans plusieurs environnements sans avoir à écrire des scripts. Pour ce faire, une simple indication de l'environnement (développement ou production) dans le fichier de propriétés (.properties) suffit à déployer l'application dans l'un ou l'autre environnement. Ceci est rendu possible grâce à la notion de profil à déclarer toujours dans le fichier de propriétés. Je vous présenterai des exemples de cas d'utilisation.
  3. Spring Boot possède un serveur d'application Tomcat embarqué afin de faciliter le déploiement d'une application web. Il est possible d'utiliser un serveur autre ou externe, grâce à une simple déclaration dans le fichier pom.xml.
  4. Spring Boot permet de mettre en place un suivi métrique de l'application une fois déployée sur le serveur afin de suivre en temps réel l'activité du serveur, ceci grâce à spring-boot-starter-actuator.
  5. Un guide complet de l'utilisation de spring Boot est disponible ici.Forum Spring Boot

I-A-4. Présentation des services REST

Les services REST représentent un style d'architecture pour développer des services web. Une API qui respecte les principes REST est appelée API-RESTful.

Les principes clés de REST impliquent la séparation de l'API en ressources logiques. Ce qui revient à penser à comment obtenir chaque ressource.

Une ressource est un objet ou une représentation d'objets contenant éventuellement des données. Exemple : un employé d'une société est une ressource. La manipulation de ces ressources repose sur le protocole HTTP à travers les méthodes d'actions GET, POST, PUT, PATCH, DELETE.

Pour obtenir une ressource, il faut déjà l'identifier à travers une URL et bien la nommer.

Une URL (Uniform Ressource Locator) indique le chemin vers une ressource. Cette ressource n'est pas toujours disponible. Lorsque l'URL pointe vers une ressource disponible, on parle d'une URI.

Une URI (Uniform Ressource Identifier) est l'identifiant unique de ressource sur un réseau (URI = URL + Ressource). Une URI est donc une URL qui pointe vers une ressource bien identifiée.

Dans l'implémentation des services REST, une bonne identification de la ressource est l'utilisation des noms et non des verbes. Par exemple, pour notre portail de connexion, j'aurai besoin d'une ressource utilisateur nommée User et d'une URL qui pointe vers cette ressource (l'ensemble formera l'URI de la ressource User), ce qui donne par exemple :

http://localhost:8080/springboot-restserver/user

Une fois que la ressource est identifiée, on a besoin de la manipuler. Par exemple la mettre à jour, supprimer, ou en créer une autre.

Pour ce faire on utilise les méthodes HTTP suivantes :

  • GET /users -- extraction de tous les utilisateurs.
  • GET /users/1 -- extraction de l'utilisateur ayant l'identifiant 1.
  • DELETE /users/1 -- suppression de l'utilisateur 1.
  • POST /users -- création d'un nouvel utilisateur.
  • PUT /users/1 -- mise à jour de l'utilisateur 1.
  • PATCH /users/1 -- mise à jour partielle de l'utilisateur 1.

Il est conseillé d'utiliser les noms au pluriel (users et non user) pour un bon nommage, qui n'est certes pas obligatoire, mais apporte une certaine lisibilité entre le mapping de l'URL et la ressource.

I-B. Création de l'application

Dans cette partie, je vais créer l'architecture de base de l'application à partir Spring Boot.

I-B-1. Création de l'architecture

Pour créer l'application, vous avez plusieurs possibilités:

C'est cette deuxième option que nous allons utiliser. L'avantage, c'est qu'on peut choisir très aisément toutes les dépendances directement.

Pour voir toutes les dépendances proposées par Spring Boot, déroulez le menu en cliquant sur: "Switch to the full version". Vous pourrez aussi changer de version Spring Boot après génération, car Spring Boot ne propose pas la possibilité de choisir les versions antérieures lors de la génération.

Voici la capture web de génération basique d'un projet web avec Spring Boot :

Archetype Spring Boot
Génération du squelette projet avec Spring Boot

Avant la génération du projet, prenez soin de sélectionner les trois dépendances principales comme le montre la capture ci-dessous:

  • Spring Web : pour tout ce qui est projet web (Spring Boot va ramener toutes les dépendances nécessaires pour une application web).
  • Spring Data JPA : pour la couche de persistance.
  • H2 Database : pour la base de données.

J'ai directement choisi un packaging war puisque notre application sera une application web et va tourner sur un serveur d'application Tomcat.

Comme on peut le constater dans la capture, l'application sera développée en Java8.

Pour importer le projet dans Eclipse, décompressez-le et suivez la démarche ci-dessous:

Fichier --> Import --> Maven --> Existing Maven Projects --> Indiquez le répertoire du pom.xml --> Finish

I-B-1-a. Configuration et exécution

Présentation du contenu du fichier pom.xml

L'élément central dans un projet Maven est le fichier pom.xml. Ci-dessous le fichier pom.xml généré :

contenu du fichier pom.xml généré
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.bnguimgo</groupId>
    <artifactId>springboot-restserver</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>springboot-restserver</name>
    <description>Serveur Rest utilisant le framework Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <start-class>com.bnguimgo.restclient.com.bnguimgo.springbootrestserver.SpringbootRestserverApplication</start-class><!-- cette déclaration est optionnelle  -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Quelques explications :

La dépendance spring-boot-starter-parent permet de rapatrier la plupart des dépendances du projet. Sans elle, le fichier pom.xml serait plus complexe.

La dépendance spring-boot-starter-web indique à Spring Boot qu'il s'agit d'une application web, ce qui permet à Spring Boot de rapatrier les dépendances comme SpringMVC, SpringContext, et même le serveur d'application Tomcat, etc.

Vous avez aussi constaté la présence de la dépendance spring-boot-starter-tomcat qui n'est pas obligatoire, mais comme il s'agit d'une application web, Spring Boot par anticipation intègre un serveur d'application Tomcat afin de faciliter le déploiement de l'application. Cette dépendance n'est pas obligatoire, vous pouvez la supprimer si jamais spring l'a embarqué. Dans notre cas, on va utiliser un serveur Tomcat externe à travers une configuration Eclipse.

Il faut noter aussi l'absence des versions dans les dépendances. Ceci est dû au fait que Spring Boot gère de manière très autonome les versions et nous n'avons plus besoin de les déclarer.

Enfin, j'ai ajouté la déclaration ci-dessous qui permet de spécifier le point d'entrée de l'application. Cette déclaration est optionnelle, car Spring Boot est capable de rechercher tout seul la classe contenant la méthode main(String[] args).

Spécification optionnelle du point d'entrée de l'application
Sélectionnez
<start-class>com.bnguimgo.restclient.com.bnguimgo.springbootrestserver.SpringbootRestserverApplication</start-class>

Pour utiliser un serveur d'application différent, ou un serveur d'application externe, il faut commenter la dépendance Tomcat et déclarer juste la version cible dans la partie de déclaration des propriétés comme ci-dessous :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <tomcat.version>9.0.24</tomcat.version><!-remplace la version embarquée de Tomcat -->
    </properties>

La dépendance spring-boot-starter-data-jpa est nécessaire pour le développement de la couche de persistance. Les autres dépendances seront ajoutées notamment pour les besoins de tests.

I-B-1-b. Structure de l'application

Ci-dessous la structure du projet généré :

Il n'est pas encore possible de tester l'application, car il manque un contrôleur pour envoyer ce qu'il faut afficher.

application générée
Structure de l'application créée par génération

Il faut relever particulièrement l'absence du fichier de descripteur du déploiement web.xml dont on a plus besoin dans une application Spring Boot. Voir ci-dessous les raisons.

Le point d'entrée de l'application est la classe SpringbootRestserverApplication:

Point d'entrée de l'application
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
package com.bnguimgo.springbootrestserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringbootRestserverApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootRestserverApplication.class, args);
    }
}

L'application est initialisée par la classe ServletInitializer:

Classe d'initialisation de l'application
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
package com.bnguimgo.springbootrestserver;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(SpringbootRestserverApplication.class);
    }
}

Dans le package principal com.bnguimgo.springbootrestserver, nous avons les deux classes ServletInitializer et SpringbootRestserverApplication. Vous pouvez nommer ces classes comme vous voulez.

La classe ServletInitializer permet l'initialisation de l'application. Elle étend la classe SpringBootServletInitializer qui est à l'origine de cette initialisation et remplace ainsi l'ancien fichier web.xml.

La classe SpringbootRestserverApplication contient la méthode void main(String[] args) nécessaire dans une application Spring Boot, et permet l'exécution de celle-ci : c'est le point d'entrée de l'application.

La classe de démarrage de l'application SpringbootRestserverApplication doit être à la racine du package principal si on veut permettre à Spring de scanner les sous-packages en utilisant l'annotation @SpringBootApplication.

L'annotation @SpringBootApplication est centrale dans une application Spring Boot et permet de scanner le package courant et ses sous-packages. Elle existe depuis SpringBoot-1.2.0 et est équivalente à l'ensemble des annotations @Configuration, @EnableAutoConfiguration et @ComponentScan

Voici les avantages qu'apporte cette annotation :

  • Elle remplace l'annotation @Configuration qui permet de configurer une classe comme une source de définition des beans Spring.
  • On aurait aussi pu ajouter l'annotation @EnableWebMvc pour indiquer qu'il s'agit d'une application SpringMVC, mais grâce à @SpringBootApplication, cette annotation n'est pas nécessaire, car Spring Boot va automatiquement l'ajouter dès qu'il verra que dans le workspace, il existe une bibliothèque spring-webmvc.
  • Elle remplace également l'annotation @ComponentScan qui autorise Spring à rechercher tous les composants, les configurations et autres services de l'application et à initialiser tous les contrôleurs.

Dans le dossier src/main/resources, il y a :

  • Le dossier static qui permet de stocker tous les fichiers images, CSS, bref tous les fichiers qui ne fournissent pas un contenu dynamique (son utilisation n'est pas obligatoire).
  • Le dossier templates qui permet de stocker des fichiers web si on utilise le Framework Thymeleaf de Spring (Thymeleaf ne sera pas utilisé dans le cadre de ce projet).
  • Le fichier application.properties qui nous sera très utile pour configurer le projet. Pour le moment, il est vide, car je vais d'abord utiliser les configurations par défaut de Spring Boot.
I-B-1-c. Création du service

Je vais créer un premier service REST qui permet juste de démarrer et tester l'application. Le service RESt complet sera développé dans le chapitre I-B-3Développement du serveur Rest. Voici un petit service qui teste si le serveur a bien démarré (mettre cette classe dans le même package que SpringbootRestserverApplication) :

Un mini service REST (RestServices) pour tester le démarrage de l'application
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
package com.bnguimgo.springbootrestserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class RestServices {
    
    private static final Logger logger = LoggerFactory.getLogger(RestServices.class);
    
    @GetMapping(value = "/")
    public ResponseEntity<String> pong() 
    {
        logger.info("Démarrage des services OK .....");
        return new ResponseEntity<String>("Réponse du serveur: "+HttpStatus.OK.name(), HttpStatus.OK);
    }

}

La classe est annotée @Controller afin de permettre à Spring d'enregistrer cette classe comme un contrôleur, et surtout de mémoriser les requêtes que cette classe est capable de gérer.

L'annotation @GetMapping(value = "/") est une nouvelle annotation introduite par Spring qui remplace l'annotation classique @RequestMapping et correspond exactement à @RequestMapping(method=RequestMethod.GET, value = "/").

J'ai intégré un logger sans faire aucune configuration et ça marche. Si vous regardez votre console, vous verrez bien les traces de logs lors du démarrage de l'application. C'est l'un des avantages de Spring Boot qui configure par défaut un logger pour l'application. Je vais vous montrer plus loin comment personnaliser le logger par défaut. Le service est prêt à être testé.

I-B-1-d. Test de la configuration

Le service sera testé avec un serveur d'application externe Tomcat. Il faut donc configurer ce serveur dans l'IDE Eclipse. Pour ajouter un nouveau serveur dans Eclipse :

Fichier --> Nouveau Others… --> Server --> Next --> Tomcat v9.0 Server --> Finish

Pour tester l'application dans Eclipse, après compilation : clic droit sur l'application --> Run As --> Run on Server --> Tomcat v8.0 Server at localhost --> Next --> Finish

Résultat dans Eclipse:

Test service REST
Test du service REST dans l'IDE Eclipse

Il est possible de tester directement sur un navigateur web ou un client REST. Voici le résultat dans le navigateur Mozilla:

Application démarrée
Test de l'application sur Mozilla

Maintenant que notre environnement est en place, on peut développer un service REST complet.

Je rappelle que dans cette partie, seul le serveur est développé, mais le client sera développé aussi dans la deuxième partie du tutoriel. Le travail à faire consiste à développer un service qui permet à un internaute de créer son compte et de se connecter à l'application, le tout en services REST.

I-B-2. Création des couches

Dans cette partie, je vais développer un service complet de gestion d'un utilisateur :

  • Extraction de tous les utilisateurs.
  • Extraction d'un utilisateur à partir de son login et mot de passe.
  • Création d'un utilisateur.
  • Mise à jour de l'utilisateur.
  • Suppression d'un utilisateur.
I-B-2-a. Création du model

Pour mettre en place le service d'extraction, il nous faut un objet utilisateur (User). Nous aimerions aussi connaître les rôles de chaque utilisateur, par exemple s'il est administrateur (ROLE_ADMIN) ou simple utilisateur (ROLE_USER) de l'application. Il faudra donc aussi créer un objet Role pour stocker ces informations. Sachant qu'un utilisateur peut avoir les deux rôles, cette relation donnera lieu à une nouvelle association ManyToMany. Voyons toute de suite comment implémenter.

Il faut créer un nouveau sous-package nommé model dans le package racine com.bnguimgo.springbootrestserver et ajouter dans le model les deux entités ci-dessous :

Création de l'entité User
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
package com.bnguimgo.springbootrestserver.model;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;

@Entity
@Table(name = "UTILISATEUR")
public class User implements Serializable{

	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "USER_ID", updatable = false, nullable = false)
    private Long id;

	@Column(name = "LOGIN", unique=true, insertable=true, updatable=true, nullable=false)
    private String login;
	
	@Column(name = "USER_PASSWORD", insertable=true, updatable=true, nullable=false)
    private String password;
	
	@Column(name = "USER_ACTIVE", insertable=true, updatable = true, nullable=false)
	private Integer active;
	
	@ManyToMany(cascade = CascadeType.DETACH)
	@JoinTable(name = "USER_ROLE", joinColumns = @JoinColumn(name = "USER_ID"), inverseJoinColumns = @JoinColumn(name = "ROLE_ID"))
	private Set<Role> roles= new HashSet<>();

    public User() {
    	super();
    }

    public User(String login, String password, Integer active) {
        this.login = login;
        this.password = password;
        this.active = active;
    }
    
	public User(Long id, String login, String password, Integer active) {
		this.id= id;
		this.login=login;
		this.password = password;
		this.active=active;
	}

	public User(String login, String password) {
		this.login=login;
		this.password = password;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getLogin() {
		return login;
	}
	public void setLogin(String login) {
		this.login = login;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}
	
	public Integer getActive() {
		return active;
	}
	public void setActive(Integer active) {
		this.active = active;
	}
	
	public Set<Role> getRoles() {
		return roles;
	}

	public void setRoles(Set<Role> roles) {
		this.roles = roles;
	}

	@Override
	public String toString() {
		return "User [id=" + id + ", login=" + login + ", password=XXXXXXX, active=" + active + ", roles="
				+ roles + "]";
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((active == null) ? 0 : active.hashCode());
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		result = prime * result + ((login == null) ? 0 : login.hashCode());
		result = prime * result + ((password == null) ? 0 : password.hashCode());
		result = prime * result + ((roles == null) ? 0 : roles.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		User other = (User) obj;
		if (active == null) {
			if (other.active != null)
				return false;
		} else if (!active.equals(other.active))
			return false;
		if (id == null) {
			if (other.id != null)
				return false;
		} else if (!id.equals(other.id))
			return false;
		if (login == null) {
			if (other.login != null)
				return false;
		} else if (!login.equals(other.login))
			return false;
		if (password == null) {
			if (other.password != null)
				return false;
		} else if (!password.equals(other.password))
			return false;
		if (roles == null) {
			if (other.roles != null)
				return false;
		} else if (!roles.equals(other.roles))
			return false;
		return true;
	}
}
Création de l'entité ROLE
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
package com.bnguimgo.springbootrestserver.model;
import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "ROLE")
public class Role implements Serializable{

	private static final long serialVersionUID = 2284252532274015507L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "ROLE_ID", updatable = false, nullable = false)
	private int id;
	
	@Column(name="ROLE_NAME", updatable = true, nullable = false)
	private String roleName;
	
	public Role(){
		super();
	}
	public Role(String roleName){
		super();
		this.roleName = roleName;
	}
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getRoleName() {
		return roleName;
	}
	public void setRoleName(String roleName) {
		this.roleName = roleName;
	}
	@Override
	public String toString() {
		return "Role [id=" + id + ", role=" + roleName + "]";
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + id;
		result = prime * result + ((roleName == null) ? 0 : roleName.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Role other = (Role) obj;
		if (id != other.id)
			return false;
		if (roleName == null) {
			if (other.roleName != null)
				return false;
		} else if (!roleName.equals(other.roleName))
			return false;
		return true;
	}
	
	public int compareTo(Role role){
		return this.roleName.compareTo(role.getRoleName());
		
	}
}

Il faut explicitement créer le constructeur par défaut Role sinon, il y a un risque d'avoir des problème de sérialisation/désérialisation.

L'annotation @Entity permet d'indiquer à l'ORM Hibernate que cette classe sera une table de la base de données et l'annotation @Table(name = "UTILISATEUR") permet de donner le nom UTILISATEUR à la table. Grâce à ces annotations, on n'a plus besoin du fichier de configuration persistence.xml.

@GeneratedValue(strategy = GenerationType.IDENTITY) ==> c'est la base de données qui va générer la clé primaire afin d'éviter les doublons, car la base contient déjà les données à l'initialisation

Il faut éviter de nommer ou de créer une table avec pour nom USER, car dans la plupart des bases de données, il y a déjà une table utilisateur nommée USER.

Une personne peut à la fois avoir le rôle administrateur et le rôle utilisateur. Plusieurs utilisateurs peuvent avoir le même rôle. On peut par exemple avoir plusieurs administrateurs : c'est ce qui explique l'existence de la relation ManyToMany. Au niveau de la base de données, cela donne naissance à une table de jointure que je vais nommer : USER_ROLE, à ne pas confondre avec la table ROLE qui stocke tous les rôles. On a ainsi terminé le model.

I-B-2-b. Création de la base de données

Dans cette section, on va initialiser la base de données par des scripts et se connecter dessus sans faire aucune déclaration ni aucune configuration de la datasource. C'est un des avantages de Spring Boot.

Il faut remarquer que jusqu'à présent, il n'y a eu aucune configuration concernant Hibernate pour le mapping objet relationnel. Le simple fait d'avoir déclaré une base de données H2 dans le pom.xml fait que Spring Boot configure HibernateEntityManager pour nous. Grâce aux annotations appliquées sur chaque objet model, Hibernate va transformer chaque entité ou association en une table et créer dans la foulée la base de données.

Voici le script de l'initialisation de la base de données nommée data.sql. Ce script doit être déposé dans le répertoire de gestion des ressources src/main/resources. Spring Boot va automatiquement l'utiliser pour initialiser la base de données au démarrage de l'application, pas besoin de déclarer une datasource.

Script d'initialisation de la base de données data.sql
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
--INITIALISATION TABLE ROLE
INSERT INTO ROLE(ROLE_ID,ROLE_NAME) VALUES (1,'ROLE_ADMIN');
INSERT INTO ROLE(ROLE_ID,ROLE_NAME) VALUES (2,'ROLE_USER');

--INITIALISATION TABLE UTILISATEURS
INSERT INTO UTILISATEUR(USER_ID, LOGIN, USER_PASSWORD, USER_ACTIVE) values (1, 'admin', 'admin', 1);
INSERT INTO UTILISATEUR(USER_ID, LOGIN, USER_PASSWORD, USER_ACTIVE) values (2, 'login2', 'user', 1);
INSERT INTO UTILISATEUR(USER_ID, LOGIN, USER_PASSWORD, USER_ACTIVE) values (3, 'login3', 'user1', 0);-- 0 signifie user inactif

-- TABLE DE JOINTURE
INSERT INTO USER_ROLE(USER_ID,ROLE_ID) VALUES (1,1);
INSERT INTO USER_ROLE(USER_ID,ROLE_ID) VALUES (1,2);
INSERT INTO USER_ROLE(USER_ID,ROLE_ID) VALUES (2,2);
INSERT INTO USER_ROLE(USER_ID,ROLE_ID) VALUES (3,2);

COMMIT;

Notez bien la table d'association (USER_ROLE) qui fait le lien ManyToMany décrit plus haut.

Nous allons hacher le mot de passe (USER_PASSWORD) en utilisant la classe Spring BCryptPasswordEncoder (voir son utilisation dans la partie service lors de la création d'un utilisateur) et le script d'initialisation de la base de données donnera le résultat ci-dessous:

Voici la classe utilitaire qui permet de créer des mots de passes hachés, juste pour initialiser la base de données

En réalité, nous n'auraons pas besoin de cette classe, car le hachage sera fait au moment de la création ou de la mise à jour de chaque utilisateur par le service.

Classe utilitaire pour hacher le mot de passe
Sélectionnez
package com.bnguimgo.springbootrestserver.util;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * Classe permettant de créer des mots de passe hachés pour des besoins de tests 
 *et d'initialisation de la base de données
 *
 */
public class PasswordEncoder {

	private static BCryptPasswordEncoder bCryptPasswordEncoder;

	public static void main(String[] args) {
		bCryptPasswordEncoder = new BCryptPasswordEncoder();
		String password ="password2";
		String encodedPassword = bCryptPasswordEncoder.encode(password);
		System.out.println("Mot de passe en clair : "+password);
		System.out.println("Mot de passe haché : "+encodedPassword);
		//Pour vérifier que le mot de passe haché correspond bien au mot de passe initial, il utiliser la méthode bCryptPasswordEncoder.matches(x, y)
		System.out.println("Le mot de passe est bien haché : "+bCryptPasswordEncoder.matches(password, encodedPassword));
	}

}
Script d'initialisation de la base de données data.sql
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
--INITIALISATION TABLE ROLE

INSERT INTO ROLE(ROLE_ID,ROLE_NAME) VALUES (1,'ROLE_ADMIN');
INSERT INTO ROLE(ROLE_ID,ROLE_NAME) VALUES (2,'ROLE_USER');

--INITIALISATION TABLE UTILISATEURS
--admin@admin.com/admin
--login2/password2
--login3/password3
INSERT INTO UTILISATEUR(USER_ID, LOGIN, USER_PASSWORD, USER_ACTIVE) values (1, 'admin@admin.com', '$2a$10$ISnv6T5sqpr5YeRKP01xEOLAr/ZWviCp73BC07hMK54GNgQHMemxm', 1);
INSERT INTO UTILISATEUR(USER_ID, LOGIN, USER_PASSWORD, USER_ACTIVE) values (2, 'login2', '$2a$10$5TLZBQgB/FOSkccGjKCRDerrD6YzsFznyNURwNHZG8tEwAumfFw1C', 1);
INSERT INTO UTILISATEUR(USER_ID, LOGIN, USER_PASSWORD, USER_ACTIVE) values (3, 'login3', '$2a$10$U2NARRA6lp0CSfDF2JEmtOaAbf3bVGx9zGqDbLkW/T59.QdGnlguO', 0);--inactif user

-- TABLE DE JOINTURE
INSERT INTO USER_ROLE(USER_ID,ROLE_ID) VALUES (1,1);
INSERT INTO USER_ROLE(USER_ID,ROLE_ID) VALUES (1,2);
INSERT INTO USER_ROLE(USER_ID,ROLE_ID) VALUES (2,2);
INSERT INTO USER_ROLE(USER_ID,ROLE_ID) VALUES (3,2);

COMMIT;

Pour le test de connexion il faudra utiliser le login et mot de passe =admin@admin.com/admin ou login2/password2.

I-B-2-c. Création de la couche DAO

Le développement de la couche DAO et même de la couche de services ne concerne pas spécifiquement les services REST. La partie spécifique aux services REST est mise en place au niveau des contrôleurs que nous allons voir plus loin.

La dépendance spring-boot-starter-data-jpa que nous avions ajoutée dans le pom.xml permet d'utiliser SpringData qui est une implémentation de JPA (Java Persistence Api). SpringData implémente toutes méthodes du CRUD (Create, Read, Update, Delete) quand on hérite de JpaRepository ou de CrudRepository.

Interface UserRepository (à créer dans un nouveau package nommé dao) :
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
package com.bnguimgo.springboot.rest.server.dao;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.bnguimgo.springbootrestserver.model.User;

public interface UserRepository extends JpaRepository<User, Long> {
	
	Optional<User> findByLogin(String loginParam);
}

On peut ajouter dans l'interface des méthodes qui ne font pas partie du CRUD. C'est le cas de la méthode findByLogin(String login)

Remarque : grâce à SpringData, pas besoin d'implémenter les méthodes createUser(User user), deleteUser(Long id), ni même la méthode findByLogin, etc. À l'exécution, SpringData va créer automatiquement une implémentation de cette interface, et par conséquent, pas besoin d'une classe d'implémentation nommée UserRepositoryImpl. Il faut par ailleurs respecter le nommage des propriétés, exemple: (login -->findByLogin, name -->findByName).

Voici l'interface Role
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
package com.bnguimgo.springbootrestserver.dao;
import java.util.stream.Stream;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import com.bnguimgo.springbootrestserver.model.Role;

public interface RoleRepository extends JpaRepository<Role, Long> {
    
    Role findByRoleName(String roleName);

    @Query("select role from Role role")
    Stream<Role> getAllRolesStream();// Java8 Stream : on place la liste des rôles dans un Stream
}
I-B-2-d. Couche de services
Interface UserService créée dans un nouveau package nommé service
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
package com.bnguimgo.springbootrestserver.service;
import java.util.Collection;
import java.util.Optional;

import com.bnguimgo.springbootrestserver.exception.BusinessResourceException;
import com.bnguimgo.springbootrestserver.model.User;

public interface UserService {

	Collection<User> getAllUsers();
	
	Optional<User> getUserById(Long id) throws BusinessResourceException;
	
	Optional<User> findByLogin(String login) throws BusinessResourceException;
	
	User saveOrUpdateUser(User user) throws BusinessResourceException;
	
	void deleteUser(Long id) throws BusinessResourceException;

	Optional<User> findByLoginAndPassword(String login, String password) throws BusinessResourceException;

}
Ajoutez l'interface RoleService dans le même package
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
package com.bnguimgo.springbootrestserver.service;
import java.util.Collection;
import java.util.stream.Stream;

import com.bnguimgo.springbootrestserver.model.Role;

public interface RoleService {
	
	Role findByRoleName(String roleName);
	
	Collection<Role> getAllRoles();
	
	Stream<Role> getAllRolesStream();
}

Les deux méthodes getAllRoles() et getAllRolesStream() font la même chose. Je veux juste montrer à travers ces deux méthodes comment Java8 permet d'implémenter différemment les collections.

Implémentation des services :

Le mot de passe utilisateur sera haché afin d'éviter de le stocker en clair dans la base de données. Pour le faire, je vais utiliser la dépendance spring-boot-starter-security que je vous invite à ajouter dans pom.xml. C'est cette dépendance qui va mettre à disposition la classe BCryptPasswordEncoder pour hacher le mot de passe à travers l'algorithme SHA-1

ajout de la dépendance spring-boot-starter-security
Sélectionnez
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Pour utiliser les Collections Apache, il faut ajouter la dépendance ci-dessous :

ajout de la dépendance apache commons-collections4
Sélectionnez
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.0</version>
</dependency>

Je vais créer aussi une classe de configuration des beans nommée BeanConfiguration qui va permettre de déclarer des beans. Par exemple le bean BCryptPasswordEncoder, ce qui permettra à spring de créer une instance de cet objet pour nous. Je vais ensuite utiliser cet objet pour hacher les mots de passe lors de la persistance d'un objet.

Création de la classe BeanConfiguration dans un nouveau package component
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
package com.bnguimgo.springbootrestserver.component;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class BeanConfiguration extends WebMvcConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        return bCryptPasswordEncoder;
    }

}

L'annotation @Configuration indique à Spring que cette classe est une source de configuration des beans. Dans cette classe, j'ai déclaré un seul bean avec l'annotation @Bean. On pourra ainsi créer une instance de BCryptPasswordEncoder en appliquant l'annotation @Autowired de Spring partout où on en a besoin.

lmplémentation de l'interface UserService avec hachage du mot de passe utilisateur
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
package com.bnguimgo.springbootrestserver.service;

import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.collections4.IteratorUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.bnguimgo.springbootrestserver.dao.RoleRepository;
import com.bnguimgo.springbootrestserver.dao.UserRepository;
import com.bnguimgo.springbootrestserver.exception.BusinessResourceException;
import com.bnguimgo.springbootrestserver.model.Role;
import com.bnguimgo.springbootrestserver.model.User;

@Service
public class UserServiceImpl implements UserService {
	
	private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
	private UserRepository userRepository;
	private RoleRepository roleRepository;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
	
	public UserServiceImpl() {
		super();
	}
	
	@Autowired
	public UserServiceImpl(UserRepository userRepository, RoleRepository roleRepository, BCryptPasswordEncoder bCryptPasswordEncoder) {
		super();
		this.userRepository = userRepository;
		this.roleRepository = roleRepository;
		this.bCryptPasswordEncoder = bCryptPasswordEncoder;
	}
	
	@Override
	public Optional<User> findByLogin(String login) throws BusinessResourceException {
		
		Optional<User> userFound = userRepository.findByLogin(login);
        if (Boolean.FALSE.equals(userFound.isPresent())) {
            throw new BusinessResourceException("User Not Found", "L'utilisateur avec ce login n'existe pas :" + login);
        }
		return userFound;
	}

	@Override
	public Collection<User> getAllUsers() {
		return IteratorUtils.toList(userRepository.findAll().iterator());
	}
	
	@Override
	public Optional<User> findUserById(Long id) throws  BusinessResourceException{
		
		Optional<User> userFound = userRepository.findById(id);
        if (Boolean.FALSE.equals(userFound.isPresent())){
            throw new BusinessResourceException("User Not Found", "Aucun utilisateur avec l'identifiant :" + id);
        }
		return userFound;
	}

	@Override
	@Transactional(readOnly=false)
	public User saveOrUpdateUser(User user) throws BusinessResourceException{
		try{
			if(null ==user.getId()) {//pas d'Id --> création d'un user
				addUserRole(user);//Ajout d'un rôle par défaut
				user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
			} else {//sinon, mise à jour d'un user
				
				Optional<User> userFromDB = findUserById(user.getId());
				if(! bCryptPasswordEncoder.matches(user.getPassword(), userFromDB.get().getPassword())) {
					user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));//MAJ du mot de passe s'il a été modifié
				} else {
					
					user.setPassword(userFromDB.get().getPassword());//Sinon, on remet le password déjà haché
				}
				updateUserRole(user);//On extrait le rôle en cas de mise à jour
			}
			User result = userRepository.save(user);
			return  result;
		} catch(DataIntegrityViolationException ex){
			logger.error("Utilisateur non existant", ex);
			throw new BusinessResourceException("DuplicateValueError", "Un utilisateur existe déjà avec le compte : "+user.getLogin(), HttpStatus.CONFLICT);
		} catch (BusinessResourceException e) {
			logger.error("Utilisateur non existant", e);
			throw new BusinessResourceException("UserNotFound", "Aucun utilisateur avec l'identifiant: "+user.getId(), HttpStatus.NOT_FOUND);
		} catch(Exception ex){
			logger.error("Erreur technique de création ou de mise à jour de l'utilisateur", ex);
			throw new BusinessResourceException("SaveOrUpdateUserError", "Erreur technique de création ou de mise à jour de l'utilisateur: "+user.getLogin(), HttpStatus.INTERNAL_SERVER_ERROR);
		}
	}

	@Override
	@Transactional(readOnly=false)
	public void deleteUser(Long id) throws BusinessResourceException {
		try{
			userRepository.deleteById(id);
		}catch(EmptyResultDataAccessException ex){
			logger.error(String.format("Aucun utilisateur n'existe avec l'identifiant: "+id, ex));
			throw new BusinessResourceException("DeleteUserError", "Erreur de suppression de l'utilisateur avec l'identifiant: "+id, HttpStatus.NOT_FOUND);
		}catch(Exception ex){
			throw new BusinessResourceException("DeleteUserError", "Erreur de suppression de l'utilisateur avec l'identifiant: "+id, HttpStatus.INTERNAL_SERVER_ERROR);
		}		
	}

	@Override
	public Optional<User> findByLoginAndPassword(String login, String password) throws BusinessResourceException{
		try {
			Optional<User> userFound = this.findByLogin(login);
			if(bCryptPasswordEncoder.matches(password, userFound.get().getPassword())) {
				return userFound;
			} else {
				throw new BusinessResourceException("UserNotFound", "Mot de passe incorrect", HttpStatus.NOT_FOUND);
			}
		} catch (BusinessResourceException ex) {
			logger.error("Login ou mot de passe incorrect", ex);
			throw new BusinessResourceException("UserNotFound", "Login ou mot de passe incorrect", HttpStatus.NOT_FOUND);
		}catch (Exception ex) {
			logger.error("Une erreur technique est survenue", ex);
			throw new BusinessResourceException("TechnicalError", "Une erreur technique est survenue", HttpStatus.INTERNAL_SERVER_ERROR);
		}
	}
	
	private void addUserRole(User user) {
		Set<Role> roles= new HashSet<>();
		Role roleUser = new Role("ROLE_USER");//initialisation du rôle ROLE_USER
		roles.add(roleUser);
		user.setActive(0);

		Set<Role> roleFromDB = extractRole_Java8(roles, roleRepository.getAllRolesStream());
		user.setRoles(roleFromDB);
	}
	
	private void updateUserRole(User user) {

		Set<Role> roleFromDB = extractRole_Java8(user.getRoles(), roleRepository.getAllRolesStream());
		user.setRoles(roleFromDB);
	}
	
 	private Set<Role> extractRole_Java8(Set<Role> rolesSetFromUser, Stream<Role> roleStreamFromDB) { 		
 	// Collect UI role names
 		Set<String> uiRoleNames = rolesSetFromUser.stream()
 		    .map(Role::getRoleName)
 		    .collect(Collectors.toCollection(HashSet::new));
 	// Filter DB roles
		return roleStreamFromDB
			.filter(role -> uiRoleNames.contains(role.getRoleName()))
		    .collect(Collectors.toSet());
	}

	@SuppressWarnings("unused")
	private Set<Role> extractRoleUsingCompareTo_Java8(Set<Role> rolesSetFromUser, Stream<Role> roleStreamFromDB) {
		return roleStreamFromDB
			.filter(roleFromDB -> rolesSetFromUser.stream()
			.anyMatch( roleFromUser -> roleFromUser.compareTo(roleFromDB) == 0))
	        .collect(Collectors.toCollection(HashSet::new));
	}

	@SuppressWarnings("unused")
	private Set<Role>  extractRole_BeforeJava8(Set<Role> rolesSetFromUser, Collection<Role> rolesFromDB) {
		Set<Role> rolesToAdd = new HashSet<>();
		for(Role roleFromUser:rolesSetFromUser){
			for(Role roleFromDB:rolesFromDB){
				if(roleFromDB.compareTo(roleFromUser)==0){
					rolesToAdd.add(roleFromDB);
					break;
				}
			}
		}
		return rolesToAdd;
	}
}

Il faut noter l'injection des dépendances par l'annotation @Autowired des objets (userRepository, roleRepository, bCryptPasswordEncoder) créées par Spring. Les dépendances userRepository et roleRepository permettent d'avoir accès aux services de la couche DAO (services fournis par l'implémentation Spring de JpaRepository). L'objet bCryptPasswordEncoder permet de hacher les mots de passe pour ne pas les stocker en clair dans la base de données.

Les classes de gestion d'exceptions (BusinessResourceException) et autres seront présentées dans un chapitre dédié à la gestion des exceptionsGestion des exceptions.

Notez la présence de l'@Autowired plutôt sur le constructeur, ce qui va faciliter le développement des tests unitaires sans avoir besoin d'injecter les dépendances ou d'utiliser SpringRunner.class
La méthode saveOrUpdateUser assure à la fois la création et la mise à jour de l'utilisateur. En cas de création, on hache directement le mot de passe avec bCryptPasswordEncoder.encode(xxx). Sinon, on vérifie si le mot de passe a été modifié par bCryptPasswordEncoder.matches(xxx), si oui, on hache aussi le nouveau mot de passe, sinon, on garde le mot de passe haché récupéré de la base.

J'ai ajouté quelques méthodes utilitaires en Java8 et avant Java8 juste pour mieux montrer la différence dans l'utilisation des Collections entre les versions de Java. Ces trois méthodes font strictement la même chose : récupérer les rôles utilisateurs de la base de données. Car en principe, je ne crée pas de rôles directement, mais je fais une requête dans la base de données pour récupérer le rôle à partir de roleName fourni par le User.

La méthode extractRole_Java8() est écrite exclusivement en Java8 et utilise la classe Stream et les filtres pour extraire les rôles utilisateurs. La méthode extractRoleUsingCompareTo_Java8() est pareille que la méthode ci-dessus, mais utilise la méthode de comparaison compareTo() de la classe Role. La méthode extractRole_BeforeJava8() est la méthode classique de parcours d'une Collection.

Chaque utilisateur est crée avec un rôle par défaut, d'où la méthode utilitaire private void addUserRole(User user) { xxx}.

Implémentation de l'interface RoleService pour gérer les rôles utilisateurs
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
package com.bnguimgo.springbootrestserver.service;
import java.util.Collection;
import java.util.stream.Stream;

import org.apache.commons.collections4.IteratorUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.bnguimgo.springbootrestserver.dao.RoleRepository;
import com.bnguimgo.springbootrestserver.model.Role;


@Service
public class RoleServiceImpl implements RoleService {
	
	
	private RoleRepository roleRepository;
	
	public RoleServiceImpl() {
		super();
	}
	@Autowired //autowired par constructeur pour bénéficier des tests unitaires
	public RoleServiceImpl(RoleRepository roleRepository) {
		super();
		this.roleRepository = roleRepository;
	}

	@Override
	public Collection<Role> getAllRoles() {
		return IteratorUtils.toList(roleRepository.findAll().iterator());
	}
	
	@Override
	public Stream<Role> getAllRolesStream() {
		return roleRepository.getAllRolesStream();
	}
	
	@Override
	public Role findByRoleName(String roleName) {
		return roleRepository.findByRoleName(roleName);
	}
}

Notez bien l'implémentation différente des deux méthodes qui renvoient les collections avec getAllRoles() et le Stream avec getAllRolesStream().

I-B-3. Mise en place du service REST

Il s'agit de développer réellement notre service REST, cette partie représente la couche de contrôle. C'est cette couche qui intercepte et filtre toutes les requêtes utilisateurs. Chaque contrôleur dispose d'un service pour traiter les requêtes: c'est le service REST.

I-B-3-a. Création du filtre Cross Domain

Avant de développer notre service REST, je vous propose de créer la classe utilitaire CrossDomainFilter qui a pour objectif de corriger les problèmes liés au Cross-Domain. En fait, comme le client et le serveur peuvent être hébergés sur deux serveurs distants, il faut penser aux problèmes réseaux qui peuvent entraver la communication. Il faut indiquer au serveur quels sont les types d'en-têtes HTTP à prendre en considération. Plus d'explications sur les filtres Cross-Domain ici:

Classe utilitaire nommée CrossDomainFilter à ajouter dans le package component
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
package com.bnguimgo.springbootrestserver.component;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
 
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
 
@Component
public class CrossDomainFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain)
            throws ServletException, IOException {
        httpServletResponse.addHeader("Access-Control-Allow-Origin", "*"); //toutes les URI sont autorisées
           httpServletResponse.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
           httpServletResponse.addHeader("Access-Control-Allow-Headers", "origin, content-type, accept, x-req");           
           filterChain.doFilter(httpServletRequest, httpServletResponse);        
    }
}

Grâce à l'annotation @Component, Spring va considérer cette classe comme un composant et pourra utiliser le filtre sans problème.

Une dernière configuration concerne les fichiers application.properties. J'ai mis à disposition trois fichiers de configuration :

  • un fichier application-dev.properties à utiliser en phase de développement.
  • un fichier application-prod.properties à utiliser en phase de production.
  • un fichier application.properties pour initier toutes les configurations de base.

En ajoutant par exemple spring.profiles.active=dev dans application.properties, Spring Boot sait que c'est le profil dev qui est activé et ce sont les fichiers de configuration application-dev.properties et application.properties qui seront chargés. Je vous invite à regarder la configuration de chaque fichier.

Pour désactiver la demande récurrente du mot de passe généré par Spring security en phase de développement, il faut ajouter cette déclaration security.basic.enabled=false dans le fichier application.properties.

Voici le contenu de chacun de ces trois fichiers:

Le fichier application.properties est le fichier de configuration principale

application.properties
Sélectionnez
1.
2.
3.
4.
5.
6.
#spring.profiles.active=dev
spring.profiles.active=prod
security.basic.enabled=false

spring.h2.console.enabled=true
spring.h2.console.path=/console

On constate bien que c'est le profil de production (prod) qui est activé, ce qui veut dire que c'est le fichier de propriétés application-prod.properties qui sera chargé. C'est ce profil que nous allons utiliser dans la suite.

application-prod.properties
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
#Charge les proprietes de la production
nextpage.message=Salut Vous etes en production
error.no.user.id = Aucun utilisateur avec l'identifiant:
error.no.resource = Not found
technical.error = Erreur technique !!!

-Dmaven.test.skip=true

################# BASE DE DONNEES ############################
logging.level.org.hibernate.SQL=error
# Supprime et re-crée les tables et sequences existantes , charge le script d'initialisation de la base de données data.sql
spring.jpa.hibernate.ddl-auto=create-drop

################# GESTION DES LOGS ############################
logging.level.org.springframework.web=DEBUG
logging.level.com.bnguimgo.springboot.rest.server=DEBUG
#
# Pattern impression des logs console
logging.pattern.console= %d{yyyy-MM-dd HH:mm:ss} - %msg%n

# Pattern impression des logs dans un fichier
logging.pattern.file= %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
# Redirection des logs vers un fichier du repertoire Temp, exemple sur windows: C:\Users\UnserName\AppData\Local\Temp\
logging.file=${java.io.tmpdir}/logs/restServer/applicationRestServer.log

Vous pouvez modifier le paramétrage de ce fichier à votre souhait pour, par exemple, mieux gérer les logs. On remarque qu'avec le profil de production, les tests sont désactivés par le paramétrage : -Dmaven.test.skip=true

application-dev.properties
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
#Chargement des propriétés ci-dessous au démarrage de l'application
nextpage.message=Salut vous etes en profile dev sur Rest Server

################# Configuration des Logs #####################
logging.level.root= WARN
logging.level.org.springframework.security= DEBUG
logging.level.org.springframework.web= ERROR
logging.level.org.apache.commons.dbcp2= DEBUG

# Pattern impression des logs console
logging.pattern.console= %d{yyyy-MM-dd HH:mm:ss} - %msg%n

# Pattern impression des logs dans un fichier
logging.pattern.file= %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
# Redirection des logs vers un fichier du repertoire Temp, exemple sur windows: C:\Users\UnserName\AppData\Local\Temp\
logging.file=${java.io.tmpdir}/logs/rest/applicationRestServer.log

################# BASE DE DONNEES ############################
logging.level.org.hibernate.SQL=debug
# Supprime et recrée les tables et séquences existantes , exécute le script data.sql qui initialise la base de données
spring.jpa.hibernate.ddl-auto=create-drop

Il faut noter que jusqu'à présent, nous n'avons développé aucune page JSP, et par conséquent il n'y a rien à mettre dans le dossier webapp, ce qui est normal, car dans cette partie, nous voulons juste exposer des services. La création d'un service REST se passe globalement dans les contrôleurs comme nous allons le voir à travers les implémentations ci-dessous.

I-B-3-b. Nouveautés dans les requêtes HTTP avec Spring

Spring-4.3 a introduit de nouvelles annotations qui facilitent les requêtes HTTP dans des projets SpringMVC. Spring prend actuellement en charge cinq nouveaux types d'annotations intégrées pour la gestion de différents types de requêtes HTTP : GET, POST, PUT, DELETE et PATCH (mise à jour partielle).

Ces annotations sont:

  1. @GetMapping = @RequestMapping + Http GET method.
  2. @PostMapping = @RequestMapping + Http POST method.
  3. @PutMapping = @RequestMapping + Http PUT method.
  4. @DeleteMapping = @RequestMapping + Http DELETE method.
Exemple de méthode HTTP GET avec la nouvelle annotation
Sélectionnez
1.
2.
3.
4.
@GetMapping("/user/{userId}")
public User findUserById (@PathVariable Long userId){
  return userService.findUserById(userId);
}

Avant la nouvelle annotation : en règle générale, si nous annotons une méthode en utilisant l'annotation traditionnelle @RequestMapping, on ferait quelque chose comme ceci :

Exemple de méthode HTTP GET avec l'ancienne annotation
Sélectionnez
1.
2.
3.
4.
@RequestMapping(method=RequestMethod.GET, value = "/user/{userId}", produces = MediaType.APPLICATION_JSON_VALUE)
public User findUserById(@PathVariable Long userId){
  return userService.findUserById(userId);
}

Nous avons spécifié le type de média JSON que le client devrait être capable de traiter. Les avantages des nouvelles annotations résident dans le fait qu'on peut indifféremment produire du XML ou du JSON sans se préoccuper du type de média. Pour avoir cette flexibilité, il est donc conseillé de ne pas spécifier le type de média, et d'utiliser les nouvelles annotations qu'on vient de décrire.

I-B-3-c. Création d'un contrôleur par défaut

Nous avions déjà créé dans la section I-B-1-cCréation du service un petit service qui permettait de tester la configuration de notre environnement. Je vous propose de renommer cette classe (RestServices) en DefaultController et de la déplacer dans un nouveau package appelé controller. Je vous remets le résultat ici :

contrôleur par défaut DefaultController
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
package com.bnguimgo.springbootrestserver.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class DefaultController {    
    private static final Logger logger = LoggerFactory.getLogger(DefaultController.class);    
    @GetMapping(value = "/")
    public ResponseEntity<String> pong() {
        logger.info("Démarrage des services OK .....");
        return new ResponseEntity<String>("Réponse du serveur: "+HttpStatus.OK.name(), HttpStatus.OK);
    }
}

La classe DefaultController est juste un contrôleur utilitaire qui va nous renseigner automatiquement dès le démarrage du client si le serveur REST est à l'écoute grâce à la réponse renvoyée par HttpStatus. Si le serveur est actif, le message de réponse sera: Réponse du serveur: OK, avec le code de réponse HTTP = 200. Sinon, une page d'erreur sera affichée.

L'utilisation de la classe ResponseEntity de Spring permet de prendre en compte la gestion des statuts HTTP de réponses. On peut toutefois utiliser la classe ResponseBody pour traiter les réponses si on n'a pas besoin d'exploiter les codes de réponses HTTP. Dans notre cas, je préfère ResponseEntity. On peut donc dire que ResponseEntity = ResponseBody + HttpStatus.

Le service est désormais prêt et vous pouvez le tester. Pour tester l'application après l'avoir déployée sous Tomcat, voici l'URL de test : http://localhost:8080/springboot-restserver/.

On obtient le même résultat que lors du premier test de la configuration vu précédemment dans la section I-B-1-cCréation du service . N'hésitez pas à regarder de temps en temps les logs dans la console. Dans la classe UserController ci-dessous, je vais développer les services de lecture, de création, de mise à jour et de suppression d'un utilisateur.

I-B-3-d. Service d'extraction de tous les utilisateurs

Il s'agit de créer réellement notre premier service REST d'extraction de tous les utilisateurs encore appelé EndPoint d'extraction de tous les utilisateurs.

service REST d'extraction de tous les utilisateurs dans UserController
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
package com.bnguimgo.springbootrestserver.controller;

import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.bnguimgo.springbootrestserver.model.User;
import com.bnguimgo.springbootrestserver.service.UserService;

@CrossOrigin(origins = "http://localhost:8080", maxAge = 3600)
@RestController
@RequestMapping("/user/*")
public class UserController {

	@Autowired
	private UserService userService;	
	
	@GetMapping(value = "/users")
	public ResponseEntity<Collection<User>> getAllUsers() {
		Collection<User> users = userService.getAllUsers();
		return new ResponseEntity<Collection<User>>(users, HttpStatus.FOUND);
	}
}

L'annotation @CrossOrigin(origins = "http://localhost:8080", maxAge = 3600) permet de favoriser une communication distante entre le client et le serveur, c'est-à-dire lorsque le client et le serveur sont déployés dans deux serveurs distincts, ce qui permet d'éviter des problèmes réseaux.

Compiler et déployer l'application dans Tomcat. Vous pouvez utiliser n'importe quel client REST pour faire les tests. Pour le cas présent, j'utilise Postman. Voici la procédure pour teser:

l'annotation @GetMapping permet de traiter les requêtes HTTP GET.

Test d'extraction des utilisateurs
  • Ouvrez votre client Rest préféré.
  • Choisir la méthode GET.
  • Entrez l'URL qui pointe vers la resource: http://localhost:8080/springboot-restserver/user/users.
  • paramétrez le header pour indiquer le type de contenu, dans le cas présent, on aura Content-Type = application/json.
  • Exécutez la requête.

Voici le paramétrage en image:

Extraction utilisateur
Requête HTTP GET d'extraction utilisateur

Notez bien que lors du paramétrage, nous n'avons rien ajouté dans le corps (contenu ou body) de la requête.

Résultat de la requête HTTP GET
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
[{
  "id": 1,
  "login": "admin@admin.com",
  "password": "$2a$10$ISnv6T5sqpr5YeRKP01xEOLAr/ZWviCp73BC07hMK54GNgQHMemxm",
  "active": 1,
  "roles": [{
    "id": 2,
    "roleName": "ROLE_USER"
  }, {
    "id": 1,
    "roleName": "ROLE_ADMIN"
  }]
}, {
  "id": 2,
  "login": "login2",
  "password": "$2a$10$5TLZBQgB/FOSkccGjKCRDerrD6YzsFznyNURwNHZG8tEwAumfFw1C",
  "active": 1,
  "roles": [{
    "id": 2,
    "roleName": "ROLE_USER"
  }]
}, {
  "id": 3,
  "login": "login3",
  "password": "$2a$10$U2NARRA6lp0CSfDF2JEmtOaAbf3bVGx9zGqDbLkW/T59.QdGnlguO",
  "active": 0,
  "roles": [{
    "id": 2,
    "roleName": "ROLE_USER"
  }]
}]

Cette requête remonte 3 utilisateurs qui sont des données d'initialisation chargées par Spring à partir du script data.sql au démarrage de l'application.

Vérifiez que le code retour HttpStatus est 302, ce qui correspond bien à HttpStatus.FOUND.

I-B-3-e. Service création d'un utilisateur
Endpoint de création d'un utilisateur à ajouter dans la classe UserController
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
	@PostMapping(value = "/users")
	@Transactional
	public ResponseEntity<User> saveUser(@RequestBody User user) {
		
		User userSaved = userService.saveOrUpdateUser(user);		
 		return new ResponseEntity<User>(userSaved, HttpStatus.CREATED);
 	}

Essayons maintenant de créer un utilisateur avec une requête POST. Voici l'URI de création d'un utilisateur : http://localhost:8080/springboot-restserver/user/users. Il s'agit toujours de la même URL utilisée ci-dessus pour l'extraction. Ce qui change, c'est le type de méthode HTTP POST au lieu de HTTP GET, et aussi le contenu de la requête.

Comme il s'agit d'une requête HTTP POST, il faut construire le corps de la requête (body), voir ci-dessous:

Requête création d'un utilisateur
Requête HTTP POST de création d'un utilisateur

Réponse du serveur après création d'un utilisateur

Réponse création d'un utilisateur
Réponse création d'un utilisateur
Analyse de la réponse
  • Un nouvel utilisateur a bien été bien avec un nouvel identifiant = 4.
  • Le login est bien celui de notre utilisateur.
  • On constate bien dans la réponse que le mot de passe est haché comme prévu.
  • L'utilisateur n'est pas actif par défaut car, active =0.
  • Un rôle par défaut a aussi été attribué à cet utilisateur: c'est le rôle ROLE_USER.
I-B-3-f. Service mise à jour utilisateur
Service qui permet de modifier un utilisateur
Sélectionnez
1.
2.
3.
4.
5.
	@PutMapping(value = "/users")
	public ResponseEntity<User> updateUser(@RequestBody User user) {
		User userUpdated = userService.saveOrUpdateUser(user);
		return new ResponseEntity<User>(userUpdated, HttpStatus.OK);
	}

On va modifier l'utilisateur qu'on a créé précédement et dont l'identifiant renvoyé était id=4.

Test mise à jour d'un utilisateur
  • Choisir la méthode HTTP PUT.
  • Entrez la même URL : http://localhost:8080/springboot-restserver/user/users.
  • Construire le body en modifiant le login, mot de passe en lui attribuant le rôle administrateur et en activant son compte.
  • Remarque: cet utilisateur doit avoir absolument un identifiant connu, sinon, erreur.
  • Voir la requête complète ci-dessous:
Requête HTTP PUT
Requête HTTP PUT à envoyer au serveur

Réponse à la requête HTTP PUT

Réponse à la requête HTTP PUT
Réponse à la requête HTTP PUT
Analyse de la réponse HTTP PUT
  • L'utilisateur avec l'identifiant 4 a été mise à jour.
  • Le login et mot de passe ont été modifiés.
  • Il est devenu administrateur et son compte a été activé.
  • Le nouveau mot de passe est bien haché.

Nous venons d'utiliser la même requête http://localhost:8080/springboot-restserver/user/users pour effectuer l'extraction, la création et la mise à jour. La seule chose qui change chaque fois, c'est le type de requête. HTTP GET, HTTP POST, ou HTTP PUT

I-B-3-g. Suppression d'un utilisateur
Méthode HTTP DELETE de suppression d'un utilisateur
Sélectionnez
1.
2.
3.
4.
5.
	@DeleteMapping(value = "/users")
	public ResponseEntity<Void> deleteUser(@RequestParam(value = "id", required = true) Long id) {
		userService.deleteUser(id);
		return new ResponseEntity<Void>(HttpStatus.GONE);
 	}

Requête de suppression : http://localhost:8080/springboot-restserver/user/users?id=3. La suppression d'un utilisateur nécessite de connaître son identifiant. Cet identifiant est passée en paramètre à l'URL. Cette requête ne contient pas de contenu (rien dans le body, mais juste l'idientifiant dans l'URL). Ne pas oublier de choisir la méthode HTTP DELETE.

Analyse de la réponse HTTP DELETE
  • L'utilisateur avec l'identifiant 3 a été supprimé.
  • Il faut vérifier que le code retour est 410, c'est-à-dire HttpStatus.GONE.
I-B-3-h. Recherche d'un utilisateur par login/password
Service de recherche d'un utilisateur par son login
Sélectionnez
1.
2.
3.
4.
5.
6.
	@PostMapping(value = "/users/login")
	public ResponseEntity<User> findUserByLoginAndPassword(@RequestBody User user) {
		
		Optional<User> userFound = userService.findByLoginAndPassword(user.getLogin(), user.getPassword());
		return new ResponseEntity<User>(userFound.get(), HttpStatus.FOUND);
	}

Recherchons par exemple l'utilisateur précédement crée avec le login : updateUser et le password: updatePassword (requête HTTP GET) http://localhost:8080/springboot-restserver/user/users/login.

On obtient:

Recherche utilisateur par login
Recherche utilisateur par login
Analyse de la réponse HTTP POST, findUserByLoginAndPassword
  • On retrouve l'utilisateur dont le login est updateUser, et le mot de passe updateUser.
  • Vérifiez que le code retour est 302, c'est-à-dire HttpStatus.FOUND.

On peut aussi chercher un utilisateur par son identifiant.

EndPoint de recherche d'un utilisateur par identifiant
Sélectionnez
1.
2.
3.
4.
5.
	@GetMapping(value = "/users/{id}")
	public ResponseEntity<User> findUserById(@PathVariable(value = "id") Long id) {
		Optional<User> userFound = userService.findUserById(id);
		return new ResponseEntity<User>(userFound.get(), HttpStatus.FOUND);
	}

Cette méthode nous sera nécessaire pour mettre à jour un utilisateur.

Recherchons maintenant un utilisateur qui n'existe pas.

Voici la requête en image:

Recherche d'un utilisateur inconnu par login
Recherche d'un utilisateur inconnu par login

Résultat recherche d'un utilisateur inconnu:

Résultat recherche d'un utilisateur inconnu par login
Résultat recherche d'un utilisateur inconnu par login

Dans la section suivante, nous allons voir comment construire et personnaliser la gestion des exceptions dans un service REST.

Comme vous pouvez le noter, il faut éviter de mettre la logique métier dans le contrôleur (pas de traitements, pas de calculs), mais, uniquement l'appel des services

I-B-3-i. API Rest au complet

Voici le Endpoint API Rest complet:

API Rest de gestion d'un utilisateur
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
import java.util.Collection;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.bnguimgo.springbootrestserver.exception.BusinessResourceException;
import com.bnguimgo.springbootrestserver.model.User;
import com.bnguimgo.springbootrestserver.service.UserService;

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/user/*")
public class UserController {//NB: pas de logique métier dans le contrôleur, mais, uniquement l'appel des services

	@Autowired
	private UserService userService;	
	
	@GetMapping(value = "/users")
	public ResponseEntity<Collection<User>> getAllUsers() {
		Collection<User> users = userService.getAllUsers();
		return new ResponseEntity<Collection<User>>(users, HttpStatus.FOUND);
	}

	@PostMapping(value = "/users")
	@Transactional
	public ResponseEntity<User> saveUser(@RequestBody User user) {
		
		User userSaved = userService.saveOrUpdateUser(user);		
 		return new ResponseEntity<User>(userSaved, HttpStatus.CREATED);
 	}
	
	@PutMapping(value = "/users")
	public ResponseEntity<User> updateUser(@RequestBody User user) {
		User userUpdated = userService.saveOrUpdateUser(user);
		return new ResponseEntity<User>(userUpdated, HttpStatus.OK);
	}

	@DeleteMapping(value = "/users")
	public ResponseEntity<Void> deleteUser(@RequestParam(value = "id", required = true) Long id) throws BusinessResourceException {
		
		userService.deleteUser(id);
		return new ResponseEntity<Void>(HttpStatus.GONE);
 	}

	@PostMapping(value = "/users/login")
	public ResponseEntity<User> findUserByLoginAndPassword(@RequestBody User user) {
		Optional<User> userFound = userService.findByLoginAndPassword(user.getLogin(), user.getPassword());
		return new ResponseEntity<User>(userFound.get(), HttpStatus.FOUND);
	}
	
	@GetMapping(value = "/users/{id}")
	public ResponseEntity<User> findUserById(@PathVariable(value = "id") Long id) {
		Optional<User> userFound = userService.findUserById(id);
		return new ResponseEntity<User>(userFound.get(), HttpStatus.FOUND);
	}
}

I-C. Gestion des exceptions

Dans cette section, je vais mettre en place la gestion des exceptions en utilisant l'annotation @ControllerAdvice.

I-C-1. Pourquoi utiliser @ControllerAdvice

L'annotation @ControllerAdvice est une spécialisation de l'annotation de @Component introduite depuis Spring-3.2. Elle permet de gérer de façon plus globale les exceptions dans un service REST. Une classe portant cette annotation est détectée automatiquement par Spring au chargement de l'application. Cette annotation prend en charge trois autres annotations très importantes dont @ExceptionHandler, @InitBinder et @ModelAttribute. Un contrôleur annoté par @ControllerAdvice utilise l'annotation @ExceptionHandler pour intercepter des exceptions dites globales, quelles que soient leurs origines. @InitBinder permet une initialisation globale et @ModelAttribute permet de créer un objet model global (par exemple : création et initialisation d'une vue ou d'une page de l'application).

Pour mieux comprendre, je vais créer un nouveau package nommé exception et une classe GlobalHandlerControllerException de gestion globale des exceptions annotée par @ControllerAdvice, et quelques-unes de ses méthodes annotées par @ExceptionHandler, @InitBinder and @ModelAttribute.

Classe global de gestion des exceptions grâce à l'annotation @ControllerAdvice
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
package com.bnguimgo.springbootrestserver.exception;

import javax.servlet.http.HttpServletRequest;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice(basePackages = {"com.bnguimgo.springbootrestserver"} )
public class GlobalHandlerControllerException extends ResponseEntityExceptionHandler{

	@InitBinder
	public void dataBinding(WebDataBinder binder) {
		//Vous pouvez intialiser n'importe quelle donnée ici

	}
	
	@ModelAttribute
    public void globalAttributes(Model model) {
		model.addAttribute("technicalError", "Une erreur technique est survenue !");
    }
	
    @ExceptionHandler(Exception.class)//toutes les autres erreurs non gérées par le service sont interceptées ici
    public ResponseEntity<BusinessResourceExceptionDTO> unknowError(HttpServletRequest req, Exception ex) {
        BusinessResourceExceptionDTO response = new BusinessResourceExceptionDTO();
        response.setErrorCode("Technical Error");
        response.setErrorMessage(ex.getMessage());
        response.setRequestURL(req.getRequestURL().toString()); 
        return new ResponseEntity<BusinessResourceExceptionDTO>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(BusinessResourceException.class)
    public ResponseEntity<BusinessResourceExceptionDTO> businessResourceError(HttpServletRequest req, BusinessResourceException ex) {
        BusinessResourceExceptionDTO businessResourceExceptionDTO = new BusinessResourceExceptionDTO();
        businessResourceExceptionDTO.setStatus(ex.getStatus());
        businessResourceExceptionDTO.setErrorCode(ex.getErrorCode());
        businessResourceExceptionDTO.setErrorMessage(ex.getMessage());
        businessResourceExceptionDTO.setRequestURL(req.getRequestURL().toString()); 
        return new ResponseEntity<BusinessResourceExceptionDTO>(businessResourceExceptionDTO, ex.getStatus());
    }
}

Comme on peut le constater dans la classe ci-dessus, j'ai annoté la classe par @ControllerAdvice. Cette classe est capable d'intercepter et de gérer plusieurs types d'erreurs grâce à l'annotation @ExceptionHandler avec en paramètre le type d'exception. On peut aussi lui passer plusieurs classes d'exceptions pour le même Handler. Il faut aussi noter le passage en paramètre de l'attribut basePackages qui permet d'indiquer dans quels packages se trouvent les contrôleurs à prendre en compte par cette classe (ici, tous les contrôleurs du package com.bnguimgo.springbootrestserver sont concernés et leurs erreurs seront donc gérées par GlobalHandlerControllerException).

Chaque exception traitée est composée de:
  • un status: pour avoir le HttpStatus code.
  • un code d'erreur: code métier pour en cas de besoin.
  • un message d'erreur à renvoyer.
  • l'URL de la requête.
Toutes les exceptions non traitées seront interceptées par la méthode
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
    @ExceptionHandler(Exception.class)//toutes les autres erreurs non gérées sont interceptées ici
    public ResponseEntity<BusinessResourceExceptionResponse> unknowError(HttpServletRequest req, Exception ex) {
        BusinessResourceExceptionResponse response = new BusinessResourceExceptionResponse();
        response.setErrorCode("Technical Error");
        response.setErrorMessage(ex.getMessage());
        response.setRequestURL(req.getRequestURL().toString()); 
        return new ResponseEntity<BusinessResourceExceptionResponse>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }

L'attribut global nommé technicalError pourra être utilisé partout dans l'application.

Voici la classe de gestion d'exception BusinessResourceException:

Classe d'exception classique BusinessResourceException
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
package com.bnguimgo.springbootrestserver.exception;

import org.springframework.http.HttpStatus;

public class BusinessResourceException extends RuntimeException {

    private static final long serialVersionUID = 1L;
    private Long resourceId;
    private String errorCode;
    private HttpStatus status;

    public BusinessResourceException(String message) {
        super(message);
    }
    
    public BusinessResourceException(Long resourceId, String message) {
        super(message);
        this.resourceId = resourceId;
    }
    public BusinessResourceException(Long resourceId, String errorCode, String message) {
        super(message);
        this.resourceId = resourceId;
        this.errorCode = errorCode;
    }
    
    public BusinessResourceException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public BusinessResourceException(String errorCode, String message, HttpStatus status) {
        super(message);
        this.errorCode = errorCode;
        this.status = status;
    }

    public Long getResourceId() {
        return resourceId;
    }

    public void setResourceId(Long resourceId) {
        this.resourceId = resourceId;
    }

    public String getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(String errorCode) {
        this.errorCode = errorCode;
    }    
    
    public HttpStatus getStatus() {
        return status;
    }

    public void setStatus(HttpStatus status) {
        this.status = status;
    }
}

Notez au passage l'existance de plusieurs constructeurs dont celui avec le paramètre HttpStatus permettant de propager le code d'erreur. C'est ce constructeur qui sera préféré: public BusinessResourceException(String errorCode, String message, HttpStatus status){ xxxx }.

Vous pouvez noter aussi que j'ai enveloppé les exceptions dans une classe utilitaire nommée BusinessResourceExceptionResponse. Cette classe va servir à stocker les messages d'erreurs.

Classe POJO BusinessResourceExceptionResponse de persistance des messages d'erreurs
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
package com.bnguimgo.springbootrestserver.exception;

import org.springframework.http.HttpStatus;

import org.springframework.http.HttpStatus;

public class BusinessResourceExceptionDTO {
 
    private String errorCode;
    private String errorMessage;
	private String requestURL;
	private HttpStatus status;
 
    public BusinessResourceExceptionDTO() {
    }
 
    public String getErrorCode() {
        return errorCode;
    }
 
    public void setErrorCode(String errorCode) {
        this.errorCode = errorCode;
    }
 
    public String getErrorMessage() {
        return errorMessage;
    }
 
    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }

	public void setRequestURL(String url) {
		this.requestURL = url;
		
	}
	public String getRequestURL(){
		return requestURL;
	}
	
    public HttpStatus getStatus() {
		return status;
	}

	public void setStatus(HttpStatus status) {
		this.status = status;
	}
}

I-C-2. Test des services avec exceptions

Pour mettre en évidence l'intérêt de l'annotation de @ControllerAdvice, je vais faire un premier cas de test en cherchant un utilisateur qui n'existe pas et avec l'annotation @ControllerAdvice désactivée (mettre en commentaire). Résultat:

Exception sans l'annotation @ControllerAdvice
Exception sans l'annotation @ControllerAdvice

On obtient ERROR 500 et un ensemble d'informations pas très personnalisées.

Et voici l'exception renvoyée lorsque l'annotation @ControllerAdvice est activée

Exception avec @ControllerAdvice
Exception lorsque @ControllerAdvice est activée

On obtient 404 Not Found, mais avec une suite d'erreurs plus claire et exploitable. Par exemple : un code d'erreur, le message, et une URL bien précise et complète.

I-D. Tests unitaires des couches

Chaque couche de l'application peut être testée unitairement. Et comme vous pouvez le constater, chaque couche a une dépendance vis-à-vis de la couche immédiatement inférieure. Dans le cas des tests unitaires, l'idée est de s'affranchir de cette dépendance et de tester unitairement chaque méthode de la couche. C'est ce qui explique le plus souvent l'usage des Mocks Objects(Objets mocks = Objets factices ou objets sans existence réelle) dans les tests unitaires (ce qui n'est pas le cas dans les tests d'intégration).

Il faut donc trouver quel Mock Object est adapté pour chaque couche de l'application, car ça change en fonction des besoins et des cas de tests.

Mais, avant de commencer les tests, j'ai ajouté quelques dépendances nécessaires pour les tests unitaires et les tests d'intégration. Voici les deux dépendances :

Dépendances nécessaires pour les tests
Sélectionnez
1.
2.
3.
4.
5.
        <dependency>
            <groupId>com.jayway.jsonpath</groupId>
            <artifactId>json-path</artifactId>
            <scope>test</scope>
        </dependency>

Et voici le pom.xml complété de l'application:

pom.xml complété avec les dépendances pour les tests
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.bnguimgo</groupId>
	<artifactId>springboot-restserver</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>

	<name>springboot-restserver</name>
	<description>Serveur Rest utilisant le framework Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.6.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<start-class>com.bnguimgo.restclient.com.bnguimgo.springbootrestserver.SpringbootRestserverApplication</start-class><!-- 
			cette déclaration est optionnelle -->
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
		<tomcat.version>9.0.24</tomcat.version><!-- On surcharge la version intégrée par défaut de Tomcat -->
	</properties>
	<repositories>
		<!-- Indiquer le repo maven avec https afin d'eviter l'erreur suivante: 
			Failed to transfer file: http://repo.maven.apache.org/maven2/org/codehaus/cargo/cargo-maven2-plugin/1.7.10/cargo-maven2-plugin-1.7.10.pom. 
			Return code is: 501 , ReasonPhrase:HTTPS Required. -->
		<repository>
			<id>central maven repo</id>
			<name>central maven repo https</name>
			<url>https://repo.maven.apache.org/maven2</url>
		</repository>
	</repositories>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		
		<!-- Spring Security: juste pour hacher le mot de passe -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

		<!-- Permet l'utilisation des collections Apache -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-collections4</artifactId>
			<version>4.0</version>
		</dependency>

		<!-- Base de données utilisée par l'application -->
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>

		<!-- Dépendances pour les tests uniquement -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
		</dependency>
		<dependency>
			<groupId>com.jayway.jsonpath</groupId>
			<artifactId>json-path</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

    <build>
        <finalName>springboot-restserver</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

I-D-1. Tests unitaires couche DAO

La couche DAO est la couche la plus basse de l'application et, de ce fait, elle communique le plus souvent avec la base de données. Pour la tester, on ne cherche pas à savoir comment se passe l'extraction des données, mais on vérifie plutôt que la couche DAO est capable de fournir les données dont on a besoin : d'où l'utilisation d'un Mock Object. Pour répondre à ce défi, Spring Boot a mis à notre disposition la classe TestEntityManager qui est l'équivalent Mock de JPA EntityManager.

Voici le service d'accès aux données utilisateur à tester
Sélectionnez
1.
2.
3.
public interface UserRepository extends JpaRepository<User, Long> {    
    Optional<User> findByLogin(String loginParam);    
}
Et ci-dessous l'initialisation des tests
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
@RunWith(SpringRunner.class)//permet d'établir une liaison entre JUnit et Spring
@DataJpaTest
public class UserRepositoryTest {

	@Autowired
    private TestEntityManager entityManager;	
	@Autowired
    private UserRepository userRepository;	
	User user = new User("Dupont", "password", 1);
	
	@Before
	public void setup(){
	    entityManager.persist(user);//on sauvegarde l'objet user au début de chaque test
	    entityManager.flush();
	}
}

@RunWith(SpringRunner.class) permet d'établir la liaison l'implémentation Spring de JUnit, donc c'est tout naturel qu'on l'utilise pour un test unitaire.

@DataJpaTest est une implémentation Spring de JPA qui fournit une configuration intégrée de la base de données H2, Hibernate, SpringData, et la DataSource. Cette annotation active également la détection des entités annotées par Entity, et intègre aussi la gestion des logs SQL.

Un utilisateur User("Dupont", "password", 1) est enregistré dans le setup avant chaque test.

Test unitaire de tous les services de la couche DAO
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
package com.bnguimgo.springbootrestserver.dao;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;

import java.util.List;
import java.util.Optional;

import static org.hamcrest.CoreMatchers.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.junit4.SpringRunner;

import com.bnguimgo.springbootrestserver.model.User;

@RunWith(SpringRunner.class)//permet d'établir une liaison entre JUnit et Spring
@DataJpaTest
public class UserRepositoryTest {

	@Autowired
    private TestEntityManager entityManager;	
	@Autowired
    private UserRepository userRepository;	
	User user = new User("Dupont", "password", 1);
	
	@Before
	public void setup(){
	    entityManager.persist(user);//on sauvegarde l'objet user au début de chaque test
	    entityManager.flush();
	}
	@Test
	public void testFindAllUsers() {
	    List<User> users = userRepository.findAll();
	    assertThat(4, is(users.size()));//on a 3 Users dans le fichier d'initialisation data.sql et un utilisateur ajouté lors du setup du test
	}
	
	@Test
    public void testSaveUser(){
		User user = new User("Paul", "password", 1);
		User userSaved =  userRepository.save(user);
		assertNotNull(userSaved.getId());
	    assertThat("Paul", is(userSaved.getLogin()));
	}
	@Test
	public void testFindByLogin() {
	    Optional<User> userFromDB = userRepository.findByLogin("login2");	 
	    assertThat("login2", is(userFromDB.get().getLogin()));//login2 a été crée lors de l'initialisation du fichier data.sql     
	}
	
	@Test
	public void testFindById() {
	    Optional<User> userFromDB = userRepository.findById(user.getId());	 
	    assertThat(user.getLogin(), is(userFromDB.get().getLogin()));//user a été crée lors du setup     
	}
	
	@Test
	public void testFindBy_Unknow_Id() {
	    Optional<User> userFromDB = userRepository.findById(50L);	 
	    assertEquals(Optional.empty(), Optional.ofNullable(userFromDB).get());
	}
	
	@Test
    public void testDeleteUser(){
		userRepository.deleteById(user.getId());
		Optional<User> userFromDB = userRepository.findByLogin(user.getLogin());
		assertEquals(Optional.empty(), Optional.ofNullable(userFromDB).get());
	}
	
	@Test
	public void testUpdateUser() {//Test si le compte utilisateur est désactivé
	    Optional<User> userToUpdate = userRepository.findByLogin(user.getLogin());
	    userToUpdate.get().setActive(0);
	    userRepository.save(userToUpdate.get());	    
	    Optional<User> userUpdatedFromDB = userRepository.findByLogin(userToUpdate.get().getLogin());
	    assertNotNull(userUpdatedFromDB);
	    assertThat(0, is(userUpdatedFromDB.get().getActive()));
	}		
}

@TestEntityManager permet de persister les données en base lors des tests. Toutes les méthodes ont été testées.

J'ai ajouté le framework hamcrest (fourni par Spring Boot) qui apporte plus de flexibilité pour l'utilisation de la méthode de test assertThat().

I-D-2. Tests unitaires couche service

La couche de service a une dépendance directe avec la couche DAO. Cependant, on n'a pas besoin de savoir comment se passe la persistance au niveau de la couche DAO. Le plus important c'est que cette couche renvoie les données sollicitées. On peut donc moquer la couche DAO en utilisant le framework Mockito

Voici le service à tester :
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
package com.bnguimgo.springbootrestserver.service;
// tous les imports ici

@Service
public class UserServiceImpl implements UserService {
	
	private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
	private UserRepository userRepository;
	private RoleRepository roleRepository;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
	
	public UserServiceImpl() {
		super();
	}
	
	@Autowired
	public UserServiceImpl(UserRepository userRepository, RoleRepository roleRepository, BCryptPasswordEncoder bCryptPasswordEncoder) {
		super();
		this.userRepository = userRepository;
		this.roleRepository = roleRepository;
		this.bCryptPasswordEncoder = bCryptPasswordEncoder;
	}
	
	@Override
	public Optional<User> findByLogin(String login) throws BusinessResourceException {
		
		Optional<User> userFound = userRepository.findByLogin(login);
        if (Boolean.FALSE.equals(userFound.isPresent())) {
            throw new BusinessResourceException("User Not Found", "L'utilisateur avec ce login n'existe pas :" + login);
        }
		return userFound;
	}

	@Override
	public Collection<User> getAllUsers() {
		return IteratorUtils.toList(userRepository.findAll().iterator());
	}
	
	@Override
	public Optional<User> findUserById(Long id) throws  BusinessResourceException{
		
		Optional<User> userFound = userRepository.findById(id);
        if (Boolean.FALSE.equals(userFound.isPresent())){
            throw new BusinessResourceException("User Not Found", "Aucun utilisateur avec l'identifiant :" + id);
        }
		return userFound;
	}

	@Override
	@Transactional(readOnly=false)
	public User saveOrUpdateUser(User user) throws BusinessResourceException{
		try{
			if(null ==user.getId()) {//pas d'Id --> création d'un user
				addUserRole(user);//Ajout d'un rôle par défaut
				user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
			} else {//sinon, mise à jour d'un user
				
				Optional<User> userFromDB = findUserById(user.getId());
				if(! bCryptPasswordEncoder.matches(user.getPassword(), userFromDB.get().getPassword())) {
					user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));//MAJ du mot de passe s'il a été modifié
				} else {
					
					user.setPassword(userFromDB.get().getPassword());//Sinon, on remet le password déjà haché
				}
				updateUserRole(user);//On extrait le rôle en cas de mise à jour
			}
			User result = userRepository.save(user);
			return  result;
		} catch(DataIntegrityViolationException ex){
			logger.error("Utilisateur non existant", ex);
			throw new BusinessResourceException("DuplicateValueError", "Un utilisateur existe déjà avec le compte : "+user.getLogin(), HttpStatus.CONFLICT);
		} catch (BusinessResourceException e) {
			logger.error("Utilisateur non existant", e);
			throw new BusinessResourceException("UserNotFound", "Aucun utilisateur avec l'identifiant: "+user.getId(), HttpStatus.NOT_FOUND);
		} catch(Exception ex){
			logger.error("Erreur technique de création ou de mise à jour de l'utilisateur", ex);
			throw new BusinessResourceException("SaveOrUpdateUserError", "Erreur technique de création ou de mise à jour de l'utilisateur: "+user.getLogin(), HttpStatus.INTERNAL_SERVER_ERROR);
		}
	}

	@Override
	@Transactional(readOnly=false)
	public void deleteUser(Long id) throws BusinessResourceException {
		try{
			userRepository.deleteById(id);
		}catch(EmptyResultDataAccessException ex){
			logger.error(String.format("Aucun utilisateur n'existe avec l'identifiant: "+id, ex));
			throw new BusinessResourceException("DeleteUserError", "Erreur de suppression de l'utilisateur avec l'identifiant: "+id, HttpStatus.NOT_FOUND);
		}catch(Exception ex){
			throw new BusinessResourceException("DeleteUserError", "Erreur de suppression de l'utilisateur avec l'identifiant: "+id, HttpStatus.INTERNAL_SERVER_ERROR);
		}		
	}

	@Override
	public Optional<User> findByLoginAndPassword(String login, String password) throws BusinessResourceException{
		try {
			Optional<User> userFound = this.findByLogin(login);
			if(bCryptPasswordEncoder.matches(password, userFound.get().getPassword())) {
				return userFound;
			} else {
				throw new BusinessResourceException("UserNotFound", "Mot de passe incorrect", HttpStatus.NOT_FOUND);
			}
		} catch (BusinessResourceException ex) {
			logger.error("Login ou mot de passe incorrect", ex);
			throw new BusinessResourceException("UserNotFound", "Login ou mot de passe incorrect", HttpStatus.NOT_FOUND);
		}catch (Exception ex) {
			logger.error("Une erreur technique est survenue", ex);
			throw new BusinessResourceException("TechnicalError", "Une erreur technique est survenue", HttpStatus.INTERNAL_SERVER_ERROR);
		}
	}
	
	private void addUserRole(User user) {
		Set<Role> roles= new HashSet<>();
		Role roleUser = new Role("ROLE_USER");//initialisation du rôle ROLE_USER
		roles.add(roleUser);
		user.setActive(0);

		Set<Role> roleFromDB = extractRole_Java8(roles, roleRepository.getAllRolesStream());
		user.setRoles(roleFromDB);
	}
	
	private void updateUserRole(User user) {

		Set<Role> roleFromDB = extractRole_Java8(user.getRoles(), roleRepository.getAllRolesStream());
		user.setRoles(roleFromDB);
	}
	
 	private Set<Role> extractRole_Java8(Set<Role> rolesSetFromUser, Stream<Role> roleStreamFromDB) { 		
 	// Collect UI role names
 		Set<String> uiRoleNames = rolesSetFromUser.stream()
 		    .map(Role::getRoleName)
 		    .collect(Collectors.toCollection(HashSet::new));
 	// Filter DB roles
		return roleStreamFromDB
			.filter(role -> uiRoleNames.contains(role.getRoleName()))
		    .collect(Collectors.toSet());
	}

	@SuppressWarnings("unused")
	private Set<Role> extractRoleUsingCompareTo_Java8(Set<Role> rolesSetFromUser, Stream<Role> roleStreamFromDB) {
		return roleStreamFromDB
			.filter(roleFromDB -> rolesSetFromUser.stream()
			.anyMatch( roleFromUser -> roleFromUser.compareTo(roleFromDB) == 0))
	        .collect(Collectors.toCollection(HashSet::new));
	}

	@SuppressWarnings("unused")
	private Set<Role>  extractRole_BeforeJava8(Set<Role> rolesSetFromUser, Collection<Role> rolesFromDB) {
		Set<Role> rolesToAdd = new HashSet<>();
		for(Role roleFromUser:rolesSetFromUser){
			for(Role roleFromDB:rolesFromDB){
				if(roleFromDB.compareTo(roleFromUser)==0){
					rolesToAdd.add(roleFromDB);
					break;
				}
			}
		}
		return rolesToAdd;
	}
}

Pour éviter d'avoir des problèmes de configuration des tests, il faut créer exactement les mêmes packages dans src/test/java. Par exemple, pour tester le service UserService, créez le package : com.bnguimgo.springbootrestserver.service

Voici les tests unitaires complets de la classe UserServiceImpl
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
package com.bnguimgo.springbootrestserver.service;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.bnguimgo.springbootrestserver.dao.RoleRepository;
import com.bnguimgo.springbootrestserver.dao.UserRepository;
import com.bnguimgo.springbootrestserver.exception.BusinessResourceException;
import com.bnguimgo.springbootrestserver.model.Role;
import com.bnguimgo.springbootrestserver.model.User;

//@RunWith(SpringRunner.class) //pas besoin car on a fait l'autowired par constructeur sur UserServiceImpl
public class UserServiceImplTest {
 
    private UserService userService;
    private RoleRepository roleRepository;
    private UserRepository userRepository;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    
    @Before
    public void setup() {
    	userRepository = Mockito.mock(UserRepository.class);
    	roleRepository = Mockito.mock(RoleRepository.class);
    	bCryptPasswordEncoder = Mockito.mock(BCryptPasswordEncoder.class);
    	userService = new UserServiceImpl(userRepository, roleRepository, bCryptPasswordEncoder);
    }
    @Test
    public void testFindAllUsers() throws Exception {
    	User user = new User("Dupont", "password", 1);
        Role role = new Role("USER_ROLE");
        Set<Role> roles = new HashSet<>();
    	roles.add(role);
    	user.setRoles(roles);
    	List<User> allUsers = Arrays.asList(user);           
        Mockito.when(userRepository.findAll()).thenReturn(allUsers);
    	Collection<User> users = userService.getAllUsers();
    	assertNotNull(users);
    	assertEquals(users, allUsers);
    	assertEquals(users.size(), allUsers.size());
    	verify(userRepository).findAll();
    }
    
    @Test
    public void testSaveUser() throws Exception {
    	User user = new User("Dupont", "password", 1);
        Role role = new Role("USER_ROLE");
        Set<Role> roles = new HashSet<>();
    	roles.add(role);
    	User userMock = new User(1L,"Dupont", "password", 1);
    	userMock.setRoles(roles);
    	Mockito.when(roleRepository.getAllRolesStream()).thenReturn(roles.stream());
    	Mockito.when(userRepository.save((user))).thenReturn(userMock);
    	User userSaved = userService.saveOrUpdateUser(user);
    	assertNotNull(userSaved);
    	assertEquals(userMock.getId(), userSaved.getId());
     	assertEquals(userMock.getLogin(), userSaved.getLogin());
     	assertEquals(userMock.getLogin(), userSaved.getLogin());
     	assertEquals(userMock.getRoles(), userSaved.getRoles());
     	verify(userRepository).save(any(User.class));
    }
    
    @Test(expected=BusinessResourceException.class)
    public void testSaveUser_existing_login_throws_error() throws Exception {
    	User user = new User("Dupont", "password", 1);
        Role role = new Role("USER_ROLE");
        Set<Role> roles = new HashSet<>();
    	roles.add(role);
    	Mockito.when(roleRepository.getAllRolesStream()).thenReturn(roles.stream());
    	Mockito.when(userRepository.save((user))).thenThrow(new DataIntegrityViolationException("Duplicate Login"));
    	userService.saveOrUpdateUser(user);
     	verify(userRepository).save(any(User.class));
    }
    
    @Test
    public void testFindUserByLogin() {
    	User user = new User("Dupont", "password", 1);
    	Mockito.when(userRepository.findByLogin(user.getLogin())).thenReturn(Optional.ofNullable(user));
        Optional<User> userFromDB = userService.findByLogin(user.getLogin());  
        assertNotNull(userFromDB);
        assertThat(userFromDB.get().getLogin(), is(user.getLogin()));  
        verify(userRepository).findByLogin(any(String.class));
     }
    
    @Test
    public void testUpdateUser() throws Exception {
    	User userToUpdate = new User(1L, "NewDupont", "newPassword", 1);
        Role role = new Role("USER_ROLE");
        Set<Role> roles = new HashSet<>();
    	roles.add(role);
    	
    	User userFoundById = new User(1L, "OldDupont", "oldpassword", 1);
    	userFoundById.setRoles(roles);
    	
    	User userUpdated = new User(1L, "NewDupont", "newPassword", 1);
    	userUpdated.setRoles(roles);
    	
    	Mockito.when(userRepository.findById(1L)).thenReturn(Optional.of(userFoundById));
    	Mockito.when(bCryptPasswordEncoder.matches(any(String.class), any(String.class))).thenReturn(false);
    	Mockito.when(bCryptPasswordEncoder.encode(any(String.class))).thenReturn("newPassword");
    	Mockito.when(userRepository.save((userToUpdate))).thenReturn(userUpdated);
    	User userFromDB = userService.saveOrUpdateUser(userToUpdate);
    	assertNotNull(userFromDB);
    	verify(userRepository).save(any(User.class));
    	assertEquals(new Long(1), userFromDB.getId());
    	assertEquals("NewDupont", userFromDB.getLogin());
    	assertEquals("newPassword", userFromDB.getPassword());
    	assertEquals(new Integer(1), userFromDB.getActive());
    	assertEquals(roles, userFromDB.getRoles());
    }
    
    @Test
    public void testDelete() throws Exception {
    	User userTodelete = new User(1L,"Dupont", "password", 1);
    	Mockito.doNothing().when(userRepository).deleteById(userTodelete.getId());
    	userService.deleteUser(userTodelete.getId());
    	
    	verify(userRepository).deleteById(any(Long.class));
    }
}

Sachant que nous avons fait l'autowired par constructeur lors du développement de UserServiceImpl, nous n'avons pas plus besoin d'injecter les dépendances. Une simple déclaration est suffisante. De plus, on n'a pas besoin de @RunWith(SpringRunner.class) pour exécuter les tests unitaires du service.

Le framework Mockito a été utilisé pour moquer tout le service DAO, ce qui a permis d'initialiser les tests.

En développant les services, nous avons haché le mot de passe pour ne pas le laisser transiter en clair dans le réseau. Il faut donc ajouter un bean BcryptPasswordEncoder.

Je vous laisse la possibilité de compléter les tests de la couche de service, et surtout le test des exceptions.

Notez l'usage de Mockito.doNothing() qui permet de tester une méthode dont le type de retour est void. exemple de de la méthode public void deleteUser(Long id) {xxx}.

Tests unitaires de la classe RoleServiceImpl
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
package com.bnguimgo.springbootrestserver.service;

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

import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.mockito.Mockito;

import com.bnguimgo.springbootrestserver.dao.RoleRepository;
import com.bnguimgo.springbootrestserver.model.Role;

public class RoleServiceImplTest {

	RoleRepository roleRepository = Mockito.mock(RoleRepository.class);
	
    @Test
    public void test_getAllRoles(){

        RoleServiceImpl roleService = new  RoleServiceImpl (roleRepository );
 
        Role role = new Role("USER_ROLE");
    	List<Role> roles = Arrays.asList(role);
    	
        Mockito.when(roleRepository.findAll()).thenReturn(roles);
        Collection<Role> result = roleService.getAllRoles();
 
        Assertions.assertThat(result).isNotNull().hasSize(1);
    }
    
    @Test
    public void test_getAllRolesStream(){

        RoleServiceImpl roleService = new  RoleServiceImpl (roleRepository );
 
        Role role = new Role("USER_ROLE");
    	List<Role> roles = Arrays.asList(role);
    	
        Mockito.when(roleRepository.findAll()).thenReturn(roles);
        Collection<Role> result = roleService.getAllRoles();
 
        Assertions.assertThat(result).isNotNull().hasSize(1);
    }
}

I-D-3. Tests unitaires du contrôleur

Le contrôleur à tester est le suivant :
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
@Controller

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/user/*")
public class UserController {

	@Autowired
	private UserService userService;	
	
	@GetMapping(value = "/users")
	public ResponseEntity<Collection<User>> getAllUsers() {
		Collection<User> users = userService.getAllUsers();
		return new ResponseEntity<Collection<User>>(users, HttpStatus.FOUND);
	}

	@PostMapping(value = "/users")
	@Transactional
	public ResponseEntity<User> saveUser(@RequestBody User user) {
		
		User userSaved = userService.saveOrUpdateUser(user);		
 		return new ResponseEntity<User>(userSaved, HttpStatus.CREATED);
 	}

	@PostMapping(value = "/users/login")
	public ResponseEntity<User> findUserByLoginAndPassword(@RequestBody User user) {
		Optional<User> userFound = userService.findByLoginAndPassword(user.getLogin(), user.getPassword());
		return new ResponseEntity<User>(userFound.get(), HttpStatus.FOUND);
	}
	
	@PutMapping(value = "/users")
	public ResponseEntity<User> updateUser(@RequestBody User user) {
		User userUpdated = userService.saveOrUpdateUser(user);
		return new ResponseEntity<User>(userUpdated, HttpStatus.OK);
	}

	@DeleteMapping(value = "/users")
	public ResponseEntity<Void> deleteUser(@RequestParam(value = "id", required = true) Long id) throws BusinessResourceException {
		
		userService.deleteUser(id);
		return new ResponseEntity<Void>(HttpStatus.GONE);
 	}
}

Ce contrôleur dépend de la couche de service. Il ne connait pas la couche DAO. Il faut donc créer un Mock Object de la couche de service grâce à l'annotation déjà vue précédemment @MockBean. Et pour indiquer le contrôleur à tester, il faut utiliser l'annotation @WebMvcTest en lui passant en paramètre la classe à tester. Cette annotation va aussi apporter toutes les dépendances SpringMVC nécessaires pour le cas de test.

Grâce à l'annotation @WebMvcTest, on peut injecter un MockMvc qui servira à construire des URL pour générer des requêtes HTTP. Nous allons tester la méthode du contrôleur qui recherche tous les utilisateurs de la base de données.

Créez le package de tests com.bnguimgo.springbootrestserver.controller et y ajouter le test UserControllerTest
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
package com.bnguimgo.springbootrestserver.controller;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
//for HTTP methods
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
//for HTTP status
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import com.bnguimgo.springbootrestserver.model.Role;
import com.bnguimgo.springbootrestserver.model.User;
import com.bnguimgo.springbootrestserver.service.UserService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = UserController.class, excludeAutoConfiguration = SecurityAutoConfiguration.class)
public class UserControllerTest {

	@Autowired
	private MockMvc mockMvc;
	private ObjectMapper objectMapper;

	@MockBean
	private UserService userService;

	User user;

	@Before
	public void setUp() {
		// Initialisation du setup avant chaque test
		Role role = new Role("USER_ROLE");// initialisation du role utilisateur
		Set<Role> roles = new HashSet<>();
		roles.add(role);
		user = new User(1L, "Dupont", "password", 1);
		user.setRoles(roles);
		List<User> allUsers = Arrays.asList(user);
		objectMapper = new ObjectMapper();

		// Mock de la couche de service
		when(userService.getAllUsers()).thenReturn(allUsers);
		when(userService.findUserById(any(Long.class))).thenReturn(Optional.of(user));

	}

	@Test
	public void testFindAllUsers() throws Exception {

		MvcResult result = mockMvc.perform(get("/user/users").contentType(MediaType.APPLICATION_JSON))
				.andExpect(status().isFound()).andReturn();

		// ceci est une redondance, car déjà vérifié par: isFound())
		assertEquals("Réponse incorrecte", HttpStatus.FOUND.value(), result.getResponse().getStatus());
		verify(userService).getAllUsers();
		assertNotNull(result);
		Collection<User> users = objectMapper.readValue(result.getResponse().getContentAsString(),
				new TypeReference<Collection<User>>() {
				});
		assertNotNull(users);
		assertEquals(1, users.size());
		User userResult = users.iterator().next();
		assertEquals(user.getLogin(), userResult.getLogin());
		assertEquals(user.getPassword(), userResult.getPassword());

	}

	@Test
	public void testSaveUser() throws Exception {

		User userToSave = new User("Dupont", "password", 1);
		String jsonContent = objectMapper.writeValueAsString(userToSave);
		when(userService.saveOrUpdateUser(any(User.class))).thenReturn(user);
		MvcResult result = mockMvc
				.perform(post("/user/users").contentType(MediaType.APPLICATION_JSON).content(jsonContent))
				.andExpect(status().isCreated()).andReturn();

		assertEquals("Erreur de sauvegarde", HttpStatus.CREATED.value(), result.getResponse().getStatus());
		verify(userService).saveOrUpdateUser(any(User.class));
		User userResult = objectMapper.readValue(result.getResponse().getContentAsString(), new TypeReference<User>() {
		});
		assertNotNull(userResult);
		assertEquals(userToSave.getLogin(), userResult.getLogin());
		assertEquals(userToSave.getPassword(), userResult.getPassword());

	}

	@Test
	public void testFindUserByLogin() throws Exception {
		when(userService.findByLoginAndPassword("Dupont", "password")).thenReturn(Optional.ofNullable(user));
		User userTofindByLogin = new User("Dupont", "password");
		String jsonContent = objectMapper.writeValueAsString(userTofindByLogin);
		// on execute la requête
		MvcResult result = mockMvc
				.perform(post("/user/users/login").contentType(MediaType.APPLICATION_JSON).content(jsonContent))
				.andExpect(status().isFound()).andReturn();

		assertEquals("Erreur de sauvegarde", HttpStatus.FOUND.value(), result.getResponse().getStatus());
		verify(userService).findByLoginAndPassword(any(String.class), any(String.class));
		User userResult = objectMapper.readValue(result.getResponse().getContentAsString(), new TypeReference<User>() {
		});
		assertNotNull(userResult);
		assertEquals(new Long(1), userResult.getId());
		assertEquals(userTofindByLogin.getLogin(), userResult.getLogin());
		assertEquals(userTofindByLogin.getPassword(), userResult.getPassword());
	}

	@Test
	public void testDeleteUser() throws Exception {

		MvcResult result = mockMvc.perform(delete("/user/users/").param("id", "1"))
				.andExpect(status().isGone()).andReturn();
		assertEquals("Erreur de suppression", HttpStatus.GONE.value(), result.getResponse().getStatus());
		verify(userService).deleteUser(any(Long.class));
	}

	@Test
	public void testUpdateUser() throws Exception {

		User userToUpdate = new User("Toto", "password", 0);
		User userUpdated = new User(2L, "Toto", "password", 0);
		String jsonContent = objectMapper.writeValueAsString(userToUpdate);
		when(userService.saveOrUpdateUser(userToUpdate)).thenReturn(userUpdated);
		// on execute la requête
		MvcResult result = mockMvc
				.perform(MockMvcRequestBuilders.put("/user/users/").contentType(MediaType.APPLICATION_JSON)
						.accept(MediaType.APPLICATION_JSON).content(jsonContent))
				.andExpect(status().isOk()).andReturn();

		verify(userService).saveOrUpdateUser(any(User.class));
		User userResult = objectMapper.readValue(result.getResponse().getContentAsString(), new TypeReference<User>() {
		});
		assertNotNull(userResult);
		assertEquals(new Long(2), userResult.getId());
		assertEquals(userToUpdate.getLogin(), userResult.getLogin());
		assertEquals(userToUpdate.getPassword(), userResult.getPassword());
		assertEquals(userToUpdate.getActive(), userResult.getActive());
	}

}

Arrêtons-nous un peu sur le test unitaire deleteUser(@RequestParam(value = "id", required = true) Long id) pour apprécier comment se fait le passage du paramètre id. Comme le passage du paramètre id se fait par @RequestParam, nous devons également utiliser .param("id", "1") pour assigner la valeur à ce paramètre pour les tests, ce qui donne le résultat ci-dessous:

Test avec @RequestParam
Sélectionnez
@Test
public void testDeleteUser() throws Exception {

	MvcResult result = mockMvc.perform(delete("/user/users/")
		.param("id", "1"))
		.andExpect(status().isGone())
		.andReturn();
	assertEquals("Erreur de suppression", HttpStatus.GONE.value(), result.getResponse().getStatus());
	verify(userService).deleteUser(any(Long.class));
}

Pour faire le même test si on avait plutôt @PathVariable en paramètre, on obtiendrait:

Test avec @PathVariable en paramètre
Sélectionnez
@Test
public void testDeleteUser() throws Exception {
	
	MvcResult result = mockMvc.perform(delete("/user/users/{id}", new Long(1)))
		.andExpect(status().isGone())
		.andReturn();
	assertEquals("Erreur de suppression", HttpStatus.GONE.value(), result.getResponse().getStatus());
	verify(userService).deleteUser(any(Long.class));     
}

I-D-4. Résumé sur les tests unitaires

Ce chapitre sur les tests unitaires a permis de voir que les besoins en matière de tests unitaires diffèrent d'une couche à l'autre.

Couche DAO :

  • @DataJpaTest pour configurer la base de données et la Datasource.
  • TestEntityManager qui est l'équivalent Mock de JPA EntityManager.

Couche de service :

  • Mockito pour créer un Mock de la couche DAO.

Couche contrôleur :

  • @WebMvcTest permet d'indiquer le contrôleur à tester.
  • @MockMvc pour injecter un mock MVC qui sert à construire des requêtes HTTP.
  • @MockBean pour créer un mock de la couche de service.

Il faut bien remarquer que nous n'avons pas utisé l'annotation @RunWith(SpringRunner.class) lors des tests de la couche de service puisque nous appliqué l'@autowired sur le constructeur.

I-E. Tests d'intégration services REST

Les tests d'intégration consistent à tester une fonctionnalité ou un service complet à travers toutes les couches de l'application. Cela suppose qu'il faut penser à un déploiement complet de l'application, d'où la nécessité d'un serveur d'application, d'une base de données et, par conséquent, pas de Mocks Objects. En pratique, pour tester toutes les fonctionnalités d'une application, ça prend beaucoup de temps. C'est pourquoi ces tests sont séparés des tests unitaires et sont généralement programmés à des heures d'activités réduites.

Avant d'écrire le test, voici le paramétrage nécessaire à l'initialisation de la classe de test :

Paramétrage des tests d'intégration
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class UserControllerIntegrationTest {

	@Autowired
	private TestRestTemplate restTemplate;
	private static final String URL = "http://localhost:8080";// url du serveur REST. Cet url peut être celle d'un serveur distant

	private String getURLWithPort(String uri) {
		return URL + uri;
	}
    
    // Les tests ici
}

Il faut garder à l'esprit que, dans le cadre d'un test d'intégration, tous les services du serveur d'application doivent être démarrés.

Quelques explications :

@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT, classes={Controller.class, AnotherController.class})

Cette annotation a pour rôle :

  • de déclencher le démarrage du serveur d'application sur un port libre.
  • de créer une ApplicationContext pour les tests.
  • d'initialiser une servlet embarquée.
  • et enfin d'enregistrer un bean TestRestTemplate pour la gestion des requêtes HTTP.

TestRestTemplate est une dépendance nécessaire pour écrire les requêtes HTTP.

Voici les tests d'intégration complets pour tous les services CRUD à créer dans un package com.bnguimgo.springbootrestserver.integrationtest :

Tests d'intégration pour tous les services CRUD
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
package com.bnguimgo.springbootrestserver.integrationtest;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.net.URI;
import java.util.Collection;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.util.UriComponentsBuilder;

import com.bnguimgo.springbootrestserver.model.Role;
import com.bnguimgo.springbootrestserver.model.User;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class UserControllerIntegrationTest {

	@Autowired
	private TestRestTemplate restTemplate;
	private static final String URL = "http://localhost:8080";// url du serveur REST. Cette url peut être celle d'un //
																// serveur distant

	private String getURLWithPort(String uri) {
		return URL + uri;
	}

	@Test
	public void testFindAllUsers() throws Exception {

		ResponseEntity<Object> responseEntity = restTemplate
				.getForEntity(getURLWithPort("/springboot-restserver/user/users"), Object.class);
		assertNotNull(responseEntity);
		@SuppressWarnings("unchecked")
		Collection<User> userCollections = (Collection<User>) responseEntity.getBody();
		assertEquals("Réponse inattendue", HttpStatus.FOUND.value(), responseEntity.getStatusCodeValue());
		assertNotNull(userCollections);
		assertEquals(4, userCollections.size());
		// on a bien 3 utilisateurs initialisés par les scripts data.sql + un nouvel
		// utilisateur crée dans testSaveUser
	}

	@Test
	public void testSaveUser() throws Exception {

		User user = new User("PIPO", "newPassword", 1);
		ResponseEntity<User> userEntitySaved = restTemplate
				.postForEntity(getURLWithPort("/springboot-restserver/user/users"), user, User.class);
		assertNotNull(userEntitySaved);
		// On vérifie le code de réponse HTTP est celui attendu
		assertEquals("Réponse inattendue", HttpStatus.CREATED.value(), userEntitySaved.getStatusCodeValue());
		User userSaved = userEntitySaved.getBody();
		assertNotNull(userSaved);
		assertEquals(user.getLogin(), userSaved.getLogin());
		assertEquals("ROLE_USER", userSaved.getRoles().iterator().next().getRoleName());// on vérifie qu'un role par
	}

	@Test
	public void testFindByLoginAndPassword() throws Exception {

		User userTofindByLoginAndPassword = new User("admin@admin.com", "admin");
		ResponseEntity<User> responseEntity = restTemplate.postForEntity(
				getURLWithPort("/springboot-restserver/user/users/login"), userTofindByLoginAndPassword, User.class);
		assertNotNull(responseEntity);
		User userFound = responseEntity.getBody();

		assertEquals("Réponse inattendue", HttpStatus.FOUND.value(), responseEntity.getStatusCodeValue());
		assertNotNull(userFound);
		assertEquals(new Long(1), userFound.getId());
	}

	@Test
	public void testFindByLoginAndPassword_notFound() throws Exception {

		User userTofindByLoginAndPassword = new User("unknowUser", "password3");
		ResponseEntity<User> responseEntity = restTemplate.postForEntity(
				getURLWithPort("/springboot-restserver/user/users/login"), userTofindByLoginAndPassword, User.class);
		assertNotNull(responseEntity);
		assertEquals("Réponse inattendue", HttpStatus.NOT_FOUND.value(), responseEntity.getStatusCodeValue());
	}

	@Test
	public void testUpdateUser() throws Exception {

		ResponseEntity<User> responseEntityToUpdate = restTemplate.postForEntity(
				getURLWithPort("/springboot-restserver/user/users/login"), new User("login3", "password3"), User.class);
		User userFromDBtoUpdate = responseEntityToUpdate.getBody();
		// on met à jour l'utilisateur en lui attribuant le role admin, nouveau login et mot de passe
		userFromDBtoUpdate.setLogin("newLogin");
		userFromDBtoUpdate.setPassword("newPassword");
		userFromDBtoUpdate.setActive(1);
		Role role = new Role("ROLE_ADMIN");
		userFromDBtoUpdate.getRoles().add(role);

		URI uri = UriComponentsBuilder.fromHttpUrl(URL).path("/springboot-restserver/user/users").build().toUri();

		HttpEntity<User> requestEntity = new HttpEntity<User>(userFromDBtoUpdate);

		ResponseEntity<User> responseEntity = restTemplate.exchange(uri, HttpMethod.PUT, requestEntity, User.class);
		assertNotNull(responseEntity);
		User userUpdated = responseEntity.getBody();
		assertNotNull(userUpdated);
		assertEquals("Réponse inattendue", HttpStatus.OK.value(), responseEntity.getStatusCodeValue());
		assertEquals(userFromDBtoUpdate.getLogin(), userUpdated.getLogin());
	}

	@Test
	public void testDeleteUser() throws Exception {

		URI uri = UriComponentsBuilder.fromHttpUrl(URL).path("/springboot-restserver/user/users")
				.queryParam("id", new Long(2)).build().toUri();

		ResponseEntity<Void> responseEntity = restTemplate.exchange(uri, HttpMethod.DELETE, null, Void.class);
		assertNotNull(responseEntity);
		assertEquals("Réponse inattendue", HttpStatus.GONE.value(), responseEntity.getStatusCodeValue());
	}
}

Pour exécuter les tests depuis Eclipse, déployez l'application dans Tomcat, puis clic droit sur le nom de la classe Run as ... ==> JUnit Test.

I-F. Automatisation des tests

Dans les chapitres précédents, nous avons développé les tests unitaires et les tests d'intégration, mais ces tests ne sont pas automatisés. c'est-à-dire qu'il faut une action humaine pour que ces tests s'exécutent. Nous pouvons encore faire mieux en les automatisant, c'est ce que nous allons voir dans les chapitres suivants.

I-F-1. Automatisation des tests unitaires

Maven offre un excellent moyen d'automatiser les tests à travers l'utilisation des plugins. Pour automatiser les tests unitaires, nous allons utliser le plugin maven-surefire-plugin. Son avantage c'est de s'arrêter imédiatement dès qu'un test unitaire échoue.

Voici sa configuration à ajouter dans pom.xml, dans la partie plugins:

Plugin d'exécution des tests unitaires
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-surefire-plugin</artifactId>
	<!-- On exclut les tests d'intégration lors de l'exécution des tests 
		unitaires -->
	<configuration>
		<excludes>
			<exclude>**/*IntegrationTest.java</exclude>
		</excludes>
	</configuration>
</plugin>

pour exécuter les tests unitaires, ouvrir la console, se placer dans le répertoire contenant le pom.xml, puis exécuter mvn clean install.

Lors de l'exécution des tests unitaires, on exclu les tests d'intégration, car ce n'est pas nécessaires.

I-F-2. Automatisation des tests d'intégration

Nous allons configurer le plugin cargo-maven2-pluginpour automatiser les tests d'intégration, et Tomcat9 pour comme serveur de tests. L'avantage de cette automatisation c'est qu'on pourra facilement intégrer l'application dans un environnement d'intégration continue comme jenkins. Voir le tutoriel de Alexandre-LaurentTutoriel Jenkins pour plus de détails.

Toute la configuration se passe dans le pom.xml. Pour ce faire, on va créer un profil d'intégration.

Ce profil permet démarrer, Tomcat9x, de jouer les tests d'intégration et d'arrêter Tomcat à la fin des tests grâce au plugin cargo-maven2-plugin

Cargo s'appuie sur le plugin maven-failsafe-plugin pour éxécuter les tests d'intégration. Nous aurons donc besoin d'intégrer maven-failsafe-plugin et le configurer.

Dans la phase des tests d'intégration, on aimerait pas rejouer les tests unitaires, il faudra donc les exclure en utilisant le plugin maven-surefire-plugin qui a été utilisé pour les tests unitaires

Configuration du profil des tests d'intégration continue
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
<profile>
	<id>integrationTest</id>
	<activation>
		<activeByDefault>false</activeByDefault>
	</activation>
	<properties>
		<skip.surefire.tests>true</skip.surefire.tests>
		<skip.failsafe.tests>false</skip.failsafe.tests>
	</properties>
	<build>
		<plugins>
			<plugin>
				<groupId>org.codehaus.cargo</groupId>
				<artifactId>cargo-maven2-plugin</artifactId>
				<version>${cargo.version}</version>
				<configuration>
					<wait>false</wait>
					<container>
						<containerId>tomcat9x</containerId>
						<type>embedded</type>
					</container>
					<configuration>
						<properties>
							<!-- les ports 8080 et 8009 sont à éviter si l'application est déjà déployée -->
							<cargo.servlet.port>8484</cargo.servlet.port>
							<cargo.tomcat.ajp.port>8090</cargo.tomcat.ajp.port>
						</properties>
					</configuration>
				</configuration>

				<executions>
					<execution>
						<!-- id obligatoire pour distinguer les deux phases start et stop -->
						<id>start-server</id>
						<phase>pre-integration-test</phase>
						<goals>
							<goal>start</goal>
						</goals>
					</execution>
					<execution>
						<id>stop-server</id>
						<phase>post-integration-test</phase>
						<goals>
							<goal>stop</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			<!-- Configuration de l'exécution des tests d'intégration -->
			<!-- C'est le plugin maven-failsafe-plugin qui va exécuter les tests d'intégration -->
			<plugin>
				<artifactId>maven-failsafe-plugin</artifactId>
				<executions>
					<execution>
						<phase>integration-test</phase>
						<goals>
							<goal>integration-test</goal>
							<goal>verify</goal>
						</goals>
					</execution>
				</executions>
				<configuration>
					<excludes>
						<exclude>none</exclude>
					</excludes>
					<includes>
						<include>**/*IntegrationTest.java</include>
					</includes>
					<skipTests>${skip.failsafe.tests}</skipTests>
				</configuration>
			</plugin>
			<!-- Le plugin maven-surefire-plugin est adapté pour les tests unitaires -->
			<plugin>
				<artifactId>maven-surefire-plugin</artifactId>
				<!-- On exclut les tests unitaires lors des tests d'intégration, car 
				déjà exécutés NB: Si vous n'ajoutez pas cette configuration, les tests unitaires 
				seront reéxécutés lors des tests d'intégration -->
				<configuration>
					<skipTests>${skip.surefire.tests}</skipTests>
				</configuration>
			</plugin>

		</plugins>
	</build>
</profile>

Il faut configurer le port de l'application correctement dans la classe de test UserControllerIntegrationTest, ==> private static final String URL = "http://localhost:8484";. Vérifier que c'est le même port à la fois dans le profil d'intégration et dans la classe. J'ai préféré le port 8484 pour éviter que le port 8080 soit déjà utilisé par l'application.

Pour exécuter les tests d'intégration depuis une console, placez-vous dans le répertoire des sources, puis exécuter: mvn clean install -PintegrationTest

Note: il n'est pas conseillé d'utiliser le plugin surfire pour les tests d'intégration, car surfire s'arrête dès la première erreur et ne continue plus les autres tests, il faut préferer plutôt le plugin failsafe.

Phases d'éxécution du plugin
  • On utilise une version embarquée de Tomcat9x.
  • Dans la phase pré-intégration, on démarre le serveur.
  • On inclut les tests d'intégration et on exclut les tests unitaires.
  • Après exécution des tests, dans la phase post-intégration, on arrêt le serveur.

Note: dans l'environnement d'intégration continue Jenkins, ce sont les mêmes commandes qu'il faut configurer, à savoir: mvn clean install -PintegrationTest pour les tests d'intégration ou simplement mvn clean install pour les test unitaires.

I-G. Déploiement automatisé

Il est question de voir dans ce paragraphe comment déployer automatiquement l'application à travers le plugin cargo-maven2-plugin

Un profil springbootServerDeploy a été ajouté au pom.xml afin de faciliter le déploiement.

Mais, on peut également déployer depuis Eclipse par un simple clic droit sur le projet, puis Run As --> Run on Server.

Configuration du profil de déploiement automatisé
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
<profile>
	<id>springbootServerDeploy</id>
	<activation>
		<activeByDefault>false</activeByDefault>
	</activation>
	<build>
		<plugins>
			<plugin>
				<groupId>org.codehaus.cargo</groupId>
				<artifactId>cargo-maven2-plugin</artifactId>
				<version>${cargo.version}</version>
				<configuration>
					<container>
						<containerId>tomcat9x</containerId>
						<type>embedded</type>
					</container>
					<configuration>
					<home>${project.build.directory}/tomcat9x</home>
						<properties>
							<cargo.servlet.port>8080</cargo.servlet.port>
						</properties>
					</configuration>
					<deployables>
						<deployable>
							<groupId>com.bnguimgo</groupId>
							<artifactId>springboot-restserver</artifactId>
							<type>war</type>
							<pingURL>http://localhost:8080/springboot-restserver</pingURL>
						</deployable>
					</deployables>
				</configuration>
			</plugin>
		</plugins>
	</build>
</profile>

I-H. Paramétrage complet

Dans ce paragraphe, il est juste question de vous fournir le paramétrage complet de l'application à travers le fichier pom.xml.

pom.xml au complet de l'application
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.bnguimgo</groupId>
	<artifactId>springboot-restserver</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>

	<name>springboot-restserver</name>
	<description>Serveur Rest utilisant le framework Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.6.RELEASE</version>
		<relativePath />
	</parent>

	<properties>
		<start-class>com.bnguimgo.restclient.com.bnguimgo.springbootrestserver.SpringbootRestserverApplication</start-class>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
		<cargo.version>1.7.10</cargo.version>
		<tomcat.version>9.0.24</tomcat.version>
	</properties>
	<repositories>
		<repository>
			<id>central maven repo</id>
			<name>central maven repo https</name>
			<url>https://repo.maven.apache.org/maven2</url>
		</repository>
	</repositories>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<!-- Spring Security: juste pour hacher le mot de passe -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

		<!-- Permet l'utilisation des collections Apache -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-collections4</artifactId>
			<version>4.0</version>
		</dependency>

		<!-- Base de données utilisée par l'application -->
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>

		<!-- Dépendances pour les tests uniquement -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
		</dependency>
		<dependency>
			<groupId>com.jayway.jsonpath</groupId>
			<artifactId>json-path</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<finalName>springboot-restserver</finalName>
		<resources>
			<resource>
				<directory>src/main/resources</directory>
			</resource>
		</resources>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-surefire-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>**/*IntegrationTest.java</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<profiles>
		<profile>
			<id>integrationTest</id>
			<activation>
				<activeByDefault>false</activeByDefault>
			</activation>
			<properties>
				<skip.surefire.tests>true</skip.surefire.tests>
				<skip.failsafe.tests>false</skip.failsafe.tests>
			</properties>
			<build>
				<plugins>
					<plugin>
						<groupId>org.codehaus.cargo</groupId>
						<artifactId>cargo-maven2-plugin</artifactId>
						<version>${cargo.version}</version>
						<configuration>
							<wait>false</wait>
							<container>
								<containerId>tomcat9x</containerId>
								<type>embedded</type>
							</container>
							<configuration>
								<properties>
									<cargo.servlet.port>8484</cargo.servlet.port>
									<cargo.tomcat.ajp.port>8090</cargo.tomcat.ajp.port>
								</properties>
							</configuration>
						</configuration>

						<executions>
							<execution>
								<id>start-server</id>
								<phase>pre-integration-test</phase>
								<goals>
									<goal>start</goal>
								</goals>
							</execution>
							<execution>
								<id>stop-server</id>
								<phase>post-integration-test</phase>
								<goals>
									<goal>stop</goal>
								</goals>
							</execution>
						</executions>
					</plugin>
					<plugin>
						<artifactId>maven-failsafe-plugin</artifactId>
						<executions>
							<execution>
								<phase>integration-test</phase>
								<goals>
									<goal>integration-test</goal>
									<goal>verify</goal>
								</goals>
							</execution>
						</executions>
						<configuration>
							<excludes>
								<exclude>none</exclude>
							</excludes>
							<includes>
								<include>**/*IntegrationTest.java</include>
							</includes>
							<skipTests>${skip.failsafe.tests}</skipTests>
						</configuration>
					</plugin>
					<plugin>
						<artifactId>maven-surefire-plugin</artifactId>
						<configuration>
							<skipTests>${skip.surefire.tests}</skipTests>
						</configuration>
					</plugin>

				</plugins>
			</build>
		</profile>
		<profile>
			<id>springbootServerDeploy</id>
			<activation>
				<activeByDefault>false</activeByDefault>
			</activation>
			<build>
				<plugins>
					<plugin>
						<groupId>org.codehaus.cargo</groupId>
						<artifactId>cargo-maven2-plugin</artifactId>
						<version>${cargo.version}</version>
						<configuration>
							<container>
								<containerId>tomcat9x</containerId>
								<type>embedded</type>
							</container>
							<configuration>
								<home>${project.build.directory}/tomcat9x</home>
								<properties>
									<cargo.servlet.port>8080</cargo.servlet.port>
								</properties>
							</configuration>
							<deployables>
								<deployable>
									<groupId>com.bnguimgo</groupId>
									<artifactId>springboot-restserver</artifactId>
									<type>war</type>
									<pingURL>http://localhost:8080/springboot-restserver</pingURL>
								</deployable>
							</deployables>
						</configuration>
					</plugin>
				</plugins>
			</build>
		</profile>
	</profiles>
</project>

Pour déployer en utilisant ce plugin, depuis une console, placez-vous au même niveau que le pom.xml et exécuter la commande: mvn cargo:run -PspringbootServerDeploy

Dans la deuxième partie, je vais développer un client web pour consommer ces services. J'utiliserai pour ce faire Spring RestTemplate pour permettre la communication entre le client et le serveur. C'est d'ailleurs pour cette que j'ai utilisé Spring RestTemplate pour les tests d'intégration.

Je vous invite à lire la deuxième partie de ce tutoriel, car la plupart des tutoriels qui existent sur internet ne montrent pas comment mettre en place le client afin de consommer les services. J'ai tenu à bien séparer les deux parties pour démontrer qu'on peut tout à fait développer et consommer ces services sur un serveur distant.


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2018 Nguimgo Bertrand. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.