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.
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 :
- 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.
- 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.
- 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.
- 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.
- 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:
- Créer à partir de l'IDE Eclipse un projet Maven simple et le compléter avec les dépendances dont vous avez besoin.
- Créer un projet à partir du générateur fourni par Spring Boot à l'adresse : Spring Boot Quick StartGénérer un projet Spring Boot.
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 :
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é :
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).
<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 :
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.
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:
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:
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) :
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:
Il est possible de tester directement sur un navigateur web ou un client REST. Voici le résultat dans le navigateur 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 :
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 =
1
L;
@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
;
}
}
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 =
2284252532274015507
L;
@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.
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.
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));
}
}
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.
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).
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▲
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;
}
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
<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 :
<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.
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.
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.
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}.
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:
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
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.
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] %-
5
level %
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
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] %-
5
level %
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:
- @GetMapping = @RequestMapping + Http GET method.
- @PostMapping = @RequestMapping + Http POST method.
- @PutMapping = @RequestMapping + Http PUT method.
- @DeleteMapping = @RequestMapping + Http DELETE method.
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 :
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 :
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.
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.
- 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:
Notez bien que lors du paramétrage, nous n'avons rien ajouté dans le corps (contenu ou body) de la requête.
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▲
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:
Réponse du serveur après création d'un utilisateur
- 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▲
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.
- 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:
Réponse à la requête 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▲
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.
- 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▲
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:
- 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.
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:
Résultat recherche d'un utilisateur inconnu:
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:
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.
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).
- 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.
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:
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 =
1
L;
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.
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:
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
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 :
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:
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.
2.
3.
public
interface
UserRepository extends
JpaRepository<
User, Long>
{
Optional<
User>
findByLogin
(
String loginParam);
}
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.
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
(
50
L);
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
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
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
(
1
L,"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
(
1
L, "NewDupont"
, "newPassword"
, 1
);
Role role =
new
Role
(
"USER_ROLE"
);
Set<
Role>
roles =
new
HashSet<>(
);
roles.add
(
role);
User userFoundById =
new
User
(
1
L, "OldDupont"
, "oldpassword"
, 1
);
userFoundById.setRoles
(
roles);
User userUpdated =
new
User
(
1
L, "NewDupont"
, "newPassword"
, 1
);
userUpdated.setRoles
(
roles);
Mockito.when
(
userRepository.findById
(
1
L)).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
(
1
L,"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}.
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▲
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.
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
(
1
L, "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
(
2
L, "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
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
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 :
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 :
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:
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
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.
- 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.
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.
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.