La programmation orientée aspect est une technique de factorisation du code.
Elle permet d'adresser des problèmes transverses a toute l'application comme la sécurité, le logging ou la gestion des transactions.
Ces apects sont des morceaux de logique (concerns) qui sont souvent orthogonaux à l'éxecution de l'application.
On crée la branche aop à partir de la branche master.
$ git checkout master Switched to branch 'master' $ git checkout -b aop Switched to a new branch 'aop'
L'exemple canonique de l'AOP est le logging.
Admettons que nous souhaitions avoir un log aux entrées et sorties des méthodes dans tous nos services.
public void doSomething() {
log.trace("Entering in doSomething()");
...
log.trace("Leaving doSomething()");
}
Imaginons que nous ayons des dizaines de services et donc des centaines de méthodes : )
Il n'y a pas de mécanisme en java afin de faire cela simplement.
L'AOP prend donc le problème à l'envers : on isole le logging dans un aspect et on l'applique partout.
public void aFaireAutourDeLa(Method method) { log.trace("Entering in " + method.getSignature()); method.call(); log.trace("Leaving " + method.getSignature()); }
la programmation orientée aspect permet de faire cela.
Avec spring, nous allons utiliser aspectj, qui est la librairie de référence pour l'AOP en java.
L'action d'injecter des aspects est appelée weaving
En java, il existe 3 façons de le faire :
Nous allons le faire au runtime, cela offre moins de possibilités mais c'est plus simple à mettre en place.
La détection d'aspects est activée avec la configuration spring suivante (spring-context.xml).
<beans ... xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="... http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd" > <!-- Support d'aspectj --> <aop:aspectj-autoproxy/> ... <beans>
Il faut aussi ajouter la librairie spring-aop
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>4.1.0.RELEASE</version> <scope>compile</scope> </dependency>
Et aspectj
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.12</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.6.12</version> <scope>compile</scope> </dependency>
Et cglib (librairie de manipulation de byte code)
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2.2</version> <scope>compile</scope> </dependency>
Nous créons le package fr.todooz.aop et on l'ajoute au component scan
<context:component-scan base-package="fr.todooz.aop"/>
Voici un aspect qui affiche le temps d'exécution de chaque méthode.
@Aspect @Component public class TraceAspect { @Around("within(fr.todooz..*)") public Object trace(ProceedingJoinPoint pjp) throws Throwable { long t1 = System.currentTimeMillis(); Object value = pjp.proceed(); long t2 = System.currentTimeMillis(); System.out.println("Executed " + pjp.getSignature() + " in " + (t2 - t1) + "ms"); return value; } }
L'aspect est un @Component
spring.
@Aspect
indique que le composant est un aspect.
@Around
indique que notre aspect doit s'exécuter autour des méthodes des classes de fr.todooz.
ProceedingJoinPoint
est une classe qui décrit l'appel de méthode en cours.
Avec cet aspect en place, on obtient le log suivant lors de l'appel de http://localhost:8080/index.
Executed TagCloud fr.todooz.service.TagCloudService.buildTagCloud() in 136ms Executed List fr.todooz.service.TaskService.findAll() in 38ms
Assez efficace pour quelques lignes de codes.
Pour rendre possible l'AOP au runtime, spring utilise une technique simple : il intercale un proxy entre l'appelant et la cible.
Au runtime, il n'est donc possible de faire de l'AOP qui si il y a un appel de méthode.
En conséquence, seule les méthodes publiques peuvent être concernées, ce qui est généralement suffisant.
Pour plus de possibilités, il faut se tourner vers le weaving au moment de la compilation.
Pour faire un proxy java, il existe 2 méthodes :
Une simple annotation avec spring et une méthode devient transactionnelle.
La technique utilisée est la même : spring ajoute un proxy qui va suivre la transaction.
La prise en compte de @Transactional
ne nécessite ni aspectj ni cglib.
Spring utilise la mécanique standard java qui permet de faire un proxy à partir d'une interface.
Les termes liés à l'AOP sont complexes mais respectés par spring.
En pratique, la connaissance des termes n'est pas un pré requis à faire de l'AOP.
Les aspects n'ont pas de rapport direct avec les annotations.
Cependant, il est possible d'utiliser les annotations comme prédicats (pointcut).
Il suffit alors d'annoter un élément afin d'injecter un aspect (à la façon de @Transational).
La problématique de cache est un élément d'une application facile à impléménter via AOP.
Nous allons ajouter les mécaniques suivantes :
TaskService
.Nous allons utiliser ehcache comme librairie de cache.
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>4.1.0.RELEASE</version> <scope>compile</scope> </dependency> <dependency> <groupId>net.sf.ehcache.internal</groupId> <artifactId>ehcache-core</artifactId> <version>2.6.9</version> <scope>compile</scope> </dependency>
Son utilisation est simple. Elle est même capable de devenir distribuée au travers de terracotta
La configuration spring
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="/WEB-INF/ehcache.xml" /> </bean> <bean id="tasksCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean"> <property name="cacheManager" ref="cacheManager" /> <property name="cacheName" value="tasksCache" /> </bean>
Et le fichier WEB-INF/ehcache.xml associé
<ehcache> <defaultCache maxElementsInMemory="500" eternal="true" overflowToDisk="false" memoryStoreEvictionPolicy="LFU"/> <cache name="tasksCache" maxElementsInMemory="500" eternal="true" overflowToDisk="false" memoryStoreEvictionPolicy="LRU"/> </ehcache>
Nous avons donc maintenant un cache tasksCache disponible dans notre contexte spring.
Nous définissons 2 annotations dans le package fr.todooz.annotation
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface UseCache { }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface FlushCache { }
Elles nous servent a annoter les méthodes du TaskServiceImpl
@FlushCache public void save(Task task) { ... } @UseCache public List<Task> findAll() { ... } ...
Il nous faut alors définir notre Aspect.
@Aspect @Component public class CacheAspect { @Inject private Ehcache postsCache; @Around("@annotation(useCache)") public Object cache(ProceedingJoinPoint pjp, UseCache useCache) throws Throwable { // ? return pjp.proceed(); } @Around("@annotation(flushCache)") public Object flush(ProceedingJoinPoint pjp, FlushCache flushCache) throws Throwable { // ? return pjp.proceed(); } private class CacheKey { private String signature; private Object[] args; public CacheKey(ProceedingJoinPoint pjp) { signature = pjp.getSignature().toString(); args = pjp.getArgs(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CacheKey cacheKey = (CacheKey) o; // Probably incorrect - comparing Object[] arrays with Arrays.equals if (!Arrays.equals(args, cacheKey.args)) return false; if (signature != null ? !signature.equals(cacheKey.signature) : cacheKey.signature != null) return false; return true; } @Override public int hashCode() { int result = signature != null ? signature.hashCode() : 0; result = 31 * result + (args != null ? Arrays.hashCode(args) : 0); return result; } } }
Proposer une implémentation pour ces 2 méthodes.
La javadoc est disponible sur ehcache.org
En ajoutant quelques traces, on peut voir le cache en action.
# première requête sur l'index Executed TagCloud fr.todooz.service.TagCloudService.buildTagCloud() in 142ms Executed List fr.todooz.service.TaskService.findAll() in 38ms # deuxième requête sur l'index Executed TagCloud fr.todooz.service.TagCloudService.buildTagCloud() in 1ms found in cache # delete flush cache Executed void fr.todooz.service.TaskService.delete(Long) in 46ms Executed TagCloud fr.todooz.service.TagCloudService.buildTagCloud() in 1ms Executed List fr.todooz.service.TaskService.findAll() in 1ms # refresh de l'index Executed TagCloud fr.todooz.service.TagCloudService.buildTagCloud() in 1ms found in cache
Ce que nous avons mis en place est simple et peut évoluer facilement.
Mais spring déjà propose une abstraction pour le cache
Elle propose ce que nous venons d'implémenter en beaucoup plus souple.