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.
numero()
proprietaire()
depotInitial()
equals()
hashCode()
toString()
Pas de setters : Comme les champs sont final, il est impossible d’ajouter des setters (le compilateur l’interdirait).
final
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é :
numero
proprietaire
depotInitial
BigDecimal
getNumero()
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 :
List
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.
equals
hashCode
Un record est immutable parce que :
C’est une garantie du langage, pas juste une convention.
@ResponseEntity est une annotation Spring qui permet de contrôler entièrement la réponse HTTP retournée par un contrôleur. Elle combine :
@ResponseEntity
200 OK
404 Not Found
201 Created
Content-Type
Location
@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 }
ResponseEntity.ok(compte)
compte
ResponseEntity.notFound().build()
Voici les alternatives courantes à @ResponseEntity pour retourner des réponses :
Spring convertit automatiquement l’objet en JSON (si jackson est dans le classpath) ou XML.
jackson
@GetMapping("/compte/{id}") public CreateCompteDTO getCompte(@PathVariable Long id) { return compteService.findById(id); // 200 OK + corps = compte (ou 404 si null) }
404
null
@RestController
@Controller
@ResponseBody
@RestController public class CompteController { @GetMapping("/compte/{id}") public CreateCompteDTO getCompte(@PathVariable Long id) { return compteService.findById(id); } }
Utile pour personnaliser le statut HTTP sans utiliser ResponseEntity.
ResponseEntity
@GetMapping("/compte/{id}") @ResponseStatus(HttpStatus.CREATED) // 201 Created public CreateCompteDTO createCompte() { return new CreateCompteDTO("123", "Alice"); }
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" } }
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()); }; }
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); }
Utile pour les méthodes qui ne retournent rien (ex: DELETE).
DELETE
@DeleteMapping("/compte/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) // 204 No Content public void deleteCompte(@PathVariable Long id) { compteService.delete(id); }
Object
return compte;
ResponseEntity<T>
return ResponseEntity.ok(compte);
String
return "compte";
void
@ResponseStatus
public void delete()
StreamingResponseBody
return outputStream -> {...};
Resource
return ResponseEntity.ok().body(file);
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 :
200
500
Les codes HTTP sont regroupés en 5 catégories, identifiées par leur premier chiffre :
100 Continue
101 Switching Protocols
204 No Content
301 Moved Permanently
302 Found
304 Not Modified
400 Bad Request
401 Unauthorized
500 Internal Server Error
503 Service Unavailable
GET /compte/123
POST
PUT
http://old.com
https://new.com
If-Modified-Since
POST /compte/123
GET
403 Forbidden
4xx
5xx
@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 }
@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 }
@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 } // ... }
@GetMapping("/comptes") public ResponseEntity<List<CreateCompteDTO>> getAllComptes() { try { return ResponseEntity.ok(compteService.findAll()); } catch (Exception e) { return ResponseEntity.internalServerError().build(); // 500 } }
GET /comptes/123
POST /comptes
PUT /comptes/123
DELETE /comptes/123
GET /comptes/999
409 Conflict
401
403
@ControllerAdvice
La classe avec l’annotation @ControllerAdvide
@ControllerAdvide
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(CompteNotFoundException.class) public ResponseEntity<String> handleNotFound(CompteNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); } }
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.
.yml
application.yml
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.
dev
Un profil Spring permet de charger des configurations différentes selon l’environnement :
test
prod
staging
Mais vous n’avez pas créé application-dev.yml !
application-dev.yml
Et bien, ce n’est pas obligatoire, Spring Boot fonctionne quand même.
La configuration ci-dessous :
signifie simplement : Le profil courant s’appelle dev.
Même s’il n’existe pas de fichier spécifique :
Spring utilise uniquement le fichier principal :
À quoi servent réellement les profils ?
Ils servent à avoir des configurations différentes.
Exemple typique :
# application-dev.yml spring: datasource: url: jdbc:mysql://localhost/dev_db
# application-prod.yml spring: datasource: url: jdbc:mysql://serveur-prod/prod_db
Comment Spring charge les fichiers ?
Spring charge toujours :
Puis, si un profil est actif :
application-{profil}.yml
Donc avec :
Spring cherche :
S’il n’existe pas, il ignore simplement.
Configuration commune :
spring: jpa: show-sql: true
Développement :
spring: datasource: url: jdbc:mysql://localhost/dev
Production :
spring: datasource: url: jdbc:mysql://prod-server/prod
Parce qu’en production :
On évite de modifier manuellement le fichier principal !
Peut-on supprimer le code ci-dessous ?
Oui. Si vous n’utilisez qu’une seule configuration, cela ne sert à rien.
java -jar app.jar --spring.profiles.active=prod
SPRING_PROFILES_ACTIVE=prod
L’idéal est de commencez sans profils, puis d’introduire :
application-prod.yml
plus tard.
spring.profiles.active=dev