MFormations
Modern Java Engineering

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

ExercicesCompetenceLignes de code
1-5Streams & Optional~150
6-10CompletableFuture~200
11-14JPA/Hibernate~250
15-18Spring Boot REST~300
19-23Testing~350
24-28Microservices~250
29-33Performance~100
34-38Security~200
39-40Projets~400

Total : environ 2200 lignes de code Java.