Implémenter un mécanisme d’AOP, sans framework !

0

L’Aspect Oriented Programming est un paradigme de programmation qui permet de traiter les préoccupations transverses (ou cross cutting concerns en anglais) telles que le logging, le cache ou les transactions séparément du code métier.

L’AOP repose la plupart du temps sur l’utilisation de librairies ou de frameworks qui rendent ce type de programmation assez obscur et parfois difficile à comprendre. L’objectif de cet article est d’expliquer comment cela fonctionne, et d’implémenter un mécanisme d’AOP sans framework afin de le démystifier.

L’ensemble du code montré dans cet article est accessible sur ce repo GitHub.

Pourquoi l’AOP ?

Pour cet article, nous allons partir d’une classe qui implémente la suite de Fibonacci et dont on veut calculer le temps d’exécution.
De manière naïve, il serait possible de calculer et logger ce temps d’exécution directement dans la méthode (voir le commit) :

Ce code pose plusieurs problèmes : le tangling et le scattering.

Code tangling

Le code tangling signifie que le code contient différentes notions qui sont mélangées : la mesure du temps et le métier. On pourrait en imaginer d’autres comme la gestion des transactions, le cache ou le monitoring.

Ceci est problématique car le code métier, qui apporte la vraie valeur, est noyé dans du code de « plomberie » qui peut finir par prendre beaucoup plus de place que le code initial !

Code scattering

Le code scattering signifie que le code non métier sera sans nul doute réécrit dans beaucoup de méthodes. On voit alors émerger un grand nombre de duplications de code, qui le rendent très difficile à maintenir.

Le pattern Proxy

La première amélioration évidente qu’il est possible d’apporter est d’extraire la logique de calcul du temps dans une classe spécifique, qui va implémenter la même interface que FibonacciCalculatorImpl et déléguer l’appel de la méthode calculate à une instance de FibonacciCalculatorImpl. Il s’agit du pattern Proxy (voir le commit).

L’utilisation de cette classe se fait comme suit :

L’utilisation de ce pattern règle le problème du code tangling mais pas du tout celui du code scattering. En effet, cette classe TimedFibonacciCalculator n’est pas vraiment réutilisable car elle est fortement couplée à l’interface FibonacciCalculator.

Externalisation de la logique de chronométrage

Pour résoudre le code scattering, il faut extraire la logique de mesure du temps et lui permettre d’appeler n’importe quelle méthode (voir le commit). On peut, pour cela, définir une interface fonctionnelle générique qui contient une méthode invoke.

Cette interface pourra alors être injectée dans une classe réutilisable, permettant de mesurer le temps nécessaire à l’exécution de la méthode invoke :

Le proxy ne sert donc plus que de passe-plat pour appeler cette classe (l’interface InvocationPoint peut aisément être implémentée par un lambda):

On remarque ici que le message de log est plus générique : il n’est plus possible de tracer l’index de la suite que l’on veut calculer. En effet, ce log est très générique et peut permettre de mesurer le temps d’exécution de n’importe quelle méthode.

Génération automatique de proxy

Le tangling et le scattering sont éliminés mais il faut tout de même créer une classe de proxy pour chaque méthode que l’on souhaite chronométrer.

Le mécanisme de JDKProxy, inclus directement dans le JDK, permet de créer des proxys dynamiques à partir d’une interface (FibonacciCalculator) et d’une implémentation de InvocationHandler (voir le commit). Celle-ci contient une méthode invoke(Object proxy, Method method, Object[] args): Object qui prend en paramètre le proxy, la méthode qui a été appelée ainsi que ses arguments et doit retourner le résultat de l’appel de la méthode.

Le plus simple des InvocationHandler, qui ne fait rien, peut s’écrire comme ceci :

Une fois le handler implémenté, la création du proxy se fait ainsi :

Dans notre cas, le handler va devoir calculer le temps d’exécution de la méthode. Il doit donc être étoffé pour appeler la classe TimedInvocation :

Activer ou désactiver la mesure du temps

Pour faire comme les pros (sic !), il est possible d’activer ou désactiver la mesure du temps sur une méthode en y ajoutant une annotation @Timed qui servira de marqueur.

Dans le handler, il suffit alors d’utiliser la reflection pour déterminer si la méthode est annotée ou non, et mesure le temps en conséquence :

Pour rappel, une annotation n’est qu’un marqueur. La magie n’est pas dans l’annotation mais dans le code qui l’utilise !

Conclusion

A propos de l’AOP

Cet article a, de manière assez rudimentaire, permis d’implémenter un mécanisme d’AOP sans avoir recours à aucun framework externe. Néanmoins, il permet de comprendre certains aspects et limitations de l’AOP.

En effet, il est très simple de créer des proxys dynamiques avec JDKProxy lorsque la classe cible implémente une interface. Lorsque ce n’est pas le cas, le proxy doit étendre cette classe et surcharger la méthode à proxifier (il est possible d’utiliser une librairie comme CGLib pour faire ça automatiquement).
Par aileurs, l’AOP implémenté grâce à ce mécanisme ne peut pas fonctionner sur des méthodes ou des classes privées ou finales. En effet, la méthode doit être définie dans une interface ou pouvoir être surchargée. De plus, si la classe à proxifier appelle directement une méthode de sa propre classe, notre AOP ne s’applique pas. Ici, la méthode calculate est récursive. Cependant, la mesure du temps ne s’opère que si la méthode est appelée à travers le proxy. Donc, seul l’appel de la méthode par « l’extérieur » est chronométré.

Certains autres mécanismes permettent de s’affranchir de ces contraintes en modifiant directement le code généré. Cette opération, appelée tissage, peut être effectuée à la compilation ou au runtime grâce à des librairies et instrumentations diverses.

AspectJ et Spring AOP

AspectJ est un framework qui permet de faire de l’AOP grâce au tissage. Spring, quant à lui, utilise abondamment l’AOP grâce au mécanisme de proxy décrit dans cet article (avec les annotations @Transactional, @Cacheable, etc.). Il va parcourir l’ensemble des beans du contexte d’application et les remplacer par des proxys dynamiques. Ainsi, les beans que l’application manipule ne sont pas les implémentations brutes, mais des proxys (plus d’information sur les proxys AOP avec Spring ici). Spring permet également de faire de l’AspectJ.

Note finale

L’AOP n’est pas magique, ni très complexe. Cependant, il est très fortement utilisé dans les framework de type Spring. Une bonne compréhension de ses mécanismes et de son implémentation permet de faciliter grandement le dévelopement et le debug des applications d’entreprise.

Partagez cet article.

A propos de l'auteur

Adepte du craftsmanship, j'accorde une très grande importance à la qualité du code et pratique quotidiennement le TDD. Je m'intéresse également à Spring ainsi qu'à des technologies Big Data comme la stack Elastic ou Storm.

Ajouter un commentaire