Aller au contenu

Quelques notions déjà abordées

  1. Retour sur immutabilité des Records
  2. Retour sur la notion de ResponseEntity
  3. Complément sur les réponses HTTP
  4. Gestion des Profiles dans Spring Boot

1. Retour sur immutabilité des Records

En Java, un record est effectivement immutable par conception, et cette immutabilité est garantie par plusieurs mécanismes intégrés, pas seulement par l’absence de setters/getters.


1. Immutabilité par défaut

Pas de setters : Comme les champs sont final, il est impossible d’ajouter des setters (le compilateur l’interdirait).


2. Exemple avec ton CreateCompteDTO

public record CreateCompteDTO(String numero, String proprietaire, BigDecimal depotInitial) {
    // Constructeur personnalisé (appelle le constructeur canonique)
    public CreateCompteDTO(String numero, String proprietaire) {
        this(numero, proprietaire, null);
    }
}

Immutabilité :


3. Pourquoi pas de setters/getters classiques ?

Note : Si un champ est un objet mutable (ex: List, BigDecimal), l’immutabilité du record ne protège pas contre la modification de l’objet lui-même. Exemple :

var dto = new CreateCompteDTO("123", "Alice", new BigDecimal("100.00"));
dto.depotInitial().add(new BigDecimal("50")); // Ne modifie PAS depotInitial dans le record !
// Mais si depotInitial était une List, on pourrait modifier son contenu.

4. Avantages de l’immutabilité


En résumé

Un record est immutable parce que :

  1. Ses champs sont final (pas de modification après initialisation).
  2. Il n’a pas de setters (et ne peut pas en avoir).
  3. Les getters (méthodes d’accès) ne permettent que la lecture.

C’est une garantie du langage, pas juste une convention.

2. Retour sur la notion de ResponseEntity

@ResponseEntity : Explication simple

@ResponseEntity est une annotation Spring qui permet de contrôler entièrement la réponse HTTP retournée par un contrôleur. Elle combine :

Exemple d’utilisation

@GetMapping("/compte/{id}")
public ResponseEntity<CreateCompteDTO> getCompte(@PathVariable Long id) {
    CreateCompteDTO compte = compteService.findById(id);
    if (compte == null) {
        return ResponseEntity.notFound().build(); // 404
    }
    return ResponseEntity.ok(compte); // 200 + corps = compte
}


Autres annotations/utilisations dans un contrôleur Spring

Voici les alternatives courantes à @ResponseEntity pour retourner des réponses :


1. Retourner directement un objet (sérialisé en JSON/XML)

Spring convertit automatiquement l’objet en JSON (si jackson est dans le classpath) ou XML.

@GetMapping("/compte/{id}")
public CreateCompteDTO getCompte(@PathVariable Long id) {
    return compteService.findById(id); // 200 OK + corps = compte (ou 404 si null)
}

2. @ResponseBody (implicite avec @RestController)

@RestController
public class CompteController {
    @GetMapping("/compte/{id}")
    public CreateCompteDTO getCompte(@PathVariable Long id) {
        return compteService.findById(id);
    }
}

3. @ResponseStatus (pour forcer un statut HTTP)

Utile pour personnaliser le statut HTTP sans utiliser ResponseEntity.

@GetMapping("/compte/{id}")
@ResponseStatus(HttpStatus.CREATED) // 201 Created
public CreateCompteDTO createCompte() {
    return new CreateCompteDTO("123", "Alice");
}

4. Retourner une vue (Thymeleaf, JSP, etc.)

Si on utilise un @Controller (pas @RestController), on peut retourner le nom d’une vue (template HTML).

@Controller
public class CompteController {
    @GetMapping("/compte")
    public String afficherCompte(Model model) {
        model.addAttribute("compte", new CreateCompteDTO("123", "Alice"));
        return "compte"; // Cherche le template "compte.html"
    }
}

5. Retourner un flux de données (StreamingResponseBody)

Pour envoyer des données en streaming (ex: gros fichiers, flux vidéo).

@GetMapping("/compte/export")
public StreamingResponseBody exportComptes() {
    return outputStream -> {
        // Écrire des données dans outputStream (ex: CSV, PDF)
        outputStream.write("numéro,propriétaire\n".getBytes());
        outputStream.write("123,Alice\n".getBytes());
    };
}

6. Retourner un fichier (ResponseEntity<Resource>)

Pour télécharger un fichier (PDF, Excel, etc.).

@GetMapping("/compte/telecharger")
public ResponseEntity<Resource> telechargerCompte() {
    Path file = Paths.get("comptes.pdf");
    Resource resource = new UrlResource(file.toUri());
    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=comptes.pdf")
            .body(resource);
}

7. Retourner une réponse vide (void)

Utile pour les méthodes qui ne retournent rien (ex: DELETE).

@DeleteMapping("/compte/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) // 204 No Content
public void deleteCompte(@PathVariable Long id) {
    compteService.delete(id);
}

Tableau récapitulatif

Type de retour Statut HTTP par défaut Cas d’usage Exemple
Object (ex: CreateCompteDTO) 200 OK Retourner des données JSON/XML return compte;
ResponseEntity<T> Personnalisable Contrôle total (statut + en-têtes) return ResponseEntity.ok(compte);
String (avec @Controller) 200 OK Retourner une vue (Thymeleaf/JSP) return "compte";
void 200 OK (ou 204 si @ResponseStatus) Méthodes sans retour (ex: DELETE) public void delete()
StreamingResponseBody 200 OK Streaming de données return outputStream -> {...};
Resource/ResponseEntity<Resource> 200 OK Téléchargement de fichiers return ResponseEntity.ok().body(file);

Quand utiliser quoi ?

3. Complément sur les réponses HTTP

Explication des codes et statuts HTTP

Les codes de statut HTTP (comme 200, 404, 500, etc.) sont des réponses standardisées envoyées par un serveur pour indiquer le résultat d’une requête HTTP. Ils sont essentiels pour :


Les 5 classes de statuts HTTP

Les codes HTTP sont regroupés en 5 catégories, identifiées par leur premier chiffre :

Classe Plage Signification Exemples
1xx 100–199 Information (requête reçue, traitement en cours) 100 Continue, 101 Switching Protocols
2xx 200–299 Succès (requête traitée avec succès) 200 OK, 201 Created, 204 No Content
3xx 300–399 Redirection (ressource déplacée) 301 Moved Permanently, 302 Found, 304 Not Modified
4xx 400–499 Erreur client (requête invalide) 400 Bad Request, 401 Unauthorized, 404 Not Found
5xx 500–599 Erreur serveur (problème côté serveur) 500 Internal Server Error, 503 Service Unavailable


Les codes HTTP les plus courants

2xx : Succès

Code Nom Description Quand l’utiliser ?
200 OK Requête réussie. Le corps contient les données demandées. Retour d’une ressource (ex: GET /compte/123).
201 Created Ressource créée avec succès. Doit inclure un en-tête Location avec l’URL de la nouvelle ressource. Après un POST (ex: création d’un compte).
204 No Content Requête réussie, mais pas de corps à retourner. Après un DELETE ou une mise à jour (PUT) sans retour de données.
202 Accepted Requête acceptée, mais traitement asynchrone (ex: tâche en arrière-plan). Pour les opérations longues (ex: génération d’un rapport).

3xx : Redirection

Code Nom Description Quand l’utiliser ?
301 Moved Permanently La ressource a été déplacée définitivement. Le client doit utiliser la nouvelle URL. Changement d’URL permanent (ex: http://old.comhttps://new.com).
302 Found (Temporary Redirect) Redirection temporaire. Le client peut continuer à utiliser l’ancienne URL. Maintenance temporaire, A/B testing.
304 Not Modified La ressource n’a pas changé (cache valide). Pour optimiser le cache (ex: If-Modified-Since).

4xx : Erreurs client

Code Nom Description Quand l’utiliser ?
400 Bad Request La requête est mal formée (syntaxe incorrecte, paramètres manquants). Données invalides envoyées par le client (ex: champ obligatoire manquant).
401 Unauthorized Authentification requise (le client n’est pas authentifié). Accès à une ressource protégée sans token valide.
403 Forbidden Accès interdit (le client est authentifié mais n’a pas les droits). Utilisateur connecté mais sans permission (ex: rôle insuffisant).
404 Not Found La ressource n’existe pas. URL invalide ou ressource supprimée.
405 Method Not Allowed La méthode HTTP n’est pas autorisée pour cette URL. Ex: POST /compte/123 alors que seul GET est autorisé.
409 Conflict Conflit (ex: ressource déjà existante). Tentative de création d’un compte avec un email déjà utilisé.
422 Unprocessable Entity La requête est syntactiquement correcte mais sémantiquement invalide. Validation des données échouée (ex: format d’email invalide).

5xx : Erreurs serveur

Code Nom Description Quand l’utiliser ?
500 Internal Server Error Erreur interne du serveur (bug non géré). Exception non capturée dans le code.
501 Not Implemented La fonctionnalité n’est pas implémentée. Méthode HTTP non supportée par le serveur.
502 Bad Gateway Mauvaise réponse d’un serveur amont (ex: proxy, microservice). Problème avec un service externe (ex: base de données inaccessible).
503 Service Unavailable Le serveur est temporairement indisponible (surcharge, maintenance). Serveur en maintenance ou trop de requêtes.
504 Gateway Timeout Timeout lors de la communication avec un serveur amont. Un microservice met trop de temps à répondre.



Sont-ils obligatoires ?

Oui, mais

  1. Obligation technique :
    • Tout serveur HTTP doit retourner un code de statut (même 200 OK par défaut).
    • Sans code de statut, le client (navigateur, application) ne saura pas comment interpréter la réponse.
  2. Obligation fonctionnelle :
    • Pour une API REST, les codes HTTP sont indispensables pour :
      • Standardiser les réponses (ex: 201 Created pour une création, 404 pour une ressource introuvable).
      • Permettre aux clients de gérer les erreurs (ex: afficher un message “Ressource introuvable” pour un 404).
      • Optimiser les performances (ex: 304 Not Modified pour éviter de renvoyer des données inutiles).
  3. Bonnes pratiques :
    • Toujours utiliser le code le plus précis (ex: 401 Unauthorized au lieu de 403 Forbidden si le problème est l’authentification).
    • Éviter le 200 OK pour les erreurs (préférer 4xx ou 5xx).
    • Documenter les codes retournés dans la documentation de l’API (ex: Swagger/OpenAPI).

Exemples concrets en Java (Spring)

1. Retourner un 201 Created avec ResponseEntity

@PostMapping("/comptes")
public ResponseEntity<CreateCompteDTO> createCompte(@RequestBody CreateCompteDTO dto) {
    CreateCompteDTO newCompte = compteService.create(dto);
    URI location = ServletUriComponentsBuilder.fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(newCompte.numero())
            .toUri();
    return ResponseEntity.created(location).body(newCompte); // 201 + Location header
}

2. Retourner un 404 Not Found

@GetMapping("/comptes/{id}")
public ResponseEntity<CreateCompteDTO> getCompte(@PathVariable String id) {
    CreateCompteDTO compte = compteService.findById(id);
    if (compte == null) {
        return ResponseEntity.notFound().build(); // 404
    }
    return ResponseEntity.ok(compte); // 200
}

3. Retourner un 400 Bad Request avec un message

@PostMapping("/comptes")
public ResponseEntity<String> createCompte(@RequestBody CreateCompteDTO dto) {
    if (dto.numero() == null) {
        return ResponseEntity.badRequest().body("Le numéro de compte est obligatoire."); // 400
    }
    // ...
}

4. Retourner un 500 Internal Server Error (à éviter en production !)

@GetMapping("/comptes")
public ResponseEntity<List<CreateCompteDTO>> getAllComptes() {
    try {
        return ResponseEntity.ok(compteService.findAll());
    } catch (Exception e) {
        return ResponseEntity.internalServerError().build(); // 500
    }
}

Résumé : Quand utiliser quel code ?

Opération Code HTTP recommandé Exemple
Lecture réussie 200 OK GET /comptes/123
Création réussie 201 Created POST /comptes
Mise à jour réussie 200 OK ou 204 No Content PUT /comptes/123
Suppression réussie 204 No Content DELETE /comptes/123
Requête invalide 400 Bad Request Données manquantes ou mal formatées
Non autorisé 401 Unauthorized Token manquant ou invalide
Accès interdit 403 Forbidden Utilisateur sans droits suffisants
Ressource introuvable 404 Not Found GET /comptes/999 (n’existe pas)
Conflit 409 Conflict Email déjà utilisé
Erreur serveur 500 Internal Server Error Bug non géré

Conseils pour bien les utiliser

  1. Sois précis : Utilise le code le plus adapté à la situation (ex: 401 pour l’authentification, 403 pour les permissions).
  2. Documente ton API : Indique dans ta documentation (Swagger, README) quels codes peuvent être retournés par chaque endpoint.
  3. Gère les erreurs : Utilise @ControllerAdvice pour centraliser la gestion des exceptions et retourner les bons codes.

La classe avec l’annotation @ControllerAdvide

   @ControllerAdvice
   public class GlobalExceptionHandler {
       @ExceptionHandler(CompteNotFoundException.class)
       public ResponseEntity<String> handleNotFound(CompteNotFoundException e) {
           return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
       }
   }
  1. Évite le 200 OK pour les erreurs : Un client s’attend à ce que 200 signifie “tout va bien”. Pour une erreur, utilise 4xx ou 5xx.
  2. Teste tes endpoints : Vérifie que tes endpoints retournent bien les codes attendus (avec Postman, cURL, ou des tests unitaires).

Ressources utiles


4. Gestion des Profiles dans Spring Boot

Vous avez du vous rendre compte de la présence d’un ou plusieurs fichiers .yml dans un projet Spring Boot. Voici quelques explications en partant du fichier application.yml de l’application Restaurant-app.

Voici ce que vous pouvez avoir dans le fichier principal application.yml :

spring:
  profiles:
    active: dev

Cela indique à Spring Boot : Le profil actif de l’application est dev.

Un profil Spring permet de charger des configurations différentes selon l’environnement :


Mais vous n’avez pas créé application-dev.yml !

Et bien, ce n’est pas obligatoire, Spring Boot fonctionne quand même.

La configuration ci-dessous :

spring:
  profiles:
    active: dev

signifie simplement : Le profil courant s’appelle dev.

Même s’il n’existe pas de fichier spécifique :

application-dev.yml

Spring utilise uniquement le fichier principal :

application.yml

À quoi servent réellement les profils ?

Ils servent à avoir des configurations différentes.

Exemple typique :

Développement

# application-dev.yml
spring:
  datasource:
    url: jdbc:mysql://localhost/dev_db

Production

# application-prod.yml
spring:
  datasource:
    url: jdbc:mysql://serveur-prod/prod_db

Comment Spring charge les fichiers ?

Spring charge toujours :

application.yml

Puis, si un profil est actif :

application-{profil}.yml

Donc avec :

spring:
  profiles:
    active: dev

Spring cherche :

application-dev.yml

S’il n’existe pas, il ignore simplement.


Structure classique

application.yml

Configuration commune :

spring:
  jpa:
    show-sql: true

application-dev.yml

Développement :

spring:
  datasource:
    url: jdbc:mysql://localhost/dev

application-prod.yml

Production :

spring:
  datasource:
    url: jdbc:mysql://prod-server/prod

Pourquoi c’est utile ?

Parce qu’en production :

On évite de modifier manuellement le fichier principal !


Peut-on supprimer le code ci-dessous ?

spring:
  profiles:
    active: dev

Oui. Si vous n’utilisez qu’une seule configuration, cela ne sert à rien.


Comment changer de profil ?

Ligne de commande

java -jar app.jar --spring.profiles.active=prod

Variable d’environnement

SPRING_PROFILES_ACTIVE=prod

L’idéal est de commencez sans profils, puis d’introduire :

plus tard.


Résumé

Élément Rôle
application.yml configuration commune
application-dev.yml configuration développement
application-prod.yml configuration production
spring.profiles.active=dev active le profil dev