đź“… 12/06/2025
Lors de mes développements avec Spring Boot, j'ai récemment été confronté à un piège subtil, mais instructif concernant l'interaction entre l'injection de dépendances et la visibilité des méthodes. Cette expérience m'a rappelé l'importance de comprendre les mécanismes internes du framework, particulièrement quand on utilise des annotations transactionnelles.
Dans mon projet, j'avais conçu une architecture basée sur une classe abstraite gérant des opérations transactionnelles. Cette approche me permettait de centraliser la logique commune tout en laissant les implémentations spécifiques aux classes filles.
public abstract class MyAbstractClass {
@Autowired
private MyRepository myRepository;
abstract protected void doSomething(Entity entity);
@Transactional(Transactional.TxType.REQUIRES_NEW)
void doSomethingWithTransaction(long entityId) {
var myEntity = myRepository.findById(entityId).orElseThrow();
doSomething(myEntity);
}
}
L'implémentation concrète était relativement simple :
public class MyImplementation extends MyAbstractClass {
protected void doSomething(Entity entity) {
System.out.println("### my entity = " + entity);
}
}
En tant que développeur soucieux des bonnes pratiques Java, j'ai naturellement voulu appliquer le principe de moindre visibilité. La méthode doSomethingWithTransaction
n'étant appelée que depuis le même package et ne devant pas être accessible aux classes filles, j'ai été tenté de la déclarer en visibilité package-private.
Cette décision, logique d'un point de vue purement orienté objet, s'est révélée problématique dans le contexte de Spring. L'exécution a révélé que myRepository
était null
, causant une NullPointerException
inattendue.
La racine du problème réside dans le fonctionnement des proxies dynamiques de Spring. Lorsqu'une classe contient des annotations comme @Transactional
, Spring génère automatiquement un proxy pour intercepter les appels de méthodes et appliquer les fonctionnalités transactionnelles.
Ce proxy généré ne se trouve pas dans le même package que ma classe originale. Par conséquent, il ne peut pas accéder aux méthodes ayant une visibilité package-private, ce qui empêche le bon fonctionnement de l'injection de dépendances.
Le framework Spring utilise différentes stratégies de création de proxies :
Dans tous les cas, ces proxies nécessitent une visibilité appropriée des méthodes pour fonctionner correctement.
La résolution de ce problème nécessite d'adapter notre approche de la visibilité aux contraintes techniques de Spring. Pour que l'Autowiring fonctionne correctement avec des méthodes annotées, ces dernières doivent avoir au minimum une visibilité protected
.
@Transactional(Transactional.TxType.REQUIRES_NEW)
protected void doSomethingWithTransaction(long entityId) {
var myEntity = myRepository.findById(entityId).orElseThrow();
doSomething(myEntity);
}
Cette modification permet au proxy Spring d'accéder correctement à la méthode et d'injecter les dépendances nécessaires.
Cette expérience illustre parfaitement les compromis parfois frustrants que nous devons accepter lorsque nous travaillons avec des frameworks modernes. Bien que Spring Boot affiche une simplicité séduisante en surface, ses mécanismes internes basés sur la programmation orientée aspect (AOP) et la génération dynamique de proxies nous contraignent parfois à abandonner nos principes de conception les plus rigoureux.
Quelques bonnes pratiques Ă retenir :
Premièrement, les méthodes annotées avec @Transactional
, @Cacheable
, @Async
ou d'autres annotations Spring AOP doivent avoir une visibilité au moins protected
pour garantir le bon fonctionnement des proxies.
Deuxièmement, il est crucial de tester le comportement de nos applications dans des conditions réelles, car certains problèmes ne se manifestent qu'à l'exécution.
Troisièmement, la documentation officielle de Spring Framework fournit des détails précieux sur le fonctionnement des proxies et leurs limitations.