Prise en main de Spring Boot : Premier Service backend

Prise en main de Spring Boot

Développement d'un simple Micro-service

Dans cet article, je montre comment créer un simple service backend avec Spring Boot. L'exemple traite le cas d'une application qui gère des comptes bancaires. Chaque compte est défini par son code, sa date  de création, son solde et son type. L'architecture de l'application est décomposée en trois couches :

  • La couche DAO qui est basée sur Spring Data, JPA, Hibernate et JDBC
  • La couche service pour implémenter des spécification fonctionnelles comme les virement bancaires. les opérations de cette couche sont transactionnelles
  • La couche Web basée sur un Web service RESTFUL implémenté de deux manières :
    • Rest Controller
    • Spring Data Rest
Les API RESTFUL sont documentée en utilisant Spring Doc Open API (SWAGGER 3.0)

Pour les Getters, Setters et Constructeur, on utilisera Lombok

Voici une architecture technique de cette application :




Structure du Projet :



  • Dépendances Maven :

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.5.2</version>
</dependency>

  • Couche DAO

    • Entité JPA
Dans cet une exemple, nous aurons besoin d'une seule entité Persistante c'est la classe Compte 
package org.sid.compteservice.entities;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.sid.compteservice.enums.TypeCompte;

import javax.persistence.*;
import java.util.Date;
@Entity
@Data @NoArgsConstructor @AllArgsConstructor
public class Compte {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long code;
private double solde;
private Date dateCreation;
@Enumerated(EnumType.STRING)
private TypeCompte type;
    • Enumérateurs:
package org.sid.compteservice.enums;

public enum TypeCompte {
COURANT,
EPARGNE
}
    • Interface CompteRepository basée sur Spring Data
Pour le mapping objet relationnel, nous allons utiliser Spring Data qui est un module de Spring qui permet de d'accéder aux données d'une application déclarative très simple en déclarant que des interface. Dans notre cas, Spring Data utilise implicitement la spécification JPA et son implémentation Hibernate pour le mapping objet relationnel du fait que nous utilisons une base de données relationnelle.
package org.sid.compteservice.repositories;

import org.sid.compteservice.entities.Compte;
import org.sid.compteservice.enums.TypeCompte;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;

import java.util.List;

@RepositoryRestResource
public interface CompteRepository extends JpaRepository<Compte,Long> {
@RestResource(path = "/byType")
List<Compte> findByType(@Param(value="type") TypeCompte typeCompte);
}
  • Couche Service :
Dans la couche service nous devrions implémenter toutes la logique métier de l'application. Dans notre cas on va limiter à une seule opération qui permet d'effectuer un virement bancaire d'un compte vers un autre.
    • Interface :
package org.sid.compteservice.service;

public interface CompteService {
void virement(Long codeSource,Long codeDestination, double montant);
}
    • Implémentation :
package org.sid.compteservice.service;

import org.sid.compteservice.entities.Compte;
import org.sid.compteservice.repositories.CompteRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class CompteServiceImpl implements CompteService {
@Autowired
private CompteRepository compteRepository;
@Override
public void virement(Long codeSource, Long codeDestination, double montant) {
Compte c1=compteRepository.getById(codeSource);
Compte c2=compteRepository.getById(codeDestination);
c1.setSolde(c1.getSolde()-montant);

c2.setSolde(c2.getSolde()+montant);
compteRepository.save(c1);
compteRepository.save(c2);
}
}
  • Couche Web :
Dans, nous allons créer les web services RESTful qui permet d'exposer les traitements de l'application pour un accès via le protocole HTTP avec un format JSON. Techniquement, il existe 3 solutions pour créer des API Restful :
    • En utilisant la spécification JEE JAXRS avec son implémentation Jersey. Ce qui est déconseillée si vous travaillez avec Spring, comme c'est notre cas
    • En utilisant un RestController en exploitant Spring MVC 
    • En utilisant Spring Data Rest : Un module de Spring qui offre un Web service générique qui exploite directement les interface Spring Data pour accéder aux données de l'application. Pour utiliser Spring Data Rest, il suffirait d'ajouter l'annotation @RepositoryRestResource aux interfaces Spring data (CompteRepository). Cependant, Spring Data Rest ne permet pas d'exploser les traitement de la couche métier via un accès REST. IL est pratique uniquement dans les cas des opération d'accès aux données de type CRUD. Autrement, il faut créer un RestController 
    • Data Rest Controller:
Ce qui j'appelle Data Rest Controller, un Restcontroller qui commnique directement avec des interface Spring Data pour accèder aux données de l'application avec des opérations CRUD. à vrai dire, ce genre de contrôleur peut remplacé simplement avec Un RestController basé sur Spring Data Rest, sans avoir besoin d'écrire son code. Voici un Exemple de Data Rest Controller
NB. Pour faire l'injection des dépendances, Spring recommande de le faire via le Constructeur au lieu de l'annotation @Autowired qui est dépréciée, même si elle fonctionne bien.
package org.sid.compteservice.web;

import org.sid.compteservice.entities.Compte;
import org.sid.compteservice.repositories.CompteRepository;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
public class CompteRestController {

private CompteRepository compteRepository;

public CompteRestController(CompteRepository compteRepository) {
this.compteRepository = compteRepository;
}
@GetMapping(path = "/comptes")
public List<Compte> listComptes(){
return compteRepository.findAll();
}

@GetMapping(path = "/comptes/{id}")
public Compte getCompte(@PathVariable(name = "id") Long code){
return compteRepository.findById(code).get();
}
@PostMapping(path="/comptes")
public Compte save(@RequestBody Compte compte){
System.out.println("********** Save **************");
System.out.println(compte.getCode());
System.out.println(compte.getSolde());
return compteRepository.save(compte);
}
@PutMapping(path="/comptes/{code}")
public Compte upadte(@PathVariable Long code,@RequestBody Compte compte){
compte.setCode(code);
return compteRepository.save(compte);
}
@DeleteMapping(path="/comptes/{code}")
public void dalete(@PathVariable Long code){
compteRepository.deleteById(code);
}
}
  • Business Rest Controller:
Ce que j'appelle Business Rest Controller, un RestController qui communique avec la couche service pour invoquer les traitement métiers. Dans l'exemple suivant, on crée un Rest controller qui permet d'effectuer un virement bancaire.
NB. 
Dans les bonnes pratiques, il faudrait éviter d'utiliser les entités JPA dans la couche Web, car ce sont des objets qui sont lourds et souvent stockent beaucoup d'attributs dont on n'a pas forcément besoin au niveau de la partie UI de l'application. Au lieu de cela, on utiliser des DTO (Data Transfer Object) qui sont des objet créé sur mesure aux requêtes et aux réponses du RestController. Du coût, pour chaque méthode du RestController, on crée un objet RequestDTO, qui va service pour récupérer les données de la requête HTTP et un autre ResponseDTO qui va servir pour stocker les données de la réponse HTTP. C'est ces objets DTOs qui représentent les Inputs et les Outputs de la couche service.  Par conséquent, les méthodes la couche service auront la charge de faire le mapping objet objet entre les Objets DTO et les Objet persistants (Entitités JPA). Pour ce faire, on peut se servir des Getters et Setters ou encore utiliser un Framework qui permet de faire le mapping objet objet d'une manière déclarative comme MapStruct, JMapper, etc. Dans cet exemple, nous créer un objet VirementRequestDTO avec un mapping manul dans la couche service avec les Getters et Setters.

package org.sid.compteservice.web;

import org.sid.compteservice.dtos.VirementRequestDTO;
import org.sid.compteservice.service.CompteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AccountRestController {
@Autowired
private CompteService compteService;

@PutMapping(path="comptes/virement")
public void virement(@RequestBody VirementRequestDTO request){
compteService.virement(request.getCodeSource(),request.getCodeDestination(),request.getMontant());
}
}
  • DTOs :
package org.sid.compteservice.dtos;

import lombok.Data;

@Data
public class VirementRequestDTO {
private Long codeSource;
private Long codeDestination;
private double montant;
}
  • Projections pour Spring Data Rest

Si vous utilisez un RestController basé sur Spring Data Rest, vous pouvez utiliser un mécanisme de Projections qui permet à un utilisateur de de préciser dans sa requête, quelle projection à utiliser pour sérialiser la ressource. Ci-dessous, nous définissons deux projections pour consulter un compte. La première permet de récupérer uniquement le code et le solde du compte et la deuxième permet au client de consulter que le solde et le type du compte. Pour faire appel à ces projections, on utilise le

paramètre url /comptes?projection=p1 ou /comptes?projection=p2

suivants, nous définissons

package org.sid.compteservice.entities;

import org.springframework.data.rest.core.config.Projection;

@Projection(name = "p1",types = Compte.class)
public interface CompteProj1 {
public Long getCode();
public double getSolde();
}
package org.sid.compteservice.entities;

import org.sid.compteservice.enums.TypeCompte;
import org.springframework.data.rest.core.config.Projection;

@Projection(name = "p2",types = Compte.class)
public interface CompteProj2 {
double getSolde();
TypeCompte getType();
}
  • Tests
    • API Docs

Consulter Les comptes


Ajout d'un compte


  • Clint REST
    • Structure du projet

    • Dépendances et plugin Maven

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.9.2</version>
</dependency>
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.9</version>
</dependency>
<dependency>
    <groupId>io.gsonfire</groupId>
    <artifactId>gson-fire</artifactId>
    <version>1.8.5</version>
</dependency>
<dependency>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator</artifactId>
    <version>5.2.0</version>
</dependency>
<dependency>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-cli</artifactId>
    <version>5.2.0</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>compile</scope>
</dependency>

 

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <!-- RELEASE_VERSION -->
    <version>5.2.0</version>
    <!-- /RELEASE_VERSION -->
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${project.basedir}/src/main/resources/api-docs.yaml</inputSpec>
                <generatorName>java</generatorName>
                <configOptions>
                    <sourceFolder>src/gen/java/main</sourceFolder>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

  • Génération du proxy à partir de api-docs.yaml

    • Code du Client Java
import org.openapitools.client.ApiException;
import org.openapitools.client.api.CompteRestControllerApi;
import org.openapitools.client.model.Compte;
import org.threeten.bp.OffsetDateTime;
import java.util.List;

public class Appli {
public static void main(String[] args) throws ApiException {
CompteRestControllerApi api=new CompteRestControllerApi();
Compte compte=api.getCompte(1L);
System.out.println(compte.getCode());
System.out.println(compte.getSolde());
System.out.println(compte.getDateCreation());
System.out.println(compte.getType().toString());


Compte cp=new Compte();
cp.setSolde(9000.0);
cp.setType(Compte.TypeEnum.COURANT);
cp.setDateCreation(OffsetDateTime.now());
api.save(cp);

compte.setSolde(1111.0);
api.upadte(1L,compte);

List<Compte> compteList=api.listComptes();

compteList.forEach(c->{
System.out.println(c.getCode());
System.out.println(c.getSolde());
System.out.println(c.getDateCreation());
System.out.println(c.getType().toString());
System.out.println("-----------------------");
});
//api.dalete(5L);
}
}



Commentaires

Posts les plus consultés de ce blog

Part 1- D’une Architecture Monolithique Vers une Architecture micro-services

Part 2 : D'une architecture monolithique vers une architecture micro-services