Aller au contenu principal

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​

  1. Les scopes des Beans (Singleton, Prototype, etc.)
  2. Quand utiliser quel scope
  3. Configuration avec @Configuration
  4. Les profils (@Profile) pour dev/test/prod
  5. @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​

ScopeDescriptionInstancesUsage
@Singleton1 instance pour toute l'application1Défaut - Services, Repositories
@PrototypeNouvelle instance à chaque injectionPlusieursObjets avec état
@RequestScope1 instance par requête HTTP1/requêteObjets liés à une requête
@SessionScope1 instance par session utilisateur1/sessionPanier, préférences utilisateur
@ApplicationScope1 instance pour l'application web1Similaire à 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​

ScopeInstancesDurée de VieUsage Principal
Singleton1 pour l'appDémarrage → ArrêtServices, Repos (défaut)
Prototype1 par demandeCréation → GCObjets avec état
Request1 par requête HTTPRequête → RéponseLogs, contexte requête
Session1 par session userSession début → finPanier, 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​

  1. Quelle est la différence entre Singleton et Prototype?
  2. Quand utiliser RequestScope?
  3. Comment activer un profil Spring?
  4. Quelle est la différence entre application.properties et application-dev.properties?
  5. À 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!


➡️ Commencer TP1: Injection Avancée

⬅️ Chapitre précédent: Beans

⬅️ Retour au Module 5