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.