TP5.2 - Beans et Scopes
Objectifs
À la fin de ce TP, vous serez capable de:
- ✅ Créer des Beans avec @Bean et @Configuration
- ✅ Comprendre la différence entre Singleton et Prototype
- ✅ Utiliser @PostConstruct et @PreDestroy
- ✅ Créer des Beans conditionnels
Durée estimée: 1h30
Prérequis
- ✅ Chapitre 3 du Module 5 complété
- ✅ TP5.1 complété
- ✅ Maven 3.9+ et Java 17+
Préparation: Nouveau Projet
Créer le Projet
Via Spring Initializr (https://start.spring.io/):
- Project: Maven
- Spring Boot: 3.2.0
- Group:
com.example - Artifact:
beans-demo - Dependencies: Spring Web
Structure:
src/main/java/com/example/beans/
├── BeansDemoApplication.java
├── service/
│ ├── CounterService.java
│ ├── ReportGenerator.java
│ └── CacheService.java
├── config/
│ └── AppConfig.java
└── controller/
└── TestController.java
Exercice 1: Singleton vs Prototype
Objectif
Comprendre la différence entre un Bean Singleton (1 instance) et Prototype (plusieurs instances).
Étape 1.1: Créer CounterService (Singleton par défaut)
Fichier: service/CounterService.java
package com.example.beans.service;
import org.springframework.stereotype.Service;
@Service // Singleton par défaut
public class CounterService {
private int counter = 0;
public void increment() {
counter++;
System.out.println("Counter incrementé: " + counter);
}
public int getCounter() {
return counter;
}
}
Points clés:
@Service→ Singleton par défautcounterpartagé entre toutes les utilisations
Étape 1.2: Tester le Singleton
Fichier: controller/TestController.java
package com.example.beans.controller;
import com.example.beans.service.CounterService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/test")
public class TestController {
private final CounterService counterService;
public TestController(CounterService counterService) {
this.counterService = counterService;
}
@GetMapping("/singleton")
public String testSingleton() {
counterService.increment();
return "Counter: " + counterService.getCounter() +
" (Singleton - même instance)";
}
}
Étape 1.3: Tester
Démarrez:
mvn clean install -DskipTests spring-boot:run
Testez plusieurs fois:
http://localhost:8080/api/test/singleton
Résultat:
1ère requête: Counter: 1
2ème requête: Counter: 2
3ème requête: Counter: 3
...
✅ C'est la MÊME instance! Le compteur continue d'augmenter.
Étape 1.4: Créer ReportGenerator (Prototype)
Fichier: service/ReportGenerator.java
package com.example.beans.service;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
@Scope("prototype") // ← Nouvelle instance à chaque fois!
public class ReportGenerator {
private String reportId;
private List<String> data = new ArrayList<>();
private LocalDateTime createdAt;
public ReportGenerator() {
this.reportId = "REPORT-" + System.currentTimeMillis();
this.createdAt = LocalDateTime.now();
System.out.println("Nouvelle instance ReportGenerator créée: " + reportId);
}
public void addData(String item) {
data.add(item);
}
public String getReport() {
return String.format("Report %s créé à %s avec %d items",
reportId, createdAt, data.size());
}
}
Points clés:
@Scope("prototype")→ Nouvelle instance à chaque injection- Chaque instance a son propre
reportIdetdata
Étape 1.5: Tester le Prototype
Ajoutez dans TestController.java:
@RestController
@RequestMapping("/api/test")
public class TestController {
private final CounterService counterService;
private final ApplicationContext context; // ← Pour récupérer des Prototype
public TestController(CounterService counterService,
ApplicationContext context) {
this.counterService = counterService;
this.context = context;
}
// ... méthode singleton ...
@GetMapping("/prototype")
public String testPrototype() {
// Récupérer 2 instances
ReportGenerator report1 = context.getBean(ReportGenerator.class);
ReportGenerator report2 = context.getBean(ReportGenerator.class);
// Modifier report1
report1.addData("Item 1");
report1.addData("Item 2");
// Vérifier les 2
return "Report1: " + report1.getReport() + "<br>" +
"Report2: " + report2.getReport() + "<br>" +
"(Ce sont des instances DIFFÉRENTES)";
}
}
Testez:
http://localhost:8080/api/test/prototype
Résultat:
Report1: Report REPORT-1234567890 créé à 2025-11-28T10:00:00 avec 2 items
Report2: Report REPORT-1234567891 créé à 2025-11-28T10:00:01 avec 0 items
✅ Instances DIFFÉRENTES! Chacune a son propre état.
Exercice 2: Cycle de Vie avec @PostConstruct et @PreDestroy
Objectif
Exécuter du code après l'injection et avant la destruction.
Étape 2.1: Créer CacheService avec Hooks de Cycle de Vie
Fichier: service/CacheService.java
package com.example.beans.service;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class CacheService {
private Map<String, String> cache;
// Constructeur
public CacheService() {
System.out.println("1. Constructeur CacheService appelé");
// ⚠️ Ne pas initialiser ici si ça dépend d'autres beans!
}
// Appelé APRÈS l'injection de toutes les dépendances
@PostConstruct
public void init() {
System.out.println("2. @PostConstruct - Initialisation du cache");
cache = new HashMap<>();
cache.put("key1", "value1");
cache.put("key2", "value2");
System.out.println(" Cache initialisé avec " + cache.size() + " éléments");
}
// Appelé AVANT la destruction du bean
@PreDestroy
public void cleanup() {
System.out.println("3. @PreDestroy - Nettoyage du cache");
if (cache != null) {
System.out.println(" Sauvegarde de " + cache.size() + " éléments");
cache.clear();
}
System.out.println(" Cache nettoyé");
}
public void put(String key, String value) {
cache.put(key, value);
System.out.println("Ajouté au cache: " + key + " = " + value);
}
public String get(String key) {
return cache.get(key);
}
public int size() {
return cache.size();
}
}
Étape 2.2: Tester
Ajoutez dans TestController.java:
private final CacheService cacheService;
@GetMapping("/lifecycle")
public String testLifecycle() {
cacheService.put("test", "valeur");
return "Cache size: " + cacheService.size() + " - Voir console pour le cycle de vie";
}
Démarrez l'application:
Console au démarrage:
1. Constructeur CacheService appelé
2. @PostConstruct - Initialisation du cache
Cache initialisé avec 2 éléments
Testez:
http://localhost:8080/api/test/lifecycle
Console:
Ajouté au cache: test = valeur
Arrêtez l'application (Ctrl+C):
Console à l'arrêt:
3. @PreDestroy - Nettoyage du cache
Sauvegarde de 3 éléments
Cache nettoyé
✅ Cycle de vie complet testé!
Exercice 3: Créer des Beans avec @Bean et @Configuration
Objectif
Créer des Beans pour des classes que vous ne contrôlez pas (bibliothèques externes).
Étape 3.1: Créer la Configuration
Fichier: config/AppConfig.java
package com.example.beans.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.format.DateTimeFormatter;
import java.util.Random;
@Configuration
public class AppConfig {
// Bean 1: DateTimeFormatter (classe Java standard)
@Bean
public DateTimeFormatter dateFormatter() {
System.out.println("Création du Bean dateFormatter");
return DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
}
// Bean 2: Random (classe Java standard)
@Bean
public Random randomGenerator() {
System.out.println("Création du Bean randomGenerator");
return new Random();
}
// Bean 3: Bean personnalisé avec dépendance
@Bean
public ReportFormatter reportFormatter(DateTimeFormatter dateFormatter) {
System.out.println("Création du Bean reportFormatter");
// Spring injecte automatiquement dateFormatter!
return new ReportFormatter(dateFormatter);
}
}
Étape 3.2: Créer ReportFormatter
Fichier: service/ReportFormatter.java
package com.example.beans.service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class ReportFormatter {
private final DateTimeFormatter dateFormatter;
public ReportFormatter(DateTimeFormatter dateFormatter) {
this.dateFormatter = dateFormatter;
}
public String formatReport(String title, String content) {
String timestamp = LocalDateTime.now().format(dateFormatter);
return String.format("=== %s ===\nDate: %s\n%s",
title, timestamp, content);
}
}
Étape 3.3: Tester
Ajoutez dans TestController.java:
private final Random random;
private final ReportFormatter reportFormatter;
@GetMapping("/config-beans")
public String testConfigBeans() {
int randomNumber = random.nextInt(100);
String report = reportFormatter.formatReport(
"Test Report",
"Nombre aléatoire: " + randomNumber
);
return report.replace("\n", "<br>");
}
Testez:
http://localhost:8080/api/test/config-beans
Résultat:
=== Test Report ===
Date: 28/11/2025 10:30:45
Nombre aléatoire: 42
✅ Beans créés via @Configuration fonctionnent!
Exercice 4: Beans Conditionnels (Bonus)
Objectif
Créer des Beans seulement dans certaines conditions.
Étape 4.1: Bean Conditionnel Simple
Ajoutez dans AppConfig.java:
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@Configuration
public class AppConfig {
// ... autres beans ...
// Bean créé seulement si la propriété est définie
@Bean
@ConditionalOnProperty(name = "app.feature.advanced", havingValue = "true")
public String advancedFeature() {
return "Feature Avancée Activée!";
}
}
Étape 4.2: Activer la Feature
Fichier: src/main/resources/application.properties
app.feature.advanced=true
Étape 4.3: Tester
Ajoutez dans TestController.java:
import org.springframework.beans.factory.annotation.Autowired;
@RestController
@RequestMapping("/api/test")
public class TestController {
@Autowired(required = false) // ← Optionnel!
private String advancedFeature;
@GetMapping("/conditional")
public String testConditional() {
if (advancedFeature != null) {
return "✅ " + advancedFeature;
} else {
return "❌ Feature avancée désactivée";
}
}
}
Testez avec app.feature.advanced=true:
✅ Feature Avancée Activée!
Testez avec app.feature.advanced=false:
❌ Feature avancée désactivée
Récapitulatif
Ce que Vous Avez Appris
| Concept | Syntaxe | Usage |
|---|---|---|
| Singleton | @Service (défaut) | Services sans état, 1 instance |
| Prototype | @Scope("prototype") | Objets avec état, N instances |
| @PostConstruct | Méthode annotée | Initialisation après injection |
| @PreDestroy | Méthode annotée | Nettoyage avant destruction |
| @Bean | Dans @Configuration | Classes externes, config complexe |
| Bean conditionnel | @ConditionalOnProperty | Beans selon configuration |
Comparaison Singleton vs Prototype
// Singleton (défaut)
@Service
public class SingletonService {
private int state = 0; // ⚠️ Partagé!
}
// Prototype
@Service
@Scope("prototype")
public class PrototypeService {
private int state = 0; // ✅ Propre à chaque instance
}
Quand Utiliser Quoi?
Singleton ✅ (défaut):
- Services sans état
- Repositories
- Controllers
- 99% des cas!
Prototype ⚠️ (rare):
- Objets avec état mutable
- Objets temporaires
- Rapports, processeurs de fichiers
Auto-Évaluation
- Quelle est la différence entre Singleton et Prototype?
- Quand sont appelés @PostConstruct et @PreDestroy?
- Pourquoi utiliser @Bean au lieu de @Service?
- Comment créer un Bean conditionnel?
Challenge Bonus 🎯
Créez un système de cache avec TTL (Time To Live):
@Service @Scope("singleton")CacheManager- Méthode
put(key, value, ttlSeconds) - Méthode
get(key)qui retourne null si expiré @PostConstructpour initialiser@PreDestroypour sauvegarder le cache
Bonus: Utilisez @Scheduled pour nettoyer automatiquement les entrées expirées.
Prochaine Étape
Passez au TP3 pour apprendre la configuration par profils!
➡️ TP5.3: Configuration par Profils