Archive

Posts Tagged ‘cache’

Spring, aop, caches…

1237036892[reprise d’un ancien article : j’ai vu depuis des librairies mieux écrites pour faire le meme genre de choses, mais cet article reste intéressant si vous voulez comprendre des mécanismes des annotations java, et un vrai exemple de programmation orientée aspect (AOP) ]

Introduction

Annotations

Les annotations java5 permettent d’ajouter des méta-données au classes, et déclarations de méthode. Ces informations peuvent être disponible au runtime. On peut savoir programmatiquement si telle ou telle méthode comporte telle ou telle annotation.

C’est une manière syntaxique élégante pour enrichir la définition des classes, et leur ajouter des informations non prévues dans le modèle de programmation propre a java.

AOP

L’AOP (aspect-oriented programming) est un paradigme de programmation permettant l’injection de code supplémentaire (‘advice’) par dessus un programme existant (‘subject’), de manière a implémenter de nouvelles fonctionnalités (‘concern’).

Le principe est de cibler les évenement d’un programme (appel d’une méthode, instanciation d’un objet, etc..) et de permettre l’execution de code arbitraire sur ces évenements. On utilise l’AOP pour ajouter des fonctions transverses a un programme sans modifier tout le code, et en évitant de mélanger des activités différentes dans un même code (typiquement : la sécurité, le logging, le tracage,…)

Spring fournit en interne une gestion de l’AOP, via la librairie AspectJ

Je me propose d’utiliser ces techniques pour implémenter une gestion automatique de cache.

principe de cache

Pour moi cache est un objet permettant de stocker des couples clé / valeur de manière a éviter de réeffectuer un traitement long.

Il s’agit donc de trouver une manière simple pour décrire qu’une méthode quelconque peut utiliser un cache, c’est a dire stocker/retrouver des résultats de manière transparent au lieu d’effectuer un traitement lors de chaque appel.

Interfaces

Je défini donc l’interface d’un cache :

public interface Cache {
      public void put(String key, Object valueToStore);
      public CacheResult get(String key);
}

et

public interface CacheResult {
     public boolean isAvailable();
     public String getKey();
     public Object getData();
}

partant ces ces deux interfaces, j’ai implémenté plusieurs types de cache (je ne copie pas le code ici, mais je peux vous envoyer un zip si vous demandez gentillement…)

  • une implémentation « dummy » qui ne stocke rien, et renvoie tout le temps des CacheResult « pas trouvé »
  • une implémentation « maison », la classe Cache ne fait qu’embarquer une hashmap.
  • une implémentation basée sur memcached, en utilisant la librairie de http://www.whalin.com/memcached/
  • une implémentation basée sur EHCache.

Annotation

Je définit une annotation : celle ci me permettra de « marquer » les méthodes :

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME) //annotation conservée a l'execution
@Target(ElementType.METHOD) // l'annotation s'applique aux méthodes
public @interface UseCache {

String cacheName(); //cacheName est le parametre de l'annotation
}

Vous remarquerez a quel point il est simple de créer ses propres annotations !
La syntaxe « cacheName(); » paraitra un peu bizarre pour aux vieux briscard du java, mais c’est bien la syntaxe qui permet d’ajouter des paramètres aux annotions…

appliquer l’annotation par AOP

Annoter son code c’est bien, faire en sorte que ça fasse des trucs, c’est mieux ! J’utilise aspectJ + Spring pour associer a mon annotation « UseCache » un comportement spécifique au runtime.

Une annotation spécifique permet de déclarer la classe qui va se charger d’éxecuter du code quand on tente d’éxecuter les méthodes annotées « useCache » :

@Aspect
public class CachingInterceptor implements ApplicationContextAware {

private static final Log _logger = LogFactory.getLog(CachingInterceptor.class);

private ApplicationContext applicationContext;

@Around("@annotation(net.jr.caching.annotation.UseCache)")
public Object aroundCachedMethod(ProceedingJoinPoint jp) {
// find the cache, and the called method.
Method invoked = getMethod(jp);
Annotation annotation = invoked.getAnnotation(UseCache.class);
String cacheKey = Integer.toHexString(invoked.hashCode()) + "_"
+ buildCacheKey(jp.getArgs());

_logger.debug("the cache key for this call is "+cacheKey);

Cache cache = (Cache) getApplicationContext().getBean(
((UseCache) annotation).cacheName());

// try to retrieve data from cache
if (cache != null) {
CacheResult result = cache.get(cacheKey);
if (result.isAvailable()) {
_logger.debug("cache hit !");
return result.getData();
}
}

// no data available, so we call the original method.
_logger.debug("no data found in cache for key "+cacheKey+", calling original method code...");
try {
Object result = jp.proceed(jp.getArgs());
if (cache != null) {
_logger.debug("... putting method call result in cache with the key "+cacheKey);
cache.put(cacheKey, result);
}
return result;
} catch (Throwable ite) {
throw new RuntimeException(ite);
}
}

protected String buildCacheKey(Object... objs) {
int hash = 0;
for (Object arg : objs) {
hash ^= arg.hashCode();
}
return Integer.toString(hash);
}

@SuppressWarnings("unchecked")
protected Method getMethod(JoinPoint jp) {
MethodSignature met = (MethodSignature) jp.getSignature();
try {
Method method = jp.getSourceLocation().getWithinType().getMethod(
met.getMethod().getName(),
met.getMethod().getParameterTypes());
method.setAccessible(true);
return method;
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}

protected Cache getCache(String name) {
return (Cache) getApplicationContext().getBean(name);
}

@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}

protected ApplicationContext getApplicationContext() {
return applicationContext;
}

Ce code mérite quelques explications :

Création de l’Advice

l’advice est l’execution de la méthode « aroundCachedMethod »

l’execution consiste

  1. a trouver le cache dans l’applicationContext d’aprés sont nom,
  2. A frabriquer une ’empreinte’ (xor entre les hashcodes) a partir de la combinaison des paramètres d’entrée de la méthode « wrappée »
  3. chercher dans le cache la valeur stockée pour cette empreinte, la renvoyer directement quand elle est trouvée
  4. sinon, éxecuter la méthode originale et stocker la valeur de retour dans le cache.

Définition des pointcuts

les pointcuts ( = expression définissant le type d’évenement a surveiller) est également définit par annotation !

@Around("@annotation(net.jr.caching.annotation.UseCache)")

Veut dire  » A la place de (toute méthode comportant l’annotation ‘UseCache’)  »

Une documentation complète sur la syntaxe des pointcuts est disponible sur le site d’AspectJ

Intégration dans Spring

Dans le fichier de conf Spring, il faut déclarer l’utilisation de l’aop :

<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

    <aop:aspectj-autoproxy proxy-target-class="false" />

....

Puis on déclare notre « Interceptor » :

<bean id="cachingInterceptor" />

Puis on déclare un cache :

<bean id="monCache" />

C’est magique, Il suffit maintenant dans le code d’utiliser l’annotation pour qu’une méthode tire partie de notre cache :

class MyClass
{
@UseCache(cacheName="monCache")
     public String rechercheCompliquee(String criteres) {
          ....
     }
}

CONCLUSION :

cette méthode d’application de vos annotations vous permet d’appliquer des aspects sans avoir a modifier la manière dont le code est compilée. L’intégration avec Spring est également appréciable.

Par contre, faites attentions a l’impact sur les performances de ce type d’aop. Je me pose également des questions dans le cas ou un autre système de modification de bytecode (par exemple hibernate) est dèja en place…

Publicités
Catégories :Uncategorized Étiquettes : , , ,