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
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
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
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 :
- 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 :
- 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:
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:
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
- Clint REST
- Structure du projet
- Dépendances et plugin Maven
<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
Enregistrer un commentaire