Aller au contenu

Exemples d'implémentation Java

Cette section présente deux projets complets et fonctionnels en Java 21 avec Spring Boot 3. Le premier illustre une API REST CRUD classique avec JPA et validation. Le second montre un microservice evenementiel base sur Apache Kafka, pattern courant dans les architectures distribuées modernes.


Projet 1 — API REST CRUD avec Spring Boot 3

Structure du projet

src/main/java/com/example/
├── DemoApplication.java
├── item/
│   ├── Item.java              # Entite JPA
│   ├── ItemRepository.java    # Repository Spring Data
│   ├── ItemService.java       # Logique metier
│   ├── ItemController.java    # Controleur REST
│   ├── ItemDto.java           # Record DTO
│   └── ItemNotFoundException.java
└── config/
    └── GlobalExceptionHandler.java

Entité JPA

package com.example.item;

import jakarta.persistence.*;
import jakarta.validation.constraints.*;

import java.time.Instant;

// Entite JPA mappee sur la table "items"
@Entity
@Table(name = "items")
public class Item {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "Le nom ne peut pas etre vide")
    @Size(max = 200, message = "Le nom ne peut pas depasser 200 caracteres")
    @Column(nullable = false, length = 200)
    private String nom;

    @Size(max = 1000)
    @Column(length = 1000)
    private String description;

    @NotNull
    @DecimalMin(value = "0.0", inclusive = false, message = "Le prix doit etre positif")
    @Column(nullable = false)
    private Double prix;

    @Column(nullable = false, updatable = false)
    private Instant creeLe = Instant.now();

    // Constructeur vide requis par JPA
    protected Item() {}

    public Item(String nom, String description, Double prix) {
        this.nom = nom;
        this.description = description;
        this.prix = prix;
    }

    // Getters et setters
    public Long getId() { return id; }
    public String getNom() { return nom; }
    public void setNom(String nom) { this.nom = nom; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    public Double getPrix() { return prix; }
    public void setPrix(Double prix) { this.prix = prix; }
    public Instant getCreeLe() { return creeLe; }
}

DTO avec record Java

package com.example.item;

import jakarta.validation.constraints.*;

// Record Java 16+ : immuable, equals/hashCode/toString generes
public record ItemDto(
    Long id,

    @NotBlank(message = "Le nom est obligatoire")
    @Size(max = 200)
    String nom,

    @Size(max = 1000)
    String description,

    @NotNull
    @DecimalMin(value = "0.0", inclusive = false)
    Double prix
) {}

Repository

package com.example.item;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

// Spring Data JPA genere l'implementation automatiquement
public interface ItemRepository extends JpaRepository<Item, Long> {

    // Requete derivee du nom de methode
    List<Item> findByNomContainingIgnoreCase(String terme);

    // Requete JPQL pour les items sous un certain prix
    @Query("SELECT i FROM Item i WHERE i.prix <= :prixMax ORDER BY i.prix ASC")
    List<Item> findByPrixMaximum(Double prixMax);
}

Service

package com.example.item;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional(readOnly = true)
public class ItemService {

    private final ItemRepository repository;

    public ItemService(ItemRepository repository) {
        this.repository = repository;
    }

    public List<Item> listerTous() {
        return repository.findAll();
    }

    public Item trouverParId(Long id) {
        return repository.findById(id)
            .orElseThrow(() -> new ItemNotFoundException(id));
    }

    @Transactional
    public Item creer(ItemDto dto) {
        Item item = new Item(dto.nom(), dto.description(), dto.prix());
        return repository.save(item);
    }

    @Transactional
    public Item modifier(Long id, ItemDto dto) {
        Item item = trouverParId(id);
        item.setNom(dto.nom());
        item.setDescription(dto.description());
        item.setPrix(dto.prix());
        return repository.save(item);
    }

    @Transactional
    public void supprimer(Long id) {
        Item item = trouverParId(id);
        repository.delete(item);
    }
}

Contrôleur REST

package com.example.item;

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/items")
public class ItemController {

    private final ItemService service;

    public ItemController(ItemService service) {
        this.service = service;
    }

    // GET /api/items
    @GetMapping
    public List<Item> listerTous() {
        return service.listerTous();
    }

    // GET /api/items/{id}
    @GetMapping("/{id}")
    public Item trouverParId(@PathVariable Long id) {
        return service.trouverParId(id);
    }

    // POST /api/items — 201 Created
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Item creer(@Valid @RequestBody ItemDto dto) {
        return service.creer(dto);
    }

    // PUT /api/items/{id}
    @PutMapping("/{id}")
    public Item modifier(@PathVariable Long id, @Valid @RequestBody ItemDto dto) {
        return service.modifier(id, dto);
    }

    // DELETE /api/items/{id} — 204 No Content
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void supprimer(@PathVariable Long id) {
        service.supprimer(id);
    }
}

Gestion des erreurs globale

package com.example.config;

import com.example.item.ItemNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.stream.Collectors;

// Intercepte les exceptions de tous les controleurs
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 404 — Item introuvable
    @ExceptionHandler(ItemNotFoundException.class)
    public ProblemDetail handleNotFound(ItemNotFoundException ex) {
        ProblemDetail detail = ProblemDetail.forStatusAndDetail(
            HttpStatus.NOT_FOUND, ex.getMessage()
        );
        detail.setTitle("Item non trouve");
        return detail;
    }

    // 400 — Erreurs de validation Bean Validation
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
        Map<String, String> erreurs = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .collect(Collectors.toMap(
                e -> e.getField(),
                e -> e.getDefaultMessage()
            ));

        ProblemDetail detail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
        detail.setTitle("Erreur de validation");
        detail.setProperty("erreurs", erreurs);
        return detail;
    }
}
package com.example.item;

// Exception metier non verifiee
public class ItemNotFoundException extends RuntimeException {
    public ItemNotFoundException(Long id) {
        super("Item introuvable avec l'identifiant : " + id);
    }
}

Projet 2 — Microservice evenementiel Spring Boot + Kafka

Ce projet illustre la communication asynchrone entre services via Apache Kafka. Un producteur publie des événements de commande, un consommateur les traite de façon indépendante.

Dépendances (pom.xml)

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

Configuration application.yml

spring:
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
    consumer:
      group-id: commande-service
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        spring.json.trusted.packages: "com.example.evenements"

Événement de commande

package com.example.evenements;

import java.time.Instant;

// Record utilise comme message Kafka — serialise en JSON
public record CommandeCreeeEvent(
    String commandeId,
    String clientId,
    Double montantTotal,
    Instant creeLe
) {
    // Constructeur compact de validation
    public CommandeCreeeEvent {
        if (montantTotal <= 0) {
            throw new IllegalArgumentException("Le montant doit etre positif");
        }
    }
}

Producteur Kafka

package com.example.kafka;

import com.example.evenements.CommandeCreeeEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;

@Service
public class CommandeProducer {

    private static final Logger log = LoggerFactory.getLogger(CommandeProducer.class);
    private static final String TOPIC = "commandes.creees";

    private final KafkaTemplate<String, CommandeCreeeEvent> kafkaTemplate;

    public CommandeProducer(KafkaTemplate<String, CommandeCreeeEvent> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void publierCommandeCreee(CommandeCreeeEvent evenement) {
        // La cle Kafka determine la partition (meme cle = meme partition = ordre garanti)
        CompletableFuture<SendResult<String, CommandeCreeeEvent>> futur =
            kafkaTemplate.send(TOPIC, evenement.commandeId(), evenement);

        futur.whenComplete((result, ex) -> {
            if (ex != null) {
                log.error("Echec envoi evenement commandeId={}", evenement.commandeId(), ex);
            } else {
                log.info("Evenement publie commandeId={} partition={} offset={}",
                    evenement.commandeId(),
                    result.getRecordMetadata().partition(),
                    result.getRecordMetadata().offset()
                );
            }
        });
    }
}

Consommateur Kafka

package com.example.kafka;

import com.example.evenements.CommandeCreeeEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.KafkaHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;

@Component
public class CommandeConsumer {

    private static final Logger log = LoggerFactory.getLogger(CommandeConsumer.class);

    // @KafkaListener ecoute le topic et desactive le thread bloquant Kafka
    @KafkaListener(
        topics = "commandes.creees",
        groupId = "commande-service",
        containerFactory = "kafkaListenerContainerFactory"
    )
    public void traiterCommande(
        @Payload CommandeCreeeEvent evenement,
        @Header(KafkaHeaders.RECEIVED_PARTITION) int partition,
        @Header(KafkaHeaders.OFFSET) long offset
    ) {
        log.info("Commande recue : id={} client={} montant={} partition={} offset={}",
            evenement.commandeId(),
            evenement.clientId(),
            evenement.montantTotal(),
            partition,
            offset
        );

        // Logique metier : traitement de la commande
        traiterLogique(evenement);
    }

    private void traiterLogique(CommandeCreeeEvent evenement) {
        // Exemple : envoi email, mise a jour stock, facturation...
        log.info("Traitement commande {} pour client {}",
            evenement.commandeId(), evenement.clientId());
    }
}

Configuration Kafka

package com.example.config;

import com.example.evenements.CommandeCreeeEvent;
import org.apache.kafka.clients.admin.NewTopic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.TopicBuilder;

@Configuration
public class KafkaConfig {

    // Creation automatique du topic au demarrage
    @Bean
    public NewTopic topicCommandesCreees() {
        return TopicBuilder.name("commandes.creees")
            .partitions(3)        // 3 partitions pour le parallelisme
            .replicas(1)          // 1 replica (a augmenter en production)
            .build();
    }
}

Dead Letter Topic

En production, configurez un Dead Letter Topic (DLT) pour capturer les messages qui échouent après N tentatives. Spring Kafka fournit DefaultErrorHandler avec DeadLetterPublishingRecoverer pour automatiser ce mécanisme sans perte de messages.

Idempotence du consommateur

Kafka garantit la livraison "at least once". Votre consommateur doit être idempotent : traiter deux fois le même message ne doit pas produire d'effets de bord. Utilisez un identifiant unique (ici commandeId) pour détecter les doublons via une base de données ou un cache Redis.