🎯 OBJECTIF
Comprendre comment :
this.method()) court-circuite toute l'infrastructure Spring@Transactional est ignoré sans erreur🧠 MODÈLE MENTAL
Spring ne modifie pas ton code Java. Il crée une enveloppe (proxy) autour de tes beans qui intercepte les appels entrants et ajoute son comportement (ouverture d'une transaction, vérification de sécurité, mise en cache...). Ce proxy est l'objet que le reste de l'application connaît et utilise.
Le problème : quand une méthode appelle une autre méthode du même objet avec this.method(), elle court-circuite cette enveloppe. Elle appelle directement le bean réel — Spring n'est plus dans la boucle. Conséquence : @Transactional, @Cacheable, @Async... toutes les annotations sont ignorées en silence.
Équation : appel externe → proxy → interception Spring active. Appel interne → bean réel → Spring hors jeu.
Au démarrage, Spring inspecte chaque bean singleton via un BeanPostProcessor. Si une annotation AOP est détectée (@Transactional, @Cacheable...), il substitue le bean par un proxy. Pour chaque appel entrant, le proxy agit en intercepteur :
sequenceDiagram
participant A as Bean A (appelant)
participant P as Proxy
participant B as Bean B (réel)
A->>P: orderService.createOrder()
P->>P: begin transaction
P->>B: createOrder() [délégué]
B-->>P: return
P->>P: commit / rollback
P-->>A: returnmermaid🔑 Conclusion clé
Le proxy est transparent pour l'appelant — il ne sait pas qu'il parle à un proxy. C'est pour ça que l'infrastructure Spring peut s'activer sans modifier le code métier.
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
notifyWarehouse(order); // ⚠️ appel interne via this
}
@Transactional(propagation = REQUIRES_NEW)
public void notifyWarehouse(Order order) {
// On croit ouvrir une 2e transaction indépendante
// En réalité : même transaction que createOrder
}
}javasequenceDiagram
participant C as Controller
participant P as Proxy
participant S as OrderService (réel)
C->>P: createOrder()
P->>P: begin T1
P->>S: createOrder() [délégué]
S->>S: this.notifyWarehouse()
Note over S: Pas de proxy traversé
Note over S: REQUIRES_NEW ignoré silencieusement
Note over S: notifyWarehouse() s'exécute dans T1
S-->>P: return
P->>P: commit T1
P-->>C: returnmermaid🚨 Le bug silencieux
Aucune erreur n'est levée. notifyWarehouse() s'exécute correctement — mais dans la mauvaise transaction. Si createOrder() rollback, notifyWarehouse() rollback aussi. REQUIRES_NEW est ignoré sans prévenir.
La solution : extraire dans un bean séparé injecté.
@Service
@RequiredArgsConstructor
public class OrderService {
private final WarehouseNotifier warehouseNotifier; // bean séparé ✓
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
warehouseNotifier.notify(order); // ✓ appel externe → proxy traversé
}
}
@Service
public class WarehouseNotifier {
@Transactional(propagation = REQUIRES_NEW) // ✓ appliqué correctement
public void notify(Order order) { ... }
}javaflowchart TD
Question{"Le bean implémente\nune interface ?"}
JDK["JDK Proxy\njava.lang.reflect.Proxy"]
CGLIB["CGLIB\nSous-classe dynamique"]
Question -- Oui --> JDK
Question -- Non --> CGLIBmermaidpublic interface PaymentService { void pay(Order o); }
@Service
public class StripePaymentService implements PaymentService { ... }
// Spring injecte un proxy de type PaymentServicejava| Propriété | Valeur |
|---|---|
instanceof StripePaymentService |
false |
getClass().getName() |
$Proxy42 |
| Méthodes proxifiables | Seulement celles de l'interface |
@Service
public class ReportService {
@Transactional
public void generate() { ... }
}
// Spring injecte ReportService$$EnhancerBySpringCGLIB$$abc123java| Propriété | Valeur |
|---|---|
instanceof ReportService |
true (sous-classe) |
getClass().getName() |
ReportService$$EnhancerBySpringCGLIB$$... |
Classe final |
❌ Impossible à proxifier |
Méthode final |
❌ Non interceptable |
Diagnostiquer le type de proxy
System.out.println(service.getClass().getName());
// "$Proxy42" → JDK Proxy
// "OrderService$$CGLIB..." → CGLIB
// "OrderService" → aucun proxyjava@Transactional, @Cacheable n'ont d'effet que sur les méthodes appelées depuis un autre bean.final sur les classes et méthodes Spring si elles nécessitent du proxying CGLIB.⚡ TL;DR — chaque concept en une ligne
Proxy
✓ Objet intermédiaire placé par Spring à la place du bean, intercepte les appels pour activer l'AOP (@Transactional, @Cacheable, etc.).
⚠ Sans proxy, aucune infrastructure Spring ne peut s'activer — ni transactions, ni cache, ni async.
Appel interne (this.method())
✓ S'exécute dans la transaction existante si elle est ouverte.
⚠ Court-circuite complètement le proxy — @Transactional de la méthode appelée est ignoré sans erreur.
JDK Proxy ✓ Proxy natif Java, utilisé quand le bean implémente une interface. ⚠ Ne proxifie que les méthodes déclarées dans l'interface.
CGLIB
✓ Proxy par sous-classe, utilisé quand le bean n'implémente pas d'interface.
⚠ Impossible sur des classes ou méthodes final.
🎓 À retenir
@Transactional ignoré ne lève aucune exception ; seul un test d'intégration avec vrai contexte Spring le révèle.ApplicationContext.getBean() sur this est un anti-pattern — workaround connu pour forcer le passage par le proxy, mais signe d'une mauvaise décomposition ; extraire dans un bean séparé est la seule solution propre.final ou une méthode final rend Spring silencieux — pas d'erreur au démarrage, mais l'annotation AOP ne s'applique simplement pas.proxyTargetClass, Objenesis, limites final@Transactional exploite le mécanisme proxy