Chapitre 19
19 - Corrections
> Corrigés détaillés des 40 exercices du chapitre 17, avec code Java complet et explications
Chapitre 19 — Corrections : 40 corriges detailles
Solutions completes et commentees des 40 exercices du chapitre 17
Correction 1 — Streams : Filter, Map, Reduce
import java.util.*;
import java.util.stream.*;
record Transaction(String type, double amount, String category) {}
public class TransactionProcessor {
public static OptionalDouble calculateCreditTotal(List<Transaction> transactions) {
return transactions.stream()
.filter(t -> "CREDIT".equals(t.type()))
.filter(t -> t.amount() > 100.0)
.mapToDouble(Transaction::amount)
.reduce(Double::sum);
}
}
Analyse : Pipeline en 3 etapes. filter type, filter montant, reduce somme. Equivalent a sum() sur DoubleStream.
Correction 2 — Streams : FlatMap et groupingBy
import java.util.*;
import java.util.stream.*;
record Article(String name, String category, int quantity) {}
record Order(String id, List<Article> articles) {}
public class SalesAnalyzer {
public static Map<String, Integer> totalQuantityByCategory(List<Order> orders) {
return orders.stream()
.flatMap(order -> order.articles().stream())
.collect(Collectors.groupingBy(
Article::category,
Collectors.summingInt(Article::quantity)
));
}
}
Analyse : flatMap aplatit les articles. groupingBy + summingInt agrege par categorie.
Correction 3 — Optional : Chainage et transformation
import java.util.*;
record User(String id, String email) {}
record Profile(String userId, String city) {}
record Address(String street, String city, String zipCode) {}
public class UserService {
public static String findCityByEmail(
String email,
Function<String, Optional<User>> findByEmail,
Function<User, Optional<Profile>> getProfile,
Function<Profile, Optional<Address>> getAddress) {
return findByEmail.apply(email)
.flatMap(getProfile).flatMap(getAddress)
.map(Address::city).orElse("Unknown");
}
}
Analyse : Chainage fonctionnel sans ifPresent. flatMap deroule les Optional.
Correction 4 — Streams : Partitionnement et statistiques
import java.util.*;
import java.util.stream.*;
record Employee(String name, double salary) {}
public class SalaryAnalyzer {
public static Map<Boolean, DoubleSummaryStatistics> partitionBySalary(
List<Employee> employees, double threshold) {
return employees.stream()
.collect(Collectors.partitioningBy(
e -> e.salary() > threshold,
Collectors.summarizingDouble(Employee::salary)
));
}
}
Analyse : partitioningBy cree deux groupes. summarizingDouble donne count/sum/min/avg/max.
Correction 5 — Streams : Custom Collector
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class CustomCollectors {
public static <T> Collector<T, ?, String> customJoining(
String delimiter, String prefix, String suffix) {
return Collector.of(
ArrayList::new,
(list, el) -> { list.add(el); },
(l1, l2) -> { l1.addAll(l2); return l1; },
list -> prefix + String.join(delimiter, list) + suffix
);
}
}
Analyse : Supplier, accumulator, combiner, finisher. Version simplifiee avec String.join.
Correction 6 — CompletableFuture : Appels paralleles
import java.util.concurrent.*;
import java.util.*;
record Price(double value) {}
record Stock(int available) {}
record Review(double rating, int count) {}
record ProductInfo(Price price, Stock stock, Review review) {}
public class ParallelService {
private static final ExecutorService executor = Executors.newFixedThreadPool(3);
public CompletableFuture<ProductInfo> getProductInfo(String productId) {
CompletableFuture<Price> priceF = supplyAsync(() -> fetchPrice(productId));
CompletableFuture<Stock> stockF = supplyAsync(() -> fetchStock(productId));
CompletableFuture<Review> reviewF = supplyAsync(() -> fetchReviews(productId));
return allOf(priceF, stockF, reviewF)
.thenApply(v -> new ProductInfo(priceF.join(), stockF.join(), reviewF.join()));
}
private <T> CompletableFuture<T> supplyAsync(Supplier<T> task) {
return CompletableFuture.supplyAsync(task, executor);
}
private Price fetchPrice(String id) { return new Price(29.99); }
private Stock fetchStock(String id) { return new Stock(42); }
private Review fetchReviews(String id) { return new Review(4.5, 128); }
}
Analyse : allOf attend tous les futures. join est sur apres completion.
Correction 7 — CompletableFuture : Timeout et fallback
import java.util.concurrent.*;
record Product(String id, String name) {
static final Product DEFAULT = new Product("unknown", "Default Product");
}
public class ProductService {
public CompletableFuture<Product> fetchWithTimeout(String id, long timeout, TimeUnit unit) {
return CompletableFuture.supplyAsync(() -> callRemote(id))
.orTimeout(timeout, unit)
.exceptionally(ex -> { System.err.println("Fallback: " + ex.getMessage());
return Product.DEFAULT; });
}
private Product callRemote(String id) {
try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return new Product(id, "Real");
}
}
Analyse : orTimeout (Java 9+) leve TimeoutException. exceptionally fournit la valeur de fallback.
Correction 8 — CompletableFuture : Pipeline asynchrone
import java.util.concurrent.*;
record Order(String id, String status, String details) {}
public class OrderPipeline {
public CompletableFuture<Void> processOrder(Order order) {
return CompletableFuture.completedFuture(order)
.thenCompose(this::validate)
.thenCompose(this::enrich)
.thenCompose(this::save)
.thenAccept(this::notify);
}
private CompletableFuture<Order> validate(Order o) {
return CompletableFuture.supplyAsync(() -> {
if (o.details() == null || o.details().isBlank())
throw new IllegalArgumentException("Invalid");
return new Order(o.id(), "VALIDATED", o.details());
});
}
private CompletableFuture<Order> enrich(Order o) {
return CompletableFuture.supplyAsync(() -> new Order(o.id(), o.status(), o.details() + " [enriched]"));
}
private CompletableFuture<Order> save(Order o) {
return CompletableFuture.supplyAsync(() -> new Order(o.id(), "SAVED", o.details()));
}
private void notify(Order o) { System.out.println("Order " + o.id() + " processed"); }
}
Analyse : thenCompose chaine les CompletableFuture. L'erreur interrompt le pipeline.
Correction 9 — CompletableFuture : Rate Limiting
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public class BatchDownloader {
public List<String> downloadAll(List<String> urls, int maxConcurrency) {
ExecutorService executor = Executors.newFixedThreadPool(maxConcurrency);
List<CompletableFuture<String>> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> download(url), executor))
.toList();
allOf(futures.toArray(new CompletableFuture[0])).join();
executor.shutdown();
return futures.stream().map(f -> f.getNow(null)).filter(Objects::nonNull).toList();
}
private String download(String url) { return "Content of " + url; }
}
Analyse : FixedThreadPool limite la concurrence. allOf attend toutes les taches.
Correction 10 — CompletableFuture : Composition conditionnelle
import java.util.concurrent.*;
import java.util.function.*;
public class CacheAsideService<T> {
private final Function<String, T> remoteLoader;
private final Function<String, T> cacheLoader;
private final BiConsumer<String, T> cacheWriter;
private final Executor ex = Executors.newFixedThreadPool(2);
public CacheAsideService(Function<String, T> r, Function<String, T> c, BiConsumer<String, T> w) {
this.remoteLoader = r; this.cacheLoader = c; this.cacheWriter = w;
}
public CompletableFuture<T> get(String key) {
return CompletableFuture.supplyAsync(() -> cacheLoader.apply(key), ex)
.thenCompose(cached -> cached != null
? CompletableFuture.completedFuture(cached)
: loadRemote(key));
}
private CompletableFuture<T> loadRemote(String key) {
return CompletableFuture.supplyAsync(() -> remoteLoader.apply(key), ex)
.orTimeout(3, TimeUnit.SECONDS)
.thenApply(v -> { cacheWriter.accept(key, v); return v; })
.exceptionally(ex -> remoteLoader.apply(key));
}
}
Analyse : Cache-aside pattern. Verifie cache puis remote avec fallback et retry.
Correction 11 — JPA : Entity Mapping avance
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.*;
@Entity @Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false) private String name;
@Column(unique = true, nullable = false) private String email;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "user_id")
private List<Address> addresses = new ArrayList<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Order> orders = new ArrayList<>();
}
@Embeddable
public class Address {
@Column(nullable = false) private String street;
@Column(nullable = false) private String city;
private String zipCode;
@Enumerated(EnumType.STRING) private AddressType type;
public enum AddressType { LIVRAISON, FACTURATION }
}
@Entity @Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private LocalDateTime orderDate = LocalDateTime.now();
@Enumerated(EnumType.STRING) private OrderStatus status = OrderStatus.PENDING;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
public enum OrderStatus { PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED }
}
@Entity @Table(name = "order_items")
public class OrderItem {
@Id @GeneratedValue private Long id;
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "order_id") private Order order;
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id") private Product product;
@Column(nullable = false) private int quantity;
@Column(nullable = false) private double unitPrice;
}
@Entity @Table(name = "products")
public class Product {
@Id @GeneratedValue private Long id;
@Column(nullable = false) private String name;
@Column(nullable = false) private double price;
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "category_id") private Category category;
}
Analyse : Modele relationnel complet. orphanRemoval=true, FetchType.LAZY, @Enumerated(STRING).
Correction 12 — JPA : Criteria API dynamique
import jakarta.persistence.*;
import jakarta.persistence.criteria.*;
import java.util.*;
public class ProductRepositoryCustom {
@PersistenceContext private EntityManager em;
public List<Product> search(String name, String cat, Double minP, Double maxP, Boolean stock) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Product> cq = cb.createQuery(Product.class);
Root<Product> root = cq.from(Product.class);
List<Predicate> preds = new ArrayList<>();
if (name != null && !name.isBlank())
preds.add(cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%"));
if (cat != null) preds.add(cb.equal(root.get("category").get("name"), cat));
if (minP != null) preds.add(cb.ge(root.get("price"), minP));
if (maxP != null) preds.add(cb.le(root.get("price"), maxP));
if (stock != null) preds.add(cb.equal(root.get("inStock"), stock));
cq.where(preds.toArray(new Predicate[0]));
return em.createQuery(cq).getResultList();
}
}
Analyse : Criteria API construit la requete dynamiquement. Chaque predicat est optionnel.
Correction 13 — JPA : Pagination Spring Data
import org.springframework.data.domain.*;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.*;
import jakarta.persistence.criteria.*;
public interface ProductRepository extends JpaRepository<Product, Long>,
JpaSpecificationExecutor<Product> {
static Specification<Product> withFilter(ProductFilter f) {
return (root, query, cb) -> {
List<Predicate> p = new ArrayList<>();
if (f.name() != null) p.add(cb.like(root.get("name"), "%" + f.name() + "%"));
if (f.category() != null) p.add(cb.equal(root.get("category"), f.category()));
if (f.minPrice() != null) p.add(cb.ge(root.get("price"), f.minPrice()));
if (f.maxPrice() != null) p.add(cb.le(root.get("price"), f.maxPrice()));
return cb.and(p.toArray(new Predicate[0]));
};
}
default Page<Product> findWithFilter(ProductFilter f, Pageable p) { return findAll(withFilter(f), p); }
}
record ProductFilter(String name, String category, Double minPrice, Double maxPrice) {}
Analyse : Specification + Pageable. Flexible et type-safe.
Correction 14 — JPA : Optimistic Locking
import jakarta.persistence.*;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
@Entity
public class Ticket {
@Id @GeneratedValue private Long id;
@Version private long version;
@Column(nullable = false) private String eventName;
@Column(nullable = false) private int availableSeats;
public boolean reserve(int q) {
if (availableSeats >= q) { availableSeats -= q; return true; }
return false;
}
}
@Service
public class TicketService {
@PersistenceContext private EntityManager em;
@Transactional
public boolean reserveTickets(Long ticketId, int quantity, int maxRetries) {
for (int i = 0; i < maxRetries; i++) {
try {
Ticket t = em.find(Ticket.class, ticketId);
if (t.reserve(quantity)) return true;
return false;
} catch (ObjectOptimisticLockingFailureException e) {
if (i == maxRetries - 1) throw e;
}
}
return false;
}
}
Analyse : @Version pour optimistic locking. Retry avec donnees fraiches en cas de conflit.
Correction 15 — Spring Boot REST : CRUD
import jakarta.validation.Valid;
import org.springframework.data.domain.*;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestController @RequestMapping("/api/products")
public class ProductController {
private final ProductService service;
public ProductController(ProductService service) { this.service = service; }
@GetMapping
public ResponseEntity<Page<Product>> list(
@RequestParam(defaultValue="0") int page,
@RequestParam(defaultValue="20") int size,
@RequestParam(defaultValue="name,asc") String[] sort) {
Sort.Direction dir = sort[1].equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC;
return ResponseEntity.ok(service.findAll(PageRequest.of(page, size, Sort.by(dir, sort[0]))));
}
@GetMapping("/{id}")
public ResponseEntity<Product> getById(@PathVariable Long id) {
return service.findById(id).map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Product> create(@Valid @RequestBody Product p) {
return ResponseEntity.status(HttpStatus.CREATED).body(service.save(p));
}
@PutMapping("/{id}")
public ResponseEntity<Product> update(@PathVariable Long id, @Valid @RequestBody Product p) {
return service.update(id, p).map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
service.softDelete(id);
return ResponseEntity.noContent().build();
}
}
Analyse : RESTful design. Pagination, validation @Valid, codes HTTP standards.
Correction 16 — Validation avancee
import jakarta.validation.*;
import jakarta.validation.constraints.*;
import java.time.LocalDate;
import java.util.List;
public class CreateOrderRequest {
@NotBlank private String userId;
@NotEmpty @Size(min=1) private List<@Valid OrderItemRequest> items;
@Future private LocalDate deliveryDate;
@Valid private AddressRequest deliveryAddress;
@AssertTrue(message = "Delivery address required if delivery date is set")
public boolean isValidDelivery() {
return deliveryDate == null || deliveryAddress != null;
}
}
class OrderItemRequest {
@NotBlank private String productId;
@Positive private int quantity;
@Positive private double unitPrice;
}
class AddressRequest {
@NotBlank private String street;
@NotBlank private String city;
@NotBlank private String zipCode;
}
Analyse : Validation cross-champ avec @AssertTrue. Validation en cascade @Valid.
Correction 17 — Gestion globale des exceptions
import org.springframework.http.*;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.*;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
List<FieldErrorDetail> errors = ex.getBindingResult().getFieldErrors().stream()
.map(e -> new FieldErrorDetail(e.getField(), e.getDefaultMessage(), e.getRejectedValue()))
.toList();
return ResponseEntity.badRequest()
.body(new ErrorResponse(LocalDateTime.now(), 400, "Validation Failed", errors));
}
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(LocalDateTime.now(), 404, ex.getMessage(), List.of()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(LocalDateTime.now(), 500, "Internal error", List.of()));
}
}
record ErrorResponse(LocalDateTime ts, int status, String message, List<FieldErrorDetail> errors) {}
record FieldErrorDetail(String field, String message, Object rejected) {}
Analyse : Handler centralise. Reponses JSON structurees avec status codes appropriés.
Correction 18 — HATEOAS
import org.springframework.hateoas.*;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
@RestController @RequestMapping("/api/products")
public class ProductHateoasController {
private final ProductService service;
@GetMapping("/{id}")
public ResponseEntity<EntityModel<Product>> getById(@PathVariable Long id) {
return service.findById(id).map(p -> {
EntityModel<Product> m = EntityModel.of(p);
m.add(linkTo(methodOn(ProductHateoasController.class).getById(id)).withSelfRel());
m.add(linkTo(methodOn(ProductHateoasController.class).list(0,20,new String[]{"name,asc"})).withRel("products"));
return ResponseEntity.ok(m);
}).orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<CollectionModel<EntityModel<Product>>> list(
@RequestParam int page, @RequestParam int size, @RequestParam String[] sort) {
Page<Product> products = service.findAll(PageRequest.of(page, size));
List<EntityModel<Product>> models = products.getContent().stream()
.map(p -> EntityModel.of(p, linkTo(methodOn(ProductHateoasController.class).getById(p.getId())).withSelfRel()))
.toList();
CollectionModel<EntityModel<Product>> cm = CollectionModel.of(models);
cm.add(linkTo(methodOn(ProductHateoasController.class).list(page, size, sort)).withSelfRel());
if (products.hasPrevious())
cm.add(linkTo(methodOn(ProductHateoasController.class).list(page-1, size, sort)).withRel("prev"));
if (products.hasNext())
cm.add(linkTo(methodOn(ProductHateoasController.class).list(page+1, size, sort)).withRel("next"));
return ResponseEntity.ok(cm);
}
}
Analyse : HATEOAS REST maturity level 3. Liens self, collection, pagination.
Correction 19 — JUnit 5 : Tests parametres
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;
class ISBNValidatorTest {
private final ISBNValidator v = new ISBNValidator();
@ParameterizedTest
@ValueSource(strings = {"978-0-13-468599-1", "978-0-321-34960-6", "978-1-61729-757-2"})
void validISBNs(String isbn) { assertTrue(v.isValidISBN(isbn)); }
@ParameterizedTest
@ValueSource(strings = {"1234567890", "invalid", "", "978-x-00-000000-0"})
void invalidISBNs(String isbn) { assertFalse(v.isValidISBN(isbn)); }
@ParameterizedTest
@CsvSource({"978-0-13-468599-1,true", "invalid,false", "0-201-63361-2,true"})
void csvTest(String isbn, boolean expected) { assertEquals(expected, v.isValidISBN(isbn)); }
@ParameterizedTest
@MethodSource("edges")
void edges(String isbn, boolean expected) { assertEquals(expected, v.isValidISBN(isbn)); }
static Stream<Arguments> edges() {
return Stream.of(Arguments.of(null, false), Arguments.of("", false),
Arguments.of("9780141036144", true));
}
}
Analyse : Tests parametres avec ValueSource, CsvSource, MethodSource. Couverture des cas limites.
Correction 20 — Mockito : Mock avance
import org.junit.jupiter.api.*;
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
class OrderServiceTest {
@Mock PaymentGateway paymentGateway;
@Mock InventoryService inventoryService;
@Mock NotificationService notificationService;
@InjectMocks OrderService orderService;
@Captor ArgumentCaptor<PaymentRequest> paymentCaptor;
@BeforeEach void setup() { MockitoAnnotations.openMocks(this); }
@Test
void shouldProcessSuccessfully() {
when(inventoryService.checkStock(any())).thenReturn(true);
when(paymentGateway.charge(any())).thenReturn(new PaymentResult("txn_1", SUCCESS));
OrderResult r = orderService.placeOrder(new Order("123", 99.99));
assertEquals(CONFIRMED, r.status());
verify(paymentGateway).charge(paymentCaptor.capture());
assertEquals(99.99, paymentCaptor.getValue().amount());
}
@Test
void shouldRollbackOnFailure() {
when(paymentGateway.charge(any())).thenThrow(new PaymentException("Failed"));
assertThrows(PaymentException.class, () -> orderService.placeOrder(new Order("456", 50)));
verify(inventoryService).releaseStock(any());
verify(notificationService, never()).sendConfirmation(any());
}
}
Analyse : Mockito stubbing, verification, ArgumentCaptor. Test de rollback et interactions.
Correction 21 — Testcontainers : PostgreSQL
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.*;
import static org.assertj.core.api.Assertions.*;
@Testcontainers @DataJpaTest
class UserRepositoryTest {
@Container @ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Autowired private UserRepository repo;
@Test void shouldSaveUser() {
User user = repo.save(new User("john@test.com", "John"));
assertThat(user.getId()).isNotNull();
}
@Test void shouldEnforceUniqueEmail() {
repo.save(new User("dup@test.com", "A"));
assertThatThrownBy(() -> repo.save(new User("dup@test.com", "B")))
.isInstanceOf(DataIntegrityViolationException.class);
}
}
Analyse : Testcontainers + @ServiceConnection (Spring 3.2+). Tests avec vraie base PostgreSQL.
Correction 22 — Testcontainers : Kafka
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.kafka.core.*;
import org.testcontainers.junit.jupiter.*;
import org.testcontainers.kafka.KafkaContainer;
import org.testcontainers.utility.DockerImageName;
@Testcontainers @SpringBootTest
class KafkaIntegrationTest {
@Container
static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0"));
@Autowired private KafkaTemplate<String, String> template;
@Test void shouldSendAndReceive() {
template.send("test-topic", "Hello Kafka!");
}
}
Analyse : Testcontainers demarre un vrai broker Kafka pour test d'integration realiste.
Correction 23 — ArchUnit : Tests d'architecture
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
@AnalyzeClasses(packages = "com.example")
public class ArchitectureTest {
@ArchTest
static final ArchRule servicesDontDependOnControllers =
noClasses().that().resideInAPackage("..service..")
.should().dependOnClassesThat().resideInAPackage("..controller..");
@ArchTest
static final ArchRule reposAreInterfaces =
classes().that().resideInAPackage("..repository..")
.should().beInterfaces();
@ArchTest
static final ArchRule noCycles = slices().matching("com.example.(*)..")
.should().beFreeOfCycles();
}
Analyse : ArchUnit verifie l'architecture au build. Previent les violations de structure.
Correction 24 — Feign Client
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(name = "catalog-service", url = "${catalog.service.url}")
public interface CatalogClient {
@GetMapping("/api/products/{id}")
Product getProduct(@PathVariable String id);
}
@Configuration
public class FeignConfig {
@Bean
public Request.Options options() {
return new Request.Options(5000, TimeUnit.MILLISECONDS, 10000, TimeUnit.MILLISECONDS, true);
}
@Bean
public Retryer retryer() { return new Retryer.Default(100, 1000, 3); }
}
Analyse : Client Feign declaratif avec timeouts, retry et configuration.
Correction 25 — Resilience4j : Circuit Breaker
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallback")
public PaymentResult processPayment(PaymentRequest req) { return callExternalApi(req); }
public PaymentResult fallback(PaymentRequest req, Throwable t) {
return new PaymentResult("fallback", PENDING);
}
}
// application.yml:
// resilience4j.circuitbreaker.instances.paymentService:
// slidingWindowSize: 10
// failureRateThreshold: 50
// waitDurationInOpenState: 5s
// permittedNumberOfCallsInHalfOpenState: 3
Analyse : Circuit Breaker evite les appels a un service defaillant. Fallback en mode degrade.
Correction 26 — Spring Cloud Config
@SpringBootApplication @EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) { SpringApplication.run(ConfigServerApplication.class, args); }
}
// bootstrap.yml client:
// spring.cloud.config.uri: http://localhost:8888
// spring.cloud.config.fail-fast: true
Analyse : Configuration centralisee. Les clients chargent la config au demarrage depuis le serveur.
Correction 27 — Service Discovery : Eureka
@SpringBootApplication @EnableEurekaServer
public class ServiceRegistry {
public static void main(String[] args) { SpringApplication.run(ServiceRegistry.class, args); }
}
// Client
@SpringBootApplication @EnableDiscoveryClient
public class OrderService {
public static void main(String[] args) { SpringApplication.run(OrderService.class, args); }
}
Analyse : Eureka enregistre et decouvre les services. Load balancing cote client.
Correction 28 — Spring Cloud Gateway
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); }
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("products", r -> r.path("/api/products/**")
.filters(f -> f.circuitBreaker(c -> c.setName("productCB").setFallbackUri("forward:/fallback")))
.uri("lb://product-service"))
.route("orders", r -> r.path("/api/orders/**")
.uri("lb://order-service"))
.build();
}
}
Analyse : Gateway comme point d'entree. Routage, circuit breaker, load balancing.
Correction 29 — JFR
# Demarrer avec JFR
java -XX:StartFlightRecording=name=rec,filename=recording.jfr,dumponexit=true -jar app.jar
# Commandes jcmd
jcmd <pid> JFR.start name=profile
jcmd <pid> JFR.dump name=profile filename=profile.jfr
jcmd <pid> JFR.stop name=profile
# Analyse avec JDK Mission Control
# jmc recording.jfr
Analyse : JFR enregistre les evenements JVM (GC, CPU, IO, threads) sans impact significatif.
Correction 30 — GC Tuning
# G1GC
java -XX:+UseG1GC -XX:G1HeapRegionSize=4m -XX:MaxGCPauseMillis=50 -Xms2g -Xmx4g -jar app.jar
# ZGC (tres faible latence)
java -XX:+UseZGC -XX:ConcGCThreads=4 -XX:SoftMaxHeapSize=4g -Xms2g -Xmx4g -jar app.jar
Analyse : G1GC pour pauses < 100ms. ZGC pour pauses < 10ms et grands heaps.
Correction 31 — Profiling : Async Profiler
# CPU profiling
./profiler.sh -e cpu -d 60 -f cpu-profile.html <pid>
# Allocation profiling
./profiler.sh -e alloc -d 60 -f alloc-profile.html <pid>
# Wall clock profiling
./profiler.sh -e wall -d 60 -f wall-profile.html <pid>
Analyse : Async Profiler utilise perf_events pour echantillonnage CPU sans safepoint bias.
Correction 32 — Memory Analysis
# Heap dump
jmap -dump:live,format=b,file=heap.hprof <pid>
# ou
jcmd <pid> GC.heap_dump heap.hprof
# Analyse avec Eclipse MAT
# ./mat heap.hprof
# - Leak Suspects Report
# - Dominator Tree (objets retenant le plus de memoire)
# - GC Roots (chemins de retention)
Analyse : Dominator Tree identifie les objets retenant le plus de memoire. GC Roots montrent les chemins de retention.
Correction 33 — JVM Tuning
java -Xms2g -Xmx4g \
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m \
-Xss512k \
-XX:+UseG1GC \
-XX:+PrintGCDetails -XX:+PrintGCDateStamps \
-Xloggc:gc-%t.log -XX:+UseGCLogFileRotation \
-XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10m \
-Dcom.sun.management.jmxremote.port=9010 \
-Dcom.sun.management.jmxremote.authenticate=false \
-jar application.jar
Analyse : Configuration JVM complete avec GC logging, JMX, tailles de heap optimisees.
Correction 34 — Spring Security basique
import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration @EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.formLogin(f -> f.defaultSuccessUrl("/dashboard"))
.oauth2ResourceServer(o -> o.jwt());
return http.build();
}
}
Analyse : Configuration basique Spring Security. Protection des endpoints par role.
Correction 35 — JWT
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Date;
public class JwtService {
private final SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private final long expiration = 3600000;
public String generateToken(String username) {
return Jwts.builder().setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(key).compact();
}
public Claims validateToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token).getBody();
}
public boolean isExpired(String token) {
return validateToken(token).getExpiration().before(new Date());
}
}
Analyse : Service JWT complet. Generation, validation, expiration. Clef securisee HS256.
Correction 36 — OAuth2 Keycloak
// application.yml
// spring.security.oauth2.resourceserver.jwt.issuer-uri:
// http://localhost:8080/realms/myrealm
@Configuration @EnableWebSecurity
public class OAuth2Config {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.oauth2ResourceServer(o -> o.jwt())
.authorizeHttpRequests(a -> a
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated());
return http.build();
}
}
Analyse : Integration Keycloak. Validation JWT via issuer-uri. Resource server securise.
Correction 37 — Method Security
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class DocumentService {
@PreAuthorize("#doc.owner == authentication.name || hasRole('ADMIN')")
public Document updateDocument(Document doc) { return save(doc); }
@PostAuthorize("returnObject.owner == authentication.name")
public Document getDocument(Long id) { return repo.findById(id).orElseThrow(); }
@PreAuthorize("hasRole('ADMIN')")
public void deleteAll() { repo.deleteAll(); }
}
Analyse : @PreAuthorize avant execution, @PostAuthorize apres. Expressions SpEL.
Correction 38 — CSRF et CORS
import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.*;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors(c -> c.configurationSource(corsConfig()))
.csrf(c -> c.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.authorizeHttpRequests(a -> a.anyRequest().authenticated());
return http.build();
}
@Bean
public CorsConfigurationSource corsConfig() {
CorsConfiguration c = new CorsConfiguration();
c.setAllowedOrigins(List.of("http://localhost:4200"));
c.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
c.setAllowedHeaders(List.of("*"));
c.setAllowCredentials(true);
UrlBasedCorsConfigurationSource s = new UrlBasedCorsConfigurationSource();
s.registerCorsConfiguration("/**", c);
return s;
}
}
Analyse : CSRF via Cookie. CORS autorise le frontend Angular.
Correction 39 — Mini-projet : API reservation
@Entity
public class Reservation {
@Id @GeneratedValue private Long id;
private String userId;
private LocalDateTime startTime;
private LocalDateTime endTime;
@Enumerated(EnumType.STRING) private ReservationStatus status;
private LocalDateTime createdAt = LocalDateTime.now();
public enum ReservationStatus { CONFIRMED, CANCELLED, COMPLETED }
}
@RestController @RequestMapping("/api/reservations")
public class ReservationController {
private final ReservationService service;
@PostMapping
public ResponseEntity<Reservation> create(@Valid @RequestBody ReservationRequest req) {
return ResponseEntity.status(HttpStatus.CREATED).body(service.create(req));
}
@PostMapping("/{id}/cancel")
public ResponseEntity<Void> cancel(@PathVariable Long id) {
service.cancel(id);
return ResponseEntity.ok().build();
}
@GetMapping("/report")
public ResponseEntity<ReservationReport> report(
@RequestParam LocalDate start, @RequestParam LocalDate end) {
return ResponseEntity.ok(service.generateReport(start, end));
}
}
Analyse : API complete avec controller, service, repository, validation, reporting.
Correction 40 — Mini-projet : Pipeline ETL
public class EtlPipeline {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
private final JpaRepository<Order, Long> orderRepo;
private final KafkaTemplate<String, OrderEvent> kafka;
public void process(String filePath) throws IOException {
List<String> lines = Files.readAllLines(Path.of(filePath));
List<Order> valid = lines.parallelStream().skip(1).map(this::parse)
.filter(Objects::nonNull).filter(this::validate).toList();
List<CompletableFuture<Order>> enriched = valid.stream()
.map(o -> CompletableFuture.supplyAsync(() -> enrich(o), executor)).toList();
List<Order> saved = enriched.stream().map(CompletableFuture::join).toList();
orderRepo.saveAll(saved);
saved.forEach(o -> kafka.send("order-events", new OrderEvent(o.getId(), "PROCESSED")));
Files.move(Path.of(filePath), Path.of(filePath + ".processed"));
}
private Order parse(String line) {
try { String[] f = line.split(","); return new Order(f[0], Double.parseDouble(f[1]), f[2]); }
catch (Exception e) { return null; }
}
private boolean validate(Order o) { return o.amount() > 0; }
private Order enrich(Order o) { return o; }
}
Analyse : Pipeline ETL complet avec streams paralleles, CompletableFuture, batch insert, Kafka, archivage.
Resume
| Exercices | Competence | Lignes de code |
|---|---|---|
| 1-5 | Streams & Optional | ~150 |
| 6-10 | CompletableFuture | ~200 |
| 11-14 | JPA/Hibernate | ~250 |
| 15-18 | Spring Boot REST | ~300 |
| 19-23 | Testing | ~350 |
| 24-28 | Microservices | ~250 |
| 29-33 | Performance | ~100 |
| 34-38 | Security | ~200 |
| 39-40 | Projets | ~400 |
Total : environ 2200 lignes de code Java.