Chapitre 4: Scopes et Configuration
Introduction
Vous savez créer des Beans, mais savez-vous combien d'instances Spring crée? Une seule pour toute l'application? Une nouvelle à chaque fois?
Ce chapitre vous apprend à contrôler le cycle de vie des Beans avec les Scopes.
Ce que vous allez apprendre
- Les scopes des Beans (Singleton, Prototype, etc.)
- Quand utiliser quel scope
- Configuration avec @Configuration
- Les profils (@Profile) pour dev/test/prod
- @ComponentScan
Durée: 45 minutes
Les Scopes des Beans
Qu'est-ce qu'un Scope?
Scope = Durée de vie et nombre d'instances d'un Bean
Question: Quand Spring crée UserService, combien d'instances crée-t-il?
- Une seule pour toute l'application? (Singleton)
- Une nouvelle à chaque injection? (Prototype)
- Une par requête HTTP? (Request)
- Une par session utilisateur? (Session)
Le scope détermine la réponse!
Les 5 Scopes Disponibles
| Scope | Description | Instances | Usage |
|---|---|---|---|
@Singleton | 1 instance pour toute l'application | 1 | Défaut - Services, Repositories |
@Prototype | Nouvelle instance à chaque injection | Plusieurs | Objets avec état |
@RequestScope | 1 instance par requête HTTP | 1/requête | Objets liés à une requête |
@SessionScope | 1 instance par session utilisateur | 1/session | Panier, préférences utilisateur |
@ApplicationScope | 1 instance pour l'application web | 1 | Similaire à Singleton pour web |
1. Scope Singleton (Défaut)
Principe
Spring crée 1 SEULE instance du Bean
Tous les composants partagent la MÊME instance
Code
@Service // Singleton par défaut
public class UserService {
private int counter = 0;
public void incrementCounter() {
counter++;
}
public int getCounter() {
return counter;
}
}
Test
@RestController
public class TestController {
private final UserService userService;
public TestController(UserService userService) {
this.userService = userService;
}
@GetMapping("/test")
public String test() {
userService.incrementCounter();
return "Counter: " + userService.getCounter();
}
}
Résultat:
Requête 1: Counter: 1
Requête 2: Counter: 2
Requête 3: Counter: 3
...
Pourquoi? C'est la MÊME instance pour toutes les requêtes!
Visualisation
Tous partagent la même instance!
Quand Utiliser Singleton?
✅ Services sans état (stateless)
@Service
public class EmailService {
public void send(String to, String message) {
// Pas de variables d'instance modifiables
// Juste des méthodes qui travaillent avec les paramètres
}
}
✅ Repositories
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Pas d'état
}
✅ Controllers
@RestController
public class UserController {
// Pas d'état (les dépendances sont final)
}
Règle: Si le Bean n'a pas d'état mutable, utilisez Singleton (défaut)!
2. Scope Prototype
Principe
Spring crée une NOUVELLE instance à chaque fois qu'on demande le Bean
Chaque composant a sa PROPRE instance
Code
import org.springframework.context.annotation.Scope;
@Service
@Scope("prototype") // ← Nouvelle instance à chaque fois
public class ShoppingCart {
private List<Product> items = new ArrayList<>();
public void addProduct(Product product) {
items.add(product);
}
public List<Product> getItems() {
return items;
}
}
Test
@RestController
public class CartController {
private final ApplicationContext context;
public CartController(ApplicationContext context) {
this.context = context;
}
@GetMapping("/test-prototype")
public String testPrototype() {
// Récupérer 2 instances
ShoppingCart cart1 = context.getBean(ShoppingCart.class);
ShoppingCart cart2 = context.getBean(ShoppingCart.class);
// Modifier cart1
cart1.addProduct(new Product("Laptop"));
// Vérifier cart2
return "Cart1: " + cart1.getItems().size() +
", Cart2: " + cart2.getItems().size();
}
}
Résultat:
Cart1: 1, Cart2: 0
Pourquoi? Ce sont des instances DIFFÉRENTES!
Visualisation
Chaque composant a sa propre instance!
Quand Utiliser Prototype?
✅ Objets avec état
@Scope("prototype")
public class ReportGenerator {
private String title;
private List<Data> data;
// État spécifique à chaque rapport
}
✅ Objets temporaires
@Scope("prototype")
public class FileProcessor {
private File currentFile;
// Traitement d'un fichier spécifique
}
⚠️ Attention: Prototype avec Singleton = problème!
Problème: Prototype dans Singleton
@Service // Singleton
public class OrderService {
private final ShoppingCart cart; // Prototype
// ❌ PROBLÈME!
public OrderService(ShoppingCart cart) {
this.cart = cart;
// cart est injecté UNE SEULE FOIS (au démarrage)
// Même si ShoppingCart est Prototype!
}
}
Résultat: Le Prototype se comporte comme un Singleton!
Solution: Utiliser ObjectProvider ou ApplicationContext.getBean() pour récupérer une nouvelle instance à chaque fois.
3. Scope Request (Web)
Principe
1 nouvelle instance PER requête HTTP
L'instance existe pendant toute la requête
Détruite après la réponse
Code
import org.springframework.web.context.annotation.RequestScope;
@Component
@RequestScope // ← Nouvelle instance par requête
public class RequestLogger {
private String requestId;
private LocalDateTime startTime;
@PostConstruct
public void init() {
this.requestId = UUID.randomUUID().toString();
this.startTime = LocalDateTime.now();
System.out.println("Requête " + requestId + " démarrée");
}
@PreDestroy
public void cleanup() {
Duration duration = Duration.between(startTime, LocalDateTime.now());
System.out.println("Requête " + requestId + " terminée en " +
duration.toMillis() + "ms");
}
}
Utilisation
@RestController
public class UserController {
private final RequestLogger logger;
public UserController(RequestLogger logger) {
this.logger = logger;
}
@GetMapping("/users")
public List<User> getUsers() {
logger.log("Récupération des utilisateurs");
// ... logique
}
}
Résultat:
Requête 1:
Requête abc-123 démarrée
Requête abc-123 terminée en 45ms
Requête 2:
Requête def-456 démarrée
Requête def-456 terminée en 32ms
Chaque requête a sa propre instance!
4. Scope Session (Web)
Principe
1 instance par session utilisateur
Partagée entre toutes les requêtes d'un même utilisateur
Détruite quand la session expire
Code
import org.springframework.web.context.annotation.SessionScope;
@Component
@SessionScope // ← Instance par session utilisateur
public class UserPreferences {
private String theme = "light";
private String language = "fr";
public void setTheme(String theme) {
this.theme = theme;
}
public String getTheme() {
return theme;
}
}
Utilisation: Panier d'achat, préférences, authentification
Comparaison des Scopes
Tableau Récapitulatif
| Scope | Instances | Durée de Vie | Usage Principal |
|---|---|---|---|
| Singleton | 1 pour l'app | Démarrage → Arrêt | Services, Repos (défaut) |
| Prototype | 1 par demande | Création → GC | Objets avec état |
| Request | 1 par requête HTTP | Requête → Réponse | Logs, contexte requête |
| Session | 1 par session user | Session début → fin | Panier, préférences |
Visualisation Temporelle
Singleton: [════════════════════════════════] (toute l'app)
Prototype: [===] [===] [===] [===] [===] (à la demande)
Request: [req1] [req2] [req3] [req4] (par requête)
Session: [═══ Session User1 ═══]
[═══════ Session User2 ═══════]
Configuration avec @Configuration
Classe de Configuration Complète
@Configuration
public class AppConfig {
// Bean Singleton (défaut)
@Bean
public EmailService emailService() {
EmailService service = new EmailService();
service.setHost("smtp.gmail.com");
return service;
}
// Bean Prototype
@Bean
@Scope("prototype")
public ReportGenerator reportGenerator() {
return new ReportGenerator();
}
// Bean conditionnel (expliqué plus bas)
@Bean
@Profile("dev")
public DataSource devDataSource() {
return new H2DataSource();
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
return new PostgreSQLDataSource();
}
}
Les Profils (@Profile)
Problème
Vous avez différentes configurations selon l'environnement:
Développement:
- Base H2 en mémoire
- Logs détaillés
- Email en mode fake
Production:
- Base PostgreSQL
- Logs minimaux
- Email réel
Comment gérer ça proprement?
Solution: Profils Spring
Créer des Beans selon le profil actif:
@Configuration
public class DatabaseConfig {
@Bean
@Profile("dev") // ← Actif seulement en dev
public DataSource devDataSource() {
System.out.println("Utilisation de H2 (dev)");
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
@Bean
@Profile("prod") // ← Actif seulement en prod
public DataSource prodDataSource() {
System.out.println("Utilisation de PostgreSQL (prod)");
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("admin");
config.setPassword("secret");
return new HikariDataSource(config);
}
@Bean
@Profile("test") // ← Actif seulement en test
public DataSource testDataSource() {
System.out.println("Utilisation de H2 (test)");
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("test-schema.sql")
.build();
}
}
Activer un Profil
Méthode 1: application.properties
spring.profiles.active=dev
Méthode 2: Variable d'environnement
export SPRING_PROFILES_ACTIVE=prod
Méthode 3: Argument JVM
java -jar app.jar --spring.profiles.active=prod
Méthode 4: IDE (VS Code, IntelliJ)
"spring.profiles.active": "dev"
Profils Multiples
Activer plusieurs profils:
spring.profiles.active=dev,debug,mock-email
Bean actif pour plusieurs profils:
@Bean
@Profile({"dev", "test"}) // ← Actif en dev OU test
public DataSource devOrTestDataSource() {
return new H2DataSource();
}
Exemple Complet: Configuration par Environnement
// Interface commune
public interface EmailService {
void send(String to, String message);
}
// Implémentation DEV (fake)
@Service
@Profile("dev")
public class FakeEmailService implements EmailService {
@Override
public void send(String to, String message) {
System.out.println("FAKE EMAIL to " + to + ": " + message);
}
}
// Implémentation PROD (réelle)
@Service
@Profile("prod")
public class RealEmailService implements EmailService {
@Override
public void send(String to, String message) {
// Vraie logique d'envoi via SMTP
smtpClient.send(to, message);
}
}
En dev: Pas de vrais emails envoyés
En prod: Vrais emails envoyés
Fichiers de Configuration par Profil
Structure
src/main/resources/
├── application.properties (commun)
├── application-dev.properties (dev seulement)
├── application-test.properties (test seulement)
└── application-prod.properties (prod seulement)
Exemple
application.properties (commun):
app.name=My Application
server.port=8080
application-dev.properties:
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.show-sql=true
logging.level.root=DEBUG
application-prod.properties:
spring.datasource.url=jdbc:postgresql://prod-server:5432/mydb
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
spring.jpa.show-sql=false
logging.level.root=WARN
Activation:
spring.profiles.active=dev
Spring charge automatiquement application-dev.properties!
@ComponentScan
Problème
Par défaut, Spring scanne seulement le package de la classe principale et ses sous-packages.
com.example.demo
├── DemoApplication.java ← Classe principale
├── service/
│ └── UserService.java ✅ Scanné
└── controller/
└── UserController.java ✅ Scanné
com.external.library
└── ExternalService.java ❌ PAS scanné
Solution: @ComponentScan
@SpringBootApplication
@ComponentScan(basePackages = {
"com.example.demo", // Package principal
"com.external.library" // Package externe
})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Maintenant ExternalService sera scanné!
Filtrer le Scan
Exclure certaines classes:
@ComponentScan(
basePackages = "com.example.demo",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {OldService.class, DeprecatedController.class}
)
)
Inclure seulement certaines annotations:
@ComponentScan(
basePackages = "com.example.demo",
includeFilters = @ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = Service.class // Seulement @Service
),
useDefaultFilters = false
)
Exemple Complet: Application Multi-Environnements
// Classe principale
@SpringBootApplication
public class ECommerceApplication {
public static void main(String[] args) {
SpringApplication.run(ECommerceApplication.class, args);
}
}
// Configuration
@Configuration
public class AppConfig {
// Email selon environnement
@Bean
@Profile("dev")
public EmailService devEmailService() {
return new FakeEmailService();
}
@Bean
@Profile("prod")
public EmailService prodEmailService() {
return new SmtpEmailService();
}
// Cache selon environnement
@Bean
@Profile("prod")
@Scope("singleton")
public CacheManager redisCacheManager() {
return new RedisCacheManager();
}
@Bean
@Profile("dev")
@Scope("singleton")
public CacheManager simpleCacheManager() {
return new SimpleCacheManager();
}
}
// Service avec scope approprié
@Service
@Scope("singleton") // Une instance pour l'app
public class OrderService {
private final EmailService emailService;
private final CacheManager cacheManager;
public OrderService(EmailService emailService,
CacheManager cacheManager) {
this.emailService = emailService;
this.cacheManager = cacheManager;
}
}
// Bean avec état (Prototype)
@Component
@Scope("prototype") // Nouvelle instance à chaque fois
public class Invoice {
private Order order;
private BigDecimal total;
public void generate(Order order) {
this.order = order;
this.total = calculateTotal(order);
}
}
Résumé du Chapitre
Scopes
@Scope("singleton") // 1 instance (défaut)
@Scope("prototype") // N instances
@RequestScope // 1 par requête HTTP
@SessionScope // 1 par session user
Profils
@Profile("dev") // Actif en dev
@Profile("prod") // Actif en prod
@Profile({"dev","test"}) // Actif en dev OU test
Activation:
spring.profiles.active=dev
Configuration
@Configuration // Classe de config
@Bean // Créer un Bean
@ComponentScan // Scanner des packages
Points Clés
✅ Singleton par défaut (services sans état)
✅ Prototype pour objets avec état
✅ Request/Session pour web
✅ @Profile pour gérer dev/test/prod
✅ application-{profile}.properties pour config
✅ @ComponentScan pour packages externes
Auto-Évaluation
- Quelle est la différence entre Singleton et Prototype?
- Quand utiliser RequestScope?
- Comment activer un profil Spring?
- Quelle est la différence entre application.properties et application-dev.properties?
- À quoi sert @ComponentScan?
Félicitations! 🎉
Vous avez terminé toute la théorie du Module 5!
Vous comprenez maintenant:
- ✅ L'Inversion de Contrôle (IoC)
- ✅ L'Injection de Dépendances (DI)
- ✅ Les Beans Spring
- ✅ Les Scopes et la Configuration
Prochaine étape: Mettre en pratique avec les TPs!