Accueil Nos publications Blog Spring 3.1 : Utiliser l’abstraction de cache – 2 « le retour »

Spring 3.1 : Utiliser l’abstraction de cache – 2 “le retour”

Dans le précédent billet, nous avons abordé un cas simple d’utilisation de l’abstraction de cache fournie par Spring 3.1. Les deux implémentations disponibles (concurrentMap & ehcache) permettent de réaliser beaucoup de choses, mais sont difficiles à imposer dans un projet où un autre cache est déjà utilisé. L’exemple suivant montrera une implémentation possible pour Coherence, la solution de cache d’Oracle.

Par la suite nous verrons comment implémenter une nouvelle annotation gérant la durée de vie des données, une manière plus “propre” que les SpEl de gérer l’éviction.

Pour utiliser coherence, vous devez d’abord l’installer dans votre repository local. Le jar se trouve ici . Pour l’installation, voici la commande à utiliser :

 mvn install:install-file -DgroupId=tangosol \
-DartifactId=coherence \
-Dversion=3.7.1 \
-Dfile=coherence.jar \
-Dpackaging=jar \
-DgeneratePom=true 

Un CacheManager très simple

Les cas de tests déjà réalisés vont permettre de valider la nouvelle implémentation. Le CacheManager réalisera une simple délégation à l’API de Coherence :


public class CoherenceCacheManager implements CacheManager {

public static ThreadLocal CONFIG = new ThreadLocal();

@Autowired
private ConfigurableBeanFactory beanFactory;

public Cache getCache(String name) {
return new CoherenceCacheWrapper(CacheFactory.getCache(name), beanFactory);
}

public Collection getCacheNames() {
List listCaches = new ArrayList();
Cluster cluster = CacheFactory.getCluster();
@SuppressWarnings("unchecked")
Enumeration serviceNames = cluster.getServiceNames();

while (serviceNames.hasMoreElements()) {
String serviceName = serviceNames.nextElement();
if (cluster.getService(serviceName) instanceof CacheService) {
CacheService serviceCache = (CacheService) cluster.getService(serviceName);
@SuppressWarnings("unchecked")
Enumeration cacheNames = serviceCache.getCacheNames();
while (cacheNames.hasMoreElements()) {
String cacheName = cacheNames.nextElement();
listCaches.add(cacheName);
}
}
}
return listCaches;
}
    }
    

Ce CacheManager retourne des objets de type Cache délégant eux aussi les opérations à l’API Coherence :


    public class CoherenceCacheWrapper implements Cache {

private NamedCache namedCache;

public CoherenceCacheWrapper(NamedCache namedCache, ConfigurableBeanFactory beanFactory) {
this.namedCache = namedCache;
}

public String getName() {
return namedCache.getCacheName();
}
}
public Object getNativeCache() {
return namedCache;
}
}
public ValueWrapper get(Object key) {
Object value = namedCache.get(key);
return (value != null ? new SimpleValueWrapper(value) : null);
}

public void put(Object key, Object value) {
if (value != null) {
namedCache.put(key, value);
}
}
}
public void evict(Object key) {
namedCache.remove(key);
}

public void clear() {
namedCache.clear();
}
    }
    

Il suffit alors de modifier la configuration en remplaçant la déclaration du bean cacheManager par celui créé ci-dessus :


    <bean id="cacheManager" class="fr.soat.spring.cache.coherence.CoherenceCacheManager"/>
    

En relançant le test unitaire du service, vous vous apercevrez qu’il faut démarrer le cluster de cache. Une modification de notre test unitaire s’impose : une instance du cache sera lancée in-memory au démarrage du test et coupée à la fin :


    @BeforeClass
    public static void setUp() {
    CoherenceCacheServerWrapper.start();
    }

    @AfterClass
    public static void tearDown() {
    CoherenceCacheServerWrapper.stop();
    }
    

 



    public class CoherenceCacheServerWrapper {
    private static Thread internalServer = new Thread(new Runnable() {
        public void run() {
        DefaultCacheServer.main(new String[0]);
        }
    });

    private CoherenceCacheServerWrapper() {
    }

    public static void start() {
        internalServer.start();
        while (!CacheFactory.getCluster().isRunning()) {
    // Loop waiting for cluster to start
        }
    }

    public static void stop() {
        internalServer.stop();
    }
    }
    

Un second lancement du test unitaire nous confirme que tout marche correctement excepté la condition d’égalité des objets en cache dans la méthode testGetRealTimeTemperature. En effet, le principe de cohérence est de stocker les objets au format POF, un format utilisant la sérialisation. Les objets récupérés du cache sont donc toujours différents (car dé-sérialisés) même si leur contenu lui est identique.

La solution à ce problème est de surcharger la méthode equals de l’objet Température, ainsi le test passe dans son intégralité.


    public int hashCode() {
    return Objects.hashCode(value, validAt);
    }

    public boolean equals(Object o) {
    return o instanceof Temperature && o.hashCode() == this.hashCode();
    }
    

Se faciliter la vie

Dans beaucoup de cas, la durée de vie ou fraicheur d’une donnée (TTL pour Time To Live) est au cœur des problématiques de cache. Cependant, cette notion étant liée à l’implémentation (cela n’existe pas avec une concurrentMap par exemple), Spring ne fournit pas d’annotation et/ou de manière de le gérer au niveau des interfaces proposées. Néanmoins nous ne sommes pas tout à fait désarmés face à ce genre de problème.

En effet, grâce à l’AOP et à l’usage ThreadLocal, nous allons pouvoir ajouter le comportement voulu. Pour commencer nous allons créer une annotation spécifique à la gestion de la durée de vie :


    @Target({ ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface CacheTTL {
    String value();
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
    }
    

Il nous faut ajouter un ThreadLocal dans le CacheManager afin de stocker les données liées à la durée de vie et de pouvoir les récupérer dans le cacheWrapper :


    public static ThreadLocal CONFIG = new ThreadLocal();
    

Nous allons utiliser ces données dans le cacheWrapper :


    public void put(Object key, Object value) {
    if (value != null) {
        Long ttl = getTTLinMs(CoherenceCacheManager.CONFIG.get());
        CoherenceCacheManager.CONFIG.set(null);

        if (ttl != null) {
    namedCache.put(key, value, ttl);
        } else {
    namedCache.put(key, value);
        }
    }
    }

    // getTTLinMs is ommited, it just compute the right value in miliseconds

    

Il ne nous reste alors qu’à ajouter l’aspect qui permettra à l’appel d’une méthode ” mise en cache ” de stocker les données de durée de vie dans le threadLocal :



    @Component
    @Aspect
    public class CacheTTLDiscoverer {

        @Pointcut(value = "execution(public * *(..))")
        public void anyPublicMethod() {
        }

        @Around("anyPublicMethod() && @annotation(cacheTTL) && @annotation(cachePut)")
        public Object doCacheTTLConfig(final ProceedingJoinPoint pjp, CacheTTL cacheTTL, CachePut cachePut) throws Throwable {
            return doCacheConfig(pjp, cacheTTL, cachePut.value());
        }

        @Around("anyPublicMethod() && @annotation(cacheTTL) && @annotation(cacheable)")
        public Object doCacheTTLConfig(final ProceedingJoinPoint pjp, CacheTTL cacheTTL, Cacheable cacheable) throws Throwable {
            return doCacheConfig(pjp, cacheTTL, cacheable.value());
        }

        private Object doCacheConfig(ProceedingJoinPoint pjp, CacheTTL cacheTTL, String[] cacheNames) throws Throwable {
            System.out.println("Interception : " + cacheTTL + " | " + cacheNames);
            CoherenceCacheManager.CONFIG.set(cacheTTL);
            return pjp.proceed();
        }
    }
    

Il ne reste plus alors qu’à déclarer notre aspect dans la configuration Spring :


<aop:aspectj-autoproxy />
<context:component-scan base-package="fr.soat.spring.cache.coherence" />
    

Nous allons remplacer les SpEl d’éviction par l’utilisation de notre annotation fraichement créée :



    @Cacheable(value = "cache-temperature", key = "#country")
    @CacheTTL("1000")
    public Temperature retrieveCachedTemperature(String country) {
    return retrieveTemperatureInternal(country);
    }

    @CachePut(value = "cache-temperature", key = "#country")
    @CacheTTL("500")
    public Temperature retrieveRealTimeTemperature(String country) {
    return retrieveTemperatureInternal(country);
    }

    

Un dernier lancement du test unitaire prouvera le bon fonctionnement de notre ajout à la boite à outil de Spring 3.1.

le mot de la fin

Si nous avions pu voir lors du précédent article les limitations de l’abstraction de cache fournie par Spring, cette dernière mise en place montre qu’il est néanmoins possible de faire ce que l’on souhaite avec l’usage “extrême” du ThreadLocal.

Gageons que Spring fournira dans ses futures versions une méthode supplémentaire dans son interface org.springframework.cache.Cache : void put(Object key, Object value, long ttl);

Ainsi qu’une variable en plus dans ses annotations de caching.

Vous pouvez trouver les sources à l’adresse suivante : https://github.com/soatexpert/spring31CacheDemoWithCoherence