🎯 OBJECTIF
Comprendre comment :
🧠 MODÈLE MENTAL
Spring démarre comme un chef de cuisine qui prépare tout avant d'ouvrir le restaurant. Tous les ingrédients (beans) sont sortis, vérifiés et mis en place avant que le premier client arrive. Si un ingrédient est manquant ou périmé, la cuisine ne s'ouvre pas — c'est le principe fail-fast.
Le cycle de vie d'un bean est une ligne de montage : instanciation → injection des dépendances → BeanPostProcessors → @PostConstruct → bean opérationnel → @PreDestroy à l'arrêt. L'ordre n'est pas anodin : les BeanPostProcessors passent avant @PostConstruct précisément pour que le proxy (AOP, @Transactional, @Async…) soit déjà en place quand le bean exécute son code d'initialisation. Si @PostConstruct appelle une méthode @Transactional du même bean, Spring intercepte l'appel via le proxy — tout fonctionne. Mais si c'est le constructeur qui appelle cette méthode, le proxy n'existe pas encore : l'appel passe directement sur l'instance brute, sans transaction, sans interception.
flowchart LR
A["Démarrage ApplicationContext"] --> B["Scan des @Component / @Bean"]
B --> C["Résolution des dépendances"]
C --> D{"Dépendance manquante ?"}
D -- Oui --> E["NoSuchBeanDefinitionException au démarrage"]
D -- Non --> F["Instanciation des singletons"]
F --> G["Injection des dépendances"]
G --> H["BeanPostProcessors — création des proxies AOP"]
H --> I["@PostConstruct"]
I --> J["Contexte prêt — Application opérationnelle"]
J --> K["Arrêt / shutdown"]
K --> L["@PreDestroy"]mermaid🔑 Conclusion clé
Le contexte Spring est tout ou rien : soit tous les singletons sont opérationnels, soit l'application ne démarre pas. C'est intentionnel — mieux vaut échouer au démarrage que découvrir une mauvaise configuration en production sous charge.
sequenceDiagram
participant SC as Spring Container
participant BP as BeanPostProcessors
participant B as Bean (instance brute)
participant P as Proxy AOP
SC->>B: new ReportService(...)
SC->>B: injection des @Autowired (champs, setters)
SC->>BP: postProcessBeforeInitialization()
BP->>P: crée le proxy (wrapping AOP)
SC->>P: appel @PostConstruct via le proxy
P->>B: méthode init() — proxy actif
SC-->>SC: bean enregistré comme prêt
Note over SC,P: ...application en cours d'utilisation...
SC->>P: appel @PreDestroy
P->>B: méthode destroy()mermaid@Service
public class ReportService {
@Autowired
private ReportRepository reportRepository;
// ⚠️ DANGER : appel depuis le constructeur — proxy AOP pas encore créé
public ReportService() {
// Ne jamais faire ça :
// this.generateDailyReport(); // ← @Transactional ignorée, pas de transaction !
}
@PostConstruct
public void init() {
// ✓ Ici le proxy est en place : @Transactional fonctionne
generateDailyReport(); // ← appel intercepté par le proxy, transaction ouverte
}
@Transactional // ✓ interceptée via proxy AOP en @PostConstruct, ignorée depuis le constructeur
public void generateDailyReport() {
reportRepository.deleteOlderThan(LocalDate.now().minusDays(30));
reportRepository.save(new DailyReport(LocalDate.now()));
}
@PreDestroy
public void cleanup() {
reportRepository.flush(); // ✓ libération des ressources à l'arrêt
}
}java⚠️ Piège critique — @Transactional dans le constructeur
Si generateDailyReport() est appelée depuis le constructeur, Spring n'a pas encore créé le proxy AOP. L'appel passe directement sur l'instance brute : @Transactional est silencieusement ignorée, aucune transaction n'est ouverte. Aucune exception n'est levée — le bug est invisible jusqu'en production.
@Service
@Lazy // ← bean instancié seulement à la première utilisation
public class HeavyAnalyticsService {
@PostConstruct
public void init() {
loadMLModel(); // opération coûteuse — s'exécute au 1er getBean(), pas au démarrage
}
}java| ✅ Avantages | ❌ Inconvénients |
|---|---|
| Démarrage plus rapide | Erreurs de configuration découvertes tard |
| Ressources chargées à la demande | Latence sur la première requête |
| Utile pour les services rarement utilisés | Perd le bénéfice fail-fast |
@Component
@Scope("prototype")
public class CsvImportJob {
private List<String> errors = new ArrayList<>(); // état local, safe en prototype
}
@Service
public class ImportService {
@Autowired
private ApplicationContext context;
public void runImport(MultipartFile file) {
// ✓ nouvelle instance à chaque appel
CsvImportJob job = context.getBean(CsvImportJob.class);
job.process(file);
}
}java⚠️ Piège — prototype injecté dans un singleton
Un singleton garde sa dépendance prototype indéfiniment. Injecter @Autowired CsvImportJob job dans un singleton signifie que tous les appels partagent la même instance (et le même état mutable). Utiliser ApplicationContext.getBean() ou ObjectProvider<CsvImportJob>.
@Service
public class AlertService {
// ✓ Pattern Optional<T> — injection safe si le bean est absent
@Autowired
private Optional<NotificationService> notificationService;
public void sendAlert(String message) {
notificationService.ifPresentOrElse(
svc -> svc.notify(message),
() -> log.warn("Aucun service de notification actif")
);
}
}java@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String correlationId = UUID.randomUUID().toString();
}java⚠️ proxyMode obligatoire pour les scopes web
Sans proxyMode = ScopedProxyMode.TARGET_CLASS, un bean request ou session injecté dans un singleton sera le même objet pour toutes les requêtes. Le proxy force Spring à déléguer chaque appel vers l'instance correcte pour la requête courante.
⚡ TL;DR — chaque concept en une ligne
Fail-fast au démarrage ✓ Spring vérifie et instancie tous les beans singleton au démarrage — toute erreur de configuration explose immédiatement. ⚠ Les beans @Lazy, prototype et conditionnels échappent à cette règle et peuvent exploser à la première utilisation en production.
@PostConstruct ✓ S'exécute après injection des dépendances ET après la création du proxy AOP — c'est le bon endroit pour l'initialisation métier. ⚠ Ne pas confondre avec le constructeur : au moment du constructeur, ni les dépendances injectées via @Autowired sur champ, ni les proxies AOP ne sont disponibles.
@PreDestroy ✓ Appelé à l'arrêt du contexte Spring — idéal pour libérer des ressources (connexions, threads, fichiers temporaires). ⚠ N'est jamais appelé pour les beans prototype : Spring ne gère pas leur destruction, c'est à l'appelant de le faire.
Scope prototype ✓ Crée une nouvelle instance à chaque injection/getBean() — utile pour les objets stateful à courte durée de vie. ⚠ Le bean parent (singleton) qui reçoit un prototype le garde indéfiniment : pour obtenir une nouvelle instance à chaque appel, il faut ApplicationContext.getBean() ou ObjectProvider.
Beans conditionnels ✓ @ConditionalOnProperty, @ConditionalOnBean etc. permettent d'activer un bean uniquement si une condition est vraie. ⚠ Injecter directement un bean conditionnel absent fait exploser le démarrage (NoSuchBeanDefinitionException) — utiliser Optional ou @Autowired(required = false).
🎓 À retenir
getBean() en conserve la référence. La destruction du singleton ne déclenche pas @PreDestroy sur le prototype.