Spring Framework
La programmation orientée aspect avec Spring AOP et AspectJ
Intermédiaire10 min de lectureMis à jour
Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise
Logs, mesure du temps, retries, contrôles de sécurité et transactions traversent toutes les couches d'une application. Spring AOP vous permet d'écrire chacun d'eux une seule fois sous forme d'aspect et de l'appliquer de façon déclarative. À l'issue de ce guide, vous saurez écrire des aspects avec @Aspect, cibler précisément des méthodes avec des expressions de pointcut, choisir le bon type d'advice et, surtout, expliquer pourquoi le modèle à base de proxies fait que certains appels ne sont jamais interceptés.
Ce que vous allez apprendre
- Définir aspect, join point, pointcut, advice et weaving, et les relier au modèle de proxies de Spring
- Écrire des advices @Around, @Before, @AfterReturning et @AfterThrowing avec @Aspect
- Composer des expressions de pointcut avec les désignateurs execution, within, @annotation, args et bean
- Expliquer la différence entre proxies dynamiques JDK et proxies CGLIB par sous-classe, et quand chacun est utilisé
- Diagnostiquer un advice qui ne s'exécute pas, sans erreur, à cause de l'auto-invocation, de méthodes final ou d'une instanciation hors de Spring
Certains comportements refusent de rester à un seul endroit. Mesurer la durée d'une méthode, vérifier une permission, relancer un appel après une erreur transitoire, ouvrir une transaction : chacun de ces besoins revient dans des dizaines de classes, à toutes les couches, et copier-coller le même try/finally partout éparpille une décision unique dans tout le code. La programmation orientée aspect (AOP) donne un nom à ce problème — les préoccupations transverses (cross-cutting concerns) — et propose d'écrire le comportement une seule fois, puis de déclarer où il s'applique. @Transactional, @Cacheable, @Async, @PreAuthorize et @Retryable reposent tous sur l'AOP : comprendre le mécanisme, c'est ce qui sépare le fait d'utiliser ces annotations de celui de savoir les déboguer. Ce guide cible Spring Framework 6.x.
Vocabulaire
L'AOP dispose d'un vocabulaire réduit et précis, emprunté à AspectJ, que Spring reprend tel quel.
| Terme | Signification | Dans Spring AOP |
|---|---|---|
| Join point | Un point de l'exécution du programme où l'on pourrait ajouter un comportement | Toujours l'exécution d'une méthode sur un bean Spring |
| Pointcut | Un prédicat qui sélectionne des join points | Une expression AspectJ comme execution(* com.shop.service.*.*(..)) |
| Advice | Le comportement à exécuter sur un join point | Une méthode d'aspect annotée @Before, @Around, etc. |
| Aspect | Un module qui regroupe pointcuts et advices | Un bean annoté @Aspect |
| Cible (target) | L'objet qui reçoit l'advice | Votre bean |
| Proxy | L'objet créé pour appliquer l'advice | Un proxy JDK ou CGLIB qui enveloppe la cible |
| Weaving | Le fait de lier les aspects au code | À l'exécution, en créant des proxies pendant le post-traitement des beans |
C'est la restriction de la première ligne qui compte : Spring AOP ne voit que les exécutions de méthodes sur les beans qu'il a lui-même créés. Les accès aux champs, les constructeurs, les méthodes static et les objets que vous construisez avec new lui sont invisibles.
Votre premier aspect
Activez le mécanisme avec @EnableAspectJAutoProxy sur une classe de configuration, ajoutez spring-aop et aspectjweaver au classpath (ce dernier fournit le parseur de pointcuts, même si aucune compilation AspectJ n'a lieu), puis déclarez un aspect comme un bean ordinaire :
@Aspect
@Component
public class TimingAspect {
private static final Logger log = LoggerFactory.getLogger(TimingAspect.class);
@Around("execution(public * com.example.shop.service..*(..))")
public Object time(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed(); // exécute la vraie méthode
} finally {
long micros = (System.nanoTime() - start) / 1_000;
log.debug("{} took {} µs", pjp.getSignature().toShortString(), micros);
}
}
}
Au démarrage, l'auto-proxy creator inspecte chaque bean ; si un pointcut de l'un des aspects correspond à au moins une de ses méthodes, le bean est remplacé dans le conteneur par un proxy. Tous les beans qui en dépendent reçoivent ce proxy. Quand OrderService.confirm() est appelée sur le proxy, celui-ci exécute la chaîne d'advices correspondante, et proceed() invoque la vraie méthode sur la cible enveloppée.
Les types d'advice
Il existe cinq sortes d'advice ; choisissez la moins puissante qui réponde au besoin.
@Befores'exécute avant la méthode. Il ne peut empêcher l'appel qu'en levant une exception.@AfterReturnings'exécute après un retour normal et peut lire la valeur renvoyée grâce à l'attributreturning.@AfterThrowings'exécute quand la méthode lève une exception et peut la lier avecthrowing. Il n'avale pas l'exception.@Afters'exécute à la sortie, normale ou exceptionnelle, comme unfinally.@Aroundenveloppe l'appel. Il reçoit unProceedingJoinPoint, décide s'il faut appelerproceed()et combien de fois, peut modifier les arguments et la valeur de retour, et peut intercepter les exceptions. C'est le seul advice capable d'implémenter un retry, un cache ou la démarcation d'une transaction.
@Aspect
@Component
public class AuditAspect {
@Before("@annotation(audited) && args(id, ..)") // lie l'annotation et le premier argument
public void logIntent(JoinPoint jp, Audited audited, long id) {
Audit.record(audited.value(), jp.getSignature().getName(), id);
}
@AfterThrowing(pointcut = "within(com.example.shop.payment..*)", throwing = "ex")
public void alertOnPaymentFailure(PaymentException ex) {
Alerts.raise("payment failure", ex);
}
@AfterReturning(pointcut = "execution(* com.example.shop.service.OrderService.place(..))",
returning = "order")
public void indexOrder(Order order) {
SearchIndex.add(order);
}
}
Un advice @Around doit renvoyer le résultat de proceed() (ou un remplacement délibéré) et déclarer throws Throwable. Oublier le return transforme chaque méthode interceptée en méthode qui renvoie null — un bug déroutant quand on le découvre, et facile à commettre.
Les expressions de pointcut
Spring prend en charge un sous-ensemble des désignateurs de pointcut d'AspectJ. Voici ceux dont vous vous servirez :
| Désignateur | Sélectionne | Exemple |
|---|---|---|
execution(…) | Un motif de signature de méthode | execution(public * *..OrderService.*(..)) |
within(…) | Toutes les méthodes des types d'un package ou d'une classe | within(com.example.shop.service..*) |
@within(…) | Les types portant une annotation | @within(org.springframework.stereotype.Service) |
@annotation(…) | Les méthodes portant une annotation | @annotation(com.example.Audited) |
args(…) | Les types des arguments à l'exécution | args(java.io.Serializable, ..) |
bean(…) | Un motif de nom de bean Spring | bean(*Repository) |
this(…) / target(…) | Le type du proxy / de la cible | target(com.example.Refundable) |
Le motif execution se lit modifiers? return-type declaring-type?.method-name(params) throws?. Dans ce motif, * correspond à un seul élément (n'importe quel type de retour, n'importe quel nom de méthode, un seul paramètre), .. dans une liste de paramètres correspond à un nombre quelconque de paramètres, et .. dans un nom de package correspond aux sous-packages.
Les pointcuts réutilisables se déclarent sous forme de méthodes vides annotées @Pointcut et se combinent avec &&, || et ! :
@Aspect
public class SystemArchitecture {
@Pointcut("within(com.example.shop..web..*)")
public void webLayer() {}
@Pointcut("within(com.example.shop..service..*)")
public void serviceLayer() {}
@Pointcut("execution(* com.example.shop..repository.*.*(..))")
public void dataAccessOperation() {}
@Pointcut("serviceLayer() && @annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalService() {}
}
Les autres aspects les référencent par leur nom pleinement qualifié : @Around("com.example.shop.SystemArchitecture.serviceLayer()"). Centraliser les pointcuts dans une seule classe transforme la question « quelles méthodes sont interceptées ? » : au lieu d'un travail d'archéologie, il suffit de lire un fichier.
Préférez les pointcuts fondés sur des annotations (@annotation, @within) pour les comportements qu'un développeur doit activer explicitement, comme l'audit ou les retries ; préférez les pointcuts fondés sur les packages (within, execution) pour les règles d'architecture qui s'appliquent à toute une couche.
Les proxies : comment fonctionne le tour de passe-passe
Spring crée l'un des deux types de proxy suivants.
Un proxy dynamique JDK est une classe générée à l'exécution qui implémente les interfaces de la cible et fait passer chaque appel par un InvocationHandler. On ne peut l'utiliser que s'il existe une interface, et il ne peut être injecté que par type d'interface : un champ de type OrderServiceImpl ne peut pas recevoir un proxy JDK de OrderService, et le conteneur échoue avec une erreur « expected single matching bean » ou BeanNotOfRequiredTypeException.
Un proxy CGLIB est une sous-classe générée de la classe cible, qui redéfinit ses méthodes public et protected. Il ne nécessite aucune interface, peut être injecté par type concret, et c'est le choix par défaut de Spring Boot. Ses contraintes sont celles de l'héritage : la classe et les méthodes interceptées ne doivent pas être final, et le proxy doit pouvoir appeler un constructeur — depuis Spring 4, Objenesis permet de sauter l'appel au constructeur, si bien qu'une classe sans constructeur sans argument fonctionne quand même.
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true) // toujours CGLIB, comme Boot
public class AopConfig { }
Quel que soit son type, le proxy conserve une référence vers la vraie cible et lui délègue l'appel une fois la chaîne d'advices exécutée. Trois conséquences en découlent, et elles expliquent presque toutes les questions du type « l'AOP ne fonctionne pas ».
L'auto-invocation n'est pas interceptée
@Service
public class InvoiceService {
public void closeMonth() {
for (var account : accounts()) {
issue(account); // this.issue() : appel direct, le proxy est contourné
}
}
@Transactional
public void issue(Account account) { … } // PAS transactionnelle quand elle est appelée depuis closeMonth
}
Le this à l'intérieur de la cible désigne l'objet brut, pas le proxy. closeMonth appelle issue sur l'objet brut : l'intercepteur de transaction ne voit donc jamais passer l'appel. La correction qui préserve un design sain consiste à déplacer issue dans un bean séparé. Le contournement consiste à injecter le proxy du bean lui-même (ObjectProvider<InvoiceService> self) et à appeler self.getObject().issue(account) ; c'est légal, mais cela mérite un commentaire, car le prochain lecteur ne s'y attendra pas.
Seuls les beans créés par Spring passent par un proxy
Un objet créé avec new, ou renvoyé par une méthode factory que Spring ne gère pas, n'a pas de proxy et ne reçoit aucun advice. Les beans prototype obtenus auprès du conteneur passent bien par un proxy ; les objets qu'ils créent eux-mêmes, non.
Le type à l'exécution change
service.getClass() renvoie la classe du proxy. instanceof OrderService fonctionne toujours avec les deux types de proxy lorsque OrderService est une interface ; avec CGLIB, instanceof OrderServiceImpl fonctionne aussi, puisque le proxy en est une sous-classe. Utilisez AopUtils.getTargetClass(bean) ou AopProxyUtils.ultimateTargetClass dans les rares morceaux de code de type framework qui ont besoin de la vraie classe.
Ordre d'exécution et aspects multiples
Quand plusieurs aspects correspondent à un même join point, leur ordre n'est pas défini, sauf si vous le précisez avec @Order sur les classes d'aspect (la valeur la plus basse s'exécute en premier, c'est-à-dire à l'extérieur). Les intercepteurs de Spring ont eux aussi Ordered.LOWEST_PRECEDENCE pour valeur par défaut — l'ordre de l'intercepteur de transaction se configure sur @EnableTransactionManagement —, si bien qu'un aspect maison sans @Order n'a pas de position définie par rapport à @Transactional. Si votre aspect de retry doit s'exécuter à l'extérieur de la transaction pour que chaque tentative en obtienne une nouvelle, donnez-lui explicitement une valeur d'ordre plus basse et laissez l'intercepteur de transaction à sa valeur par défaut.
Quand Spring AOP ne suffit pas : le weaving AspectJ
AspectJ complet modifie le bytecode au lieu d'envelopper les objets. Il peut intercepter les constructeurs, les lectures et écritures de champs, les méthodes static et private, ainsi que les classes que Spring n'instancie jamais. Il se décline en deux variantes : le weaving à la compilation, via le compilateur ajc ou un plugin Maven/Gradle, et le weaving au chargement, via un agent Java (-javaagent:aspectjweaver.jar, ou @EnableLoadTimeWeaving de Spring avec un serveur ou un agent d'instrumentation adapté). Spring prend en charge les deux et peut injecter des dépendances dans les aspects tissés par AspectJ, et même dans des objets du domaine (@Configurable). Le prix à payer : un build plus complexe et des stack traces qui affichent des méthodes synthétiques. La plupart des équipes n'en ont jamais besoin ; quand elles pensent en avoir besoin, le vrai problème est généralement l'auto-invocation, et la meilleure correction est un petit refactoring.
Erreurs courantes
Oublier le return dans un advice @Around. Toutes les méthodes interceptées renvoient null. Le compilateur ne peut pas le détecter, puisque l'advice renvoie Object.
Intercepter avec des proxies JDK une classe injectée par son type concret. Le démarrage échoue, car le proxy n'implémente que l'interface. Positionnez proxyTargetClass = true ou injectez par interface.
Un pointcut qui ratisse trop large. execution(* *(..)) intercepte tous les beans, y compris les rouages internes de Spring, et peut créer des proxies de classes qui ne doivent pas en avoir. Ancrez toujours le pointcut à votre package.
Attendre un advice sur une classe ou une méthode final avec CGLIB. La sous-classe ne peut pas la redéfinir ; Spring émet un warning au démarrage et la méthode s'exécute sans advice.
Mettre de la logique métier dans les aspects. Les aspects sont faits pour des préoccupations réellement orthogonales — mesure du temps, audit, transactions. Les règles de validation et les décisions de workflow ont leur place dans le service, où elles restent visibles et testables.
Tester un aspect en testant unitairement la cible. Un simple new OrderService() n'a pas de proxy. Vérifiez les aspects avec un contexte de test Spring qui charge l'aspect et la cible, ou testez directement la méthode d'advice avec un ProceedingJoinPoint mocké.
Bonnes pratiques
- Gardez des aspects petits, sans état et peu nombreux. Deux ou trois aspects bien nommés pour les logs, l'audit et la résilience, c'est un nombre sain ; vingt, c'est un problème de maintenance.
- Déclarez les pointcuts une seule fois dans une classe partagée et référencez-les.
- Préférez les pointcuts pilotés par annotations pour les comportements que l'on active explicitement, afin qu'en lisant une méthode on voie ce qui l'enveloppe.
- Donnez à chaque aspect un
@Orderexplicite dès que plusieurs peuvent s'appliquer à la même méthode. - Standardisez sur les proxies CGLIB (
proxyTargetClass = true) pour que l'injection par type concret comme par interface fonctionne, et évitezfinalsur les beans Spring. - Utilisez les aspects fournis par Spring (
@Transactional,@Cacheable,@Async,@Retryablede Spring Retry) avant d'écrire les vôtres pour le même besoin.
Points clés à retenir
- L'AOP isole les comportements transverses dans des aspects ; Spring l'implémente avec des proxies créés à l'exécution autour des beans, limités à l'exécution de méthodes.
- Les beans
@Aspectcontiennent pointcuts et advices ;@Aroundest l'advice le plus puissant et le seul capable de remplacer ou de répéter l'appel. - Les expressions de pointcut combinent les désignateurs
execution,within,@annotation,argsetbean; centralisez-les dans une seule classe. - Les proxies JDK implémentent des interfaces ; les proxies CGLIB sont des sous-classes de la cible et constituent le choix par défaut de Spring Boot. Aucun des deux ne peut intercepter des méthodes
finalou static. - Les appels qu'un bean fait à ses propres méthodes contournent le proxy et ne reçoivent aucun advice — le bug AOP le plus courant, qui touche aussi bien
@Transactionalque@Cacheable. - Le weaving AspectJ complet lève ces limites au prix d'un build plus complexe ; il est rarement nécessaire.
Sources et lectures complémentaires
Questions fréquentes
- Pourquoi mon aspect n'est-il pas appliqué quand une méthode en appelle une autre de la même classe ?
- Spring AOP fonctionne en enveloppant le bean dans un proxy. Seuls les appels qui passent par le proxy reçoivent l'advice. Quand une méthode appelle this.otherMethod(), l'appel va directement à l'objet cible et contourne le proxy : aucun advice ne s'exécute. Déplacez la méthode concernée dans un autre bean, injectez le proxy du bean lui-même via ObjectProvider, ou passez au weaving AspectJ à la compilation ou au chargement si l'auto-invocation doit vraiment être interceptée.
- Spring AOP peut-il intercepter des méthodes private, static ou final ?
- Non. Un proxy JDK ne peut implémenter que des méthodes d'interface ; un proxy CGLIB redéfinit les méthodes dans une sous-classe générée, ce qui exclut les méthodes et classes final, et les méthodes static ou private ne sont de toute façon pas virtuelles. Le weaving AspectJ complet modifie directement le bytecode et peut toutes les intercepter, au prix d'une configuration du build ou d'un agent.
- Spring utilise-t-il par défaut des proxies JDK ou CGLIB ?
- Dans Spring Framework, @EnableAspectJAutoProxy utilise par défaut un proxy dynamique JDK quand le bean implémente au moins une interface, et CGLIB sinon. Spring Boot inverse ce choix et passe à CGLIB (proxyTargetClass=true) pour que les beans puissent être injectés par leur classe concrète. Dans tous les cas, vous pouvez imposer des proxies par classe avec proxyTargetClass = true.
- Spring AOP, est-ce la même chose qu'AspectJ ?
- Non. Spring AOP reprend les annotations et le langage de pointcut d'AspectJ, mais les implémente avec des proxies créés à l'exécution, limités à l'exécution de méthodes sur des beans Spring. AspectJ est une véritable extension du langage, avec un compilateur et un weaver, capable d'intercepter les accès aux champs, les constructeurs, les méthodes static et des objets que Spring ne voit jamais. Spring AOP couvre l'immense majorité des besoins en entreprise ; ne recourez à AspectJ que lorsque le modèle à base de proxies est réellement insuffisant.