Spring Boot est une sur-couche au dessus de spring framework qui vous permettra de boostrapper (démarrer) un projet java basé sur Spring framework rapidement. Les starter de SpringBoot minimiseront énormément la liste des dépendances directes que vous devez déclarer vous memes d'une part et d'autres part ces starter auto-configurerons une part très importante de votre application. De ce fait, elle démarrera avec les fonctionnalités dont vous avez besoin.
Ainsi, nous nous appuierons sur SpringBoot afin de créer rapidement notre API Rest avec le moins d'effort possible. Il ne faudrait perdre à l'esprit que nous pourrions faire la meme chose avec le framework spring de base, mais plus longtemps.
Spring Boot permet également de démarrer une application web comme une simple application console le serait via la méthode main.
1. Les composants de notre application
Dans ce tutoriel, il s'agira de développer un service java qui exposera une API Rest afin de permettre la lecture, la création, la modification et la suppression d'un utilisateur (user).
Ce module du framework spring nous permettra d'utiliser l'implémentation de Spring quant à la création et l'exposition d'API Rest
Dans ce module, Spring nous met à disposition toute une batterie de modules, classes et services pour faciliter l'accès au données d'une base de données via la spécification JPA (Java Persistence API). Et de plus, l'implémentation JPA par défaut, proposée par Spring Data JPA est l'ORM (Object-Relational Mapping) Hibernate.
Nous utiliserons ensuite Spring Doc pour la documentation OpenAPI (swagger) de notre API Rest. Ce module de spring framework nous mettra à disposition SwaggerUI, une interface graphique nous permettra de tester notre API Rest.
Cette section sera détaillée dans un autre tutoriel dédié qu'à la documentation des API Rest
Nous utiliserons comme base de données, la base de données PostgreSQL. Ce qui implique nous devrons utiliser le driver jdbc postgresql.
2. Spring Initializr
Spring Initialzr est un portail simple qui nous permet de créer à la volée un projet basée sur SpringBoot et nous donne la possibilité de lister les dépendances utiles à notre projet. Dans notre cas, spring-web, spring-data-jpa et spring-doc seront les composants que nous listerons dans les dépendances.
Avec Spring Initilizr, nous ajoutons les dépendances à notre projet
2.1 Dépendances Gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.javatutorialshub.usermanagement'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
2.2 Dépendances Maven
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.javatutorialshub</groupId>
<artifactId>user-management-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>user-management-service</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</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>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3. Les entités
Les tables de données dans notre base de données seront représentée dans notre code java, par ces bean (POJO) java que nous appelons entités (Entity). Une ligne dans la table dédiée en base de donnée sera en réalité une instance de notre entité.
Et un framework tel qu' Hibernate (qui n'est est fait une implémentation de la spécification Java JPA) nous permettra de faire le lien entre la table table en base de données et l'objet dans notre programme Java. On parle d'ORM. Ainsi nous allons définir les entités utiles à notre application.
La classe d'entité "Role" définit les Roles que peut posséder un utilisateur. A l'aide des annotations de JPA, nous définissons les le nom de la table (@Table), et la clé primaire de la table (@Id). Le champ id sera auto-générée avec la stratégie UUID (@GeneratedValue).
@Entity
@Table(name = "tbl_role")
@Data
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
private String name;
private String description;
}
Puis nous définissons de même la classe d'entité User. Et un user possède plusieurs roles alors que le même role peut attribué à plusieurs utilisateurs. Une association est donc rajoutée dans la classe d'entité user.
@Entity
@Table(name = "tbl_user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
private String name;
private String firstName;
private LocalDate birthDate;
@OneToMany
@JoinTable(
name = "user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private List<Role> roles;
}
4. Les Repository
Pour exécuter les requêtes SQL pour insérer, modifier, supprimer ou sélectionner des données dans la base de données, il est conseillé d'utiliser le design pattern DAO (Data Access Object). Cette classe aura pour unique but, de faire la connection à la base de données puis puis faire le nécessaire afin que vos requêtes SQL soient exécutées dans la base de données. Dans une application basée sur Spring, comme la notre, les Repository sont une sorte de couche au dessus des DAO de Spring. Mais pour simplifier disons que les Repository sont les DAO dans une application Spring.
Ainsi nous créerons un repository pour chaque classe d'entité. Et ce seront des interfaces héritant des repository de base JpaRepository, vu que nous faisons du JPA, et mis à dispositions par spring. Ces interfaces n'ont pas besoin d'être implémentées, car Spring Data prendra la main dans le processus d'execution et créera les requêtes SQL/JPQL adéquates.
public interface RoleRepository extends JpaRepository<Role, String> {
}
public interface UserRepository extends JpaRepository<User, String> {
}
Comme vous pouvez le constater, dans la déclaration du repository, nous indiquons la classe Repository de base puis la classe entité concernée suivi du type de la clé primaire.
Il est maintenant l'heure d'écrire le coeur de notre application. Encore une fois, nous appliquerons le fait qu'une classe doit avoir une seule et une seule responsabilité: Premier principe SOLID.
5. Les services
Définissons ici quelques uns des services que nous créerons.
RetrieveUserService: Cette classe se charge uniquement de requêter la base de données pour retourner une liste d'utilisateur. Cette classe sera annotée avec le stereotype @Service afin qu'elle soit vu comme un bean de spring.
@Service
@RequiredArgsConstructor
@Log
public class RetrieveUserService {
private final UserRepository userRepository;
@Transactional(readOnly = true)
public Collection<User> retrieveUsers(int page, int size) throws RetrieveUserException {
try {
Pageable pageable = PageRequest.of(page, size);
Page<User> users = userRepository.findAll(pageable);
return users.getContent();
} catch (Exception e) {
String message = String.format("A problem was encountered when " +
"attempting to " +
"load users at page: %d and size: %d", page, size);
log.log(Level.SEVERE, message);
throw new RetrieveUserException(e);
}
}
}
CreateUserService: Cette classe se charge uniquement de requêter la base de données pour sauvegarder une liste d'utilisateur. Cette classe sera annotée avec le stereotype @Service afin qu'elle soit vu comme un bean de spring.
@Service
@RequiredArgsConstructor
@Log
public class CreateUserService {
private final UserRepository userRepository;
@Transactional
public void createUsers(Collection<User> users) throws CreateUserException {
try {
userRepository.saveAll(users);
} catch (Exception e) {
String message = String.format("A problem was encountered when " +
"attempting to create users with names: %s",
users.stream().map(User::getName).toList());
log.log(Level.SEVERE, message);
throw new CreateUserException(e);
}
}
}
UpdateUserService: Cette classe se charge uniquement de requêter la base de données pour sauvegarder une liste d'utilisateur. Cette classe sera annotée avec le stereotype @Service afin qu'elle soit vu comme un bean de spring. De base, Spring Data considère que toute entité est nouvelle donc essaiera de faire un insert dans la base de donnée en lieu et place d'un update. Afin de lui indiquer qu'il faille faire un update ici, toutes nos entités étendrons l'interface: Persistable de Spring Data, qui nous permettra d'indiquer que les entités ici ne sont pas nouvelles, mais existent déjà dans la base de données.
@Service
@RequiredArgsConstructor
@Log
public class UpdateUserService {
private final UserRepository userRepository;
@Transactional
public void updateUsers(Collection<User> users) throws UpdateUserException {
try {
users.forEach(AbstractEntity::markNotNew);
userRepository.saveAll(users);
} catch (Exception e) {
String message = String.format("A problem was encountered when " +
"attempting to update users with names: %s",
users.stream().map(User::getName).toList());
log.log(Level.SEVERE, message);
throw new UpdateUserException(e);
}
}
}
DeleteUserService: Cette classe se charge uniquement de requêter la base de données pour supprimer une liste d'utilisateur. Cette classe sera annotée avec le stereotype @Service afin qu'elle soit vu comme un bean de spring.
@Service
@RequiredArgsConstructor
@Log
public class DeleteUserService {
private final UserRepository userRepository;
@Transactional
public void deleteUsers(Collection<String> userIds) throws DeleteUserException {
try {
userRepository.deleteAllById(userIds);
} catch (Exception e) {
String message = String.format("A problem was encountered when " +
"attempting to delete users with ids: %s",
userIds);
log.log(Level.SEVERE, message);
throw new DeleteUserException(e);
}
}
}
Il est temps d'exposer nos API Rest qui utiliserons les services afin d'exécuter la logique métier qui nous interesse. Spring Web, met à notre disposition l'annotation @RestController qui nous permet d'exposer nos API Rest. Au metre titre que pour les services, nous créerons une classe par Action.
6. Les controllers
@RestController
Il s'agit de l'annotation principale, qui permettra à Spring de consider la classe annotée comme exposant une API Rest.
@RestController
public class RetrieveUserController {
}
@RequestMapping
Cette annotation permet d'indiquer le context path qui sera utiliser pour atteindre la resource concernée. Cette annotation peut être indiquée sur une classe ou sur une méthode.
@RestController
@RequestMapping("users")
public class RetrieveUserController {
}
@GetMapping
Cette annotation indique que la méthode GET du protocole HTTP doit être utilisée pour accéder à la resource concernée. Cette méthode est utilisée lorsque la ressource ne modifie aucune donnée et retourne une réponse.
@RestController
@RequestMapping("users")
public class RetrieveUserController {
@GetMapping
public Collection<User> getUsers(){
return null;
}
}
@PostMapping
Cette annotation indique que la méthode POST du protocole HTTP doit être utilisée pour accéder à la resource concernée. Cette méthode est utilisée lorsque la ressource peut créer ou modifier des données et retourne ou non une réponse.
@RestController
@RequestMapping("users")
public class CreateUserController {
@PostMapping
public void createUsers(Collection<User> users){
}
}
@PutMapping
Cette annotation indique que la méthode PUT du protocole HTTP doit être utilisée pour accéder à la resource concernée. Cette méthode est utilisée lorsque la ressource peut modifier des données et retourne ou non une réponse.
@RestController
@RequestMapping("users")
public class UpdateUserController {
@PutMapping
public void updateUsers(Collection<User> users){
}
}
@DeleteMapping
Cette annotation indique que la méthode DELETE du protocole HTTP doit être utilisée pour accéder à la resource concernée. Cette méthode est utilisée lorsque la ressource peut supprimer des données et retourne ou non une réponse.
@RestController
@RequestMapping("users")
public class DeleteUserController {
@DeleteMapping
public void deleteUser(){
}
}
@RequestParam
Cette annotation que nous retrouverons au niveau des paramètres de la méthode du controller, permet de récupérer les paramètres provenant de l'url de la ressource Rest et ensuite de les passer à la méthode java concernée. L'url suivante: /users?page=0&size=10 sera traduite dans le controller comme suit.
@RestController
@RequestMapping("users")
public class RetrieveUserController {
@GetMapping
public Collection<User> getUsers(@RequestParam("page") int page,
@RequestParam("size") int size){
return null;
}
}
@PathVariable
Cette annotation que nous retrouverons au niveau des paramètres de la méthode du controller, permet de récupérer une partie de l'url de la ressource Rest, comme valeur qui sera ensuite passer à la méthode java concernée. L'exemple suivant /users/{userId} sera traduit dans le controller comme suit:
@RestController
@RequestMapping("users")
public class DeleteUserController {
@DeleteMapping("/{userId}")
public void deleteUser(@PathVariable("userId") String userId){
}
}
@RequestBody
Cette annotation que nous retrouverons au niveau des paramètres de la méthode du controller, permet de passer un objet JSON comme valeur qui sera ensuite passer à la méthode java concernée:
@RestController
@RequestMapping("users")
public class CreateUserController {
@PostMapping
public void createUsers(@RequestBody Collection<User> users){
}
}
RetrieveUserController: Ce controller permet de créer une API Rest qui se chargera de retourner une liste d'utilisateurs en fonction de la page et de la taille de la page indiquée en paramètre.
@RestController
@RequestMapping("users")
@RequiredArgsConstructor
public class RetrieveUserController {
private final RetrieveUserService retrieveUserService;
@GetMapping
public Collection<User> getUsers(@RequestParam("page") int page,
@RequestParam("size") int size)
throws RetrieveUserException {
return retrieveUserService.retrieveUsers(page, size);
}
}
Pour tester cette API:
Url : http://localhost:8080/users?page=0&size=10
Method: GET
CreateUserController: Ce controller permet de créer une API Rest qui se chargera de créer les utilisateurs présents dans une liste en invoquant le service dédié à cet effet.
@RestController
@RequestMapping("users")
@RequiredArgsConstructor
public class CreateUserController {
private final CreateUserService createUserService;
@PostMapping
public void createUsers(@RequestBody Collection<User> users)
throws CreateUserException {
createUserService.createUsers(users);
}
}
Pour tester cette API:
Url : http://localhost:8080/users
Method: POST
Data: json
UpdateUserController: Ce controller permet de créer une API Rest qui se chargera de modifier les utilisateurs présents dans une liste en invoquant le service dédié à cet effet.
@RestController
@RequestMapping("users")
@RequiredArgsConstructor
public class UpdateUserController {
private final UpdateUserService updateUserService;
@PutMapping
public void updateUsers(@RequestBody Collection<User> users)
throws UpdateUserException {
updateUserService.updateUsers(users);
}
}
Pour tester cette API:
Url : http://localhost:8080/users
Method: PUT
Data: json
DeleteUserController: Ce controller permet de créer une API Rest qui se chargera de supprimer un utilisateur en invoquant le service dédié à cet effet.
@RestController
@RequestMapping("users")
@RequiredArgsConstructor
public class DeleteUserController {
private final DeleteUserService deleteUserService;
@DeleteMapping("/{userId}")
public void deleteUser(@PathVariable("userId") String userId)
throws DeleteUserException {
deleteUserService.deleteUsers(List.of(userId));
}
}
Pour tester cette API:
Url : http://localhost:8080/users/1234-abcd-yx34
Method: DELETE
Ainsi, spring et plus précisément springboot nous permet de créer facilement des API Rest via les controller et plus précisément via l'annotation @RestController.
Ne pas hésiter à commenter ou à poser une question ou à demander de l'aide autour de java et des technologies connexes. Nous nous ferons un plaisir de vous répondre.
Commentaires
Enregistrer un commentaire