Chapitre 4: Architecture 3 Couches Professionnelle
Introduction
Ce chapitre synthétise tout ce que vous avez appris dans les chapitres précédents pour créer une architecture professionnelle, robuste et maintenable.
Vue d'Ensemble de l'Architecture
Responsabilités par Couche
| Couche | Rôle | Technologies | Annotations |
|---|---|---|---|
| Controller | Gestion HTTP, Validation, Conversion | Spring MVC | @RestController, @GetMapping |
| Service | Logique métier, Orchestration, Transactions | Spring Core | @Service, @Transactional |
| Repository | Accès données, Requêtes BD | Spring Data JPA | @Repository, JpaRepository |
| Exception Handler | Gestion erreurs, Formatage réponses | Spring MVC | @ControllerAdvice |
Projet Complet: Gestion de Produits
Structure des Packages
src/main/java/com/example/demo/
├── DemoApplication.java
├── controller/
│ └── ProductController.java
├── service/
│ └── ProductService.java
├── repository/
│ └── ProductRepository.java
├── model/
│ └── Product.java
└── exception/
├── ProductNotFoundException.java
├── InvalidProductException.java
├── ErrorResponse.java
└── GlobalExceptionHandler.java
1. Entité (Model)
package com.example.demo.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Le nom ne peut pas être vide")
@Size(min = 3, max = 50, message = "Le nom doit contenir entre 3 et 50 caractères")
private String name;
@NotNull(message = "Le prix est obligatoire")
@Min(value = 0, message = "Le prix ne peut pas être négatif")
private Double price;
@NotNull(message = "La quantité est obligatoire")
@Min(value = 0, message = "La quantité ne peut pas être négative")
private Integer quantity;
// Constructeurs
public Product() {}
public Product(String name, Double price, Integer quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }
}
2. Repository
package com.example.demo.repository;
import com.example.demo.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// Méthodes héritées: save, findById, findAll, deleteById
// Méthodes personnalisées
List<Product> findByName(String name);
List<Product> findByPriceLessThan(Double price);
}
3. Service
package com.example.demo.service;
import com.example.demo.exception.ProductNotFoundException;
import com.example.demo.model.Product;
import com.example.demo.repository.ProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// CREATE
public Product createProduct(Product product) {
// Validation métier supplémentaire si nécessaire
return productRepository.save(product);
}
// READ - Tous
public List<Product> getAllProducts() {
return productRepository.findAll();
}
// READ - Par ID
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
// UPDATE
public Product updateProduct(Long id, Product productDetails) {
Product product = getProductById(id);
product.setName(productDetails.getName());
product.setPrice(productDetails.getPrice());
product.setQuantity(productDetails.getQuantity());
return productRepository.save(product);
}
// DELETE
public void deleteProduct(Long id) {
Product product = getProductById(id);
productRepository.delete(product);
}
}
4. Controller
package com.example.demo.controller;
import com.example.demo.model.Product;
import com.example.demo.service.ProductService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
// CREATE
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
// READ - Tous
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
List<Product> products = productService.getAllProducts();
return ResponseEntity.ok(products);
}
// READ - Par ID
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}
// UPDATE
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@Valid @RequestBody Product product) {
Product updated = productService.updateProduct(id, product);
return ResponseEntity.ok(updated);
}
// DELETE
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}
5. Exceptions
package com.example.demo.exception;
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super("Product not found with id: " + id);
}
}
package com.example.demo.exception;
import java.time.LocalDateTime;
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
public ErrorResponse(int status, String error, String message, String path) {
this.timestamp = LocalDateTime.now();
this.status = status;
this.error = error;
this.message = message;
this.path = path;
}
// Getters et Setters (générés)
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
}
6. Gestionnaire d'Exceptions
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ErrorResponse> handleProductNotFound(
ProductNotFoundException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"Not Found",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(
Exception ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Internal Server Error",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
Configuration application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
path: /h2-console
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
server:
port: 8080
Tests avec Postman
1. CREATE - POST /api/products
Request:
POST http://localhost:8080/api/products
Content-Type: application/json
{
"name": "Laptop",
"price": 999.99,
"quantity": 10
}
Response: 201 Created
{
"id": 1,
"name": "Laptop",
"price": 999.99,
"quantity": 10
}
2. GET ALL - GET /api/products
Response: 200 OK
[
{
"id": 1,
"name": "Laptop",
"price": 999.99,
"quantity": 10
}
]
3. GET BY ID - GET /api/products/1
Response: 200 OK
{
"id": 1,
"name": "Laptop",
"price": 999.99,
"quantity": 10
}
4. UPDATE - PUT /api/products/1
Request:
{
"name": "Laptop Pro",
"price": 1299.99,
"quantity": 5
}
Response: 200 OK
{
"id": 1,
"name": "Laptop Pro",
"price": 1299.99,
"quantity": 5
}
5. DELETE - DELETE /api/products/1
Response: 204 No Content
Conventions de Nommage
Packages
com.example.demo.controller
com.example.demo.service
com.example.demo.repository
com.example.demo.model
com.example.demo.exception
Classes
- Controller:
ProductController,UserController - Service:
ProductService,UserService - Repository:
ProductRepository,UserRepository - Model:
Product,User - Exception:
ProductNotFoundException,InvalidProductException
Méthodes
- CREATE:
create...()ouadd...() - READ:
get...(),find...(),getAll...() - UPDATE:
update...() - DELETE:
delete...()
Résumé du Chapitre
Architecture Professionnelle
✅ Controller: Gestion HTTP uniquement
✅ Service: Logique métier et orchestration
✅ Repository: Accès aux données
✅ Exception Handler: Gestion globale des erreurs
Points Clés
- Séparation claire des responsabilités
- Injection par constructeur
- Validation à deux niveaux (syntaxique + métier)
- Gestion professionnelle des erreurs
- Codes HTTP appropriés
- Structure de projet standardisée