Tutoriel sur Spring Cache

| Zenika | Anis Ben Hamda |

Cet article s'intéresse à la mise en cache avec Spring pour à la fois améliorer les performances d'une application et alléger sa charge de travail.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Tutoriel sur Spring Cache

- Auteurs : Zenika, Anis Ben Hamda

- Parution : 2 février 2015

- Licence : Copyright ® 2015 Zenika. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Utilisation

- III. Génération des clés de cache

- IV. Cache personnalisé

- IV-A. La clé de cache (key)

- IV-B. Condition de la mise en cache

- V. L'annotation @CacheEvict

- VI. L'annotation @CachePut

- VII. Activation de cache

- VIII. Contraintes techniques

- IX. Choix de l'implémentation

- IX-A. Implémentation cache de base ConcurrentHashMap de Java

- IX-B. Implémentation ehcache

- X. Conclusion

- XI. Remerciements

I - Introduction

La mise en cache a toujours été un besoin important pour à la fois améliorer les performances d'une application et alléger sa charge de travail. De plus, son utilité est particulièrement évidente aujourd'hui avec les applications Web qui peuvent être amenées à gérer des milliers de visiteurs concurrents. D'un point de vue architectural, la gestion du cache est orthogonale à la logique métier de l'application et pour cette raison, elle devrait avoir un impact minimal sur le développement de l'application elle-même.

Depuis la version 3.1, Spring fournit une API pour la gestion de cache, semblable à la gestion déclarative des transactions. L'abstraction de la mise en cache permet une utilisation cohérente des différentes solutions de mise en cache avec un impact minimal sur le code.

Le cache Spring est appliqué à des méthodes Java. Au premier appel d'une méthode avec une combinaison de paramètres, Spring stocke sa valeur de retour dans le cache. Ainsi, l'appel suivant se verra directement servir la valeur venant du cache sans avoir besoin d'appeler le traitement derrière qui peut être couteux. Le tout est appliqué de façon transparente sans impacter l'appelant.

Dans cet article nous allons voir deux implémentations différentes du stockage de caches avec Spring :

- implémentation à base des ConcurrentHashMap de Java ;

- implémentation ehcache.

II - Utilisation

La mise en cache d'une méthode avec Spring est une opération simple et transparente, on doit annoter notre méthode via l'annotation @Cacheable.

@Cacheable(value= "dataCache")
public Reponse getDatas(Long param1,String param2){
}

dataCache est le nom du gestionnaire de caches associé.

Au premier appel à la méthode, Spring cache la réponse, identifiée par une clé unique calculée sur la base de hashcode des paramètres d'appels < param1, param2>, et pour le énième appel avec les mêmes paramètres, Spring retourne la réponse déjà cachée sans avoir réexécuté la méthode.

Aussi, il est possible d'associer plus qu'un seul cache à notre méthode

@Cacheable({"dataCache","default"})
public Reponse getDatas(Long param1,String param2){
}

Dans ce cas, chacun des caches sera vérifié avant d'exécuter la méthode, si au moins un cache est trouvé, alors la valeur associée sera retournée.

III - Génération des clés de cache

L'algorithme de base d'une gestion de caches est relativement trivial. Le cache est une zone mémoire dans laquelle on stocke des objets dont chacun est identifié par une clé unique calculée, on utilise en général les Maps pour stocker un cache. L'algorithme de récupération d'un objet est donc le suivant :

- on calcule sa clé (généralement hashCode ou combinaison de hashCode) ;

- on recherche l'objet dans le cache (recherche par clé) ;

- si l'objet est trouvé

- on le renvoie ;

- sinon

- on recalcule l'objet réel,

- on met l'objet dans le cache associé à sa clé,

- on renvoie l'objet.

De même Spring utilise un KeyGenerator basé sur le hachage simple, qui calcule la clé sur la base des tables de hachage des objets passés en paramètre de la méthode.

IV - Cache personnalisé

IV-A - La clé de cache (key)

Il est tout à fait probable que les méthodes cibles ont différentes signatures qui ne peuvent pas être simplement mappées, cela tend à devenir évident lorsque la méthode cible a plusieurs arguments dont seulement certains sont adaptés pour la mise en cache (alors que le reste n'est utilisé que par la logique de la méthode)

@Cacheable(value= "dataCache")
public Reponse getDatas(Long param1,String param2,boolean param3){
}

Pour de tels cas, l'annotation @Cacheable permet au développeur de spécifier la manière dont la clé de cache est générée. Le développeur peut utiliser SpEL pour choisir les arguments d'intérêt (ou leurs propriétés imbriquées).

@Cacheable(value= "dataCache",key="#param2")
public Reponse getDatas(Long param1,String param2,boolean param3){
}

Dans ce cas, la clé de cache sera calculée seulement avec le deuxième paramètre <param2>.

Spring permet aussi de spécifier des propriétés imbriquées :

@Cacheable(value= "dataCache",key="#param2.name")
public Reponse getDatas(Long param1,Data param2,boolean param3){
}

Dans ce cas, la clé de cache sera calculée avec l'attribut name du paramètre <param2>.

Les exemples ci-dessus montrent comment il est simple de sélectionner certains arguments ou une de ses propriétés.

IV-B - Condition de la mise en cache

Parfois, une méthode pourrait ne pas être appropriée pour mettre en cache tout le temps, mais sous certaines conditions. Les annotations de cache supportent cette fonctionnalité. Le paramètre condition prend une expression SpEL qui est évaluée à true ou false. Donc si la condition est vraie, la méthode sera mise en cache.

@Cacheable(value= "dataCache",key="#param2",condition="#param2.length<64")
public Reponse getDatas(Long param1,String param2,boolean param3){
}

Dans ce cas, la méthode est mise en cache si et seulement si la taille du deuxième paramètre est inférieure à 64.

V - L'annotation @CacheEvict

Le cache Spring permet non seulement la population d'un magasin de caches, mais aussi son expulsion. Ce processus est utile pour supprimer les données obsolètes ou inutilisées de la mémoire cache. Opposée à @Cacheable, l'annotation @CacheEvict délimite des méthodes qui effectuent l'expulsion de cache, ce sont des méthodes qui agissent comme des déclencheurs de suppression des données à partir du cache. @CacheEvict nécessite un ou plusieurs caches qui sont touchés par l'action.

@CacheEvict(value= "dataCache")
public void reloadData(){
}

Cette option est très pratique lorsqu'une région de caches entière doit être vidée. Il est important de noter que les méthodes void peuvent être utilisées avec @CacheEvict, ces méthodes agissent comme des déclencheurs de suppression de cache, les valeurs de retour sont ignorées (comme elles n'interagissent pas avec le cache). Ce n'est pas le cas avec @Cacheable qui ajoute/met à jour les données dans le cache et nécessite donc un retour.

VI - L'annotation @CachePut

Forcer la mise à jour d'une entrée de cache. Par opposition à l'annotation @Cacheable, cette annotation provoque toujours l'exécution de la méthode et le stockage de son résultat dans la mémoire cache.

@CachePut(value= "dataCache",key="#param2.name")
public Reponse getDatas(Long param1,Data param2,boolean param3){
}

Donc le @CachePut, est nécessaire pour forcer la création ou la mise à jour d'une entrée dans le cache, sans attendre son expiration.

Le seul cas où la méthode n'est pas exécutée, c'est quand vous fournissez l'option condition de @CachePut, et votre condition est évaluée à false.

VII - Activation de cache

Il est important de noter que les annotations de cache ne déclenchent pas automatiquement leur exécution.

Pour activer le support de cache dans un projet Spring, on commence par activer le traitement des annotations @Cacheable via la balise annotation-driven du namespace cache :

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

<cache:annotation-driven />

Notez qu'il suffit de supprimer cette balise pour désactiver le cache.

On peut aussi activer le support de cache par annotation en ajoutant @enableCaching dans l'une de nos classes de configuration (@Configuration classes) :

@Configuration
@EnableCaching
public class AppConfig {
}

VIII - Contraintes techniques

- Les objets passés en paramètre de la méthode doivent avoir leurs propres comportements haschode() afin que Spring puisse calculer les clés.

- Les objets passés en paramètre et l'objet de retour (ainsi leurs propriétés imbriquées) doivent être serialisables.

IX - Choix de l'implémentation

Spring offre deux implémentations différentes du stockage de caches :

- implémentation à base des ConcurrentHashMap de Java ;

- implémentation ehcache.

Pour les utiliser, il faut simplement déclarer un CacheManager approprié et une entité qui contrôle et gère les caches.

IX-A - Implémentation cache de base ConcurrentHashMap de Java

Il faut déclarer les gestionnaires de caches SimpleCacheManager dans le contexte de l'application.

<bean id="cacheManager"  class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" name="default"/>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" name="dataCache"/>
</set>
</property>
</bean>

Chaque gestionnaire nécessite un nom (name) unique afin de l'identifier par annotation.

On peut déclarer plusieurs caches gérés par un seul manager SimpleCacheManager.

Cette implémentation est basique, elle n'a pas besoin d'une bibliothèque supplémentaire, mais elle n'est pas trop prévue pour des grosses charges qui nécessitent des paramétrages supplémentaires.

IX-B - Implémentation ehcache

Cette implémentation utilise ehcache, elle est beaucoup plus puissante et flexible et elle permet un paramétrage avancé du cache de l'application.

L'implémentation ehcache est localisée sous le package org.springframework.cache.ehcache. Pour l'utiliser, on doit déclarer le CacheManager approprié.

<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="ehcache"/>
</bean>
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml"/>
<property name="shared" value="true"/>
</bean>

Le fichier ehcache.xml est le fichier de paramétrage des caches de l'application :

<ehcache
xsi:noNamespaceSchemaLocation="ehcache.xsd"
updateCheck="true"
monitoring="autodetect"
dynamicConfig="true"
maxBytesLocalHeap="150M">
<diskStore path="java.io.tmpdir"/>
<defaultCache
eternal="false"
maxElementsInMemory="100"
overflowToDisk="false"    />
<cache name="dataCache"
eternal="false"
timeToIdleSeconds="300"
maxBytesLocalHeap="30M"
timeToLiveSeconds="300"
overflowToDisk="true"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>

En utilisant l'ehcache, on peut définir plusieurs caches avec différents paramètres d'une manière très simple :

- name : un identifiant du cache ;

- maxBytesLocalHeap : définit le nombre d'octets que le cache peut utiliser de la VM. Si un CacheManager maxBytesLocalHeap a été défini, la taille déterminée de ce cache sera soustraite du CacheManager. D'autres caches partagent le reste. Les valeurs de cet attribut sont données sous la forme <nombre> k | K | M | M | g |G (k | K pour kilo-octets, m | M pour mégaoctets, ou g | G pour gigaoctets). Par exemple : 30m pour 30 mégaoctets ;

- eternal : définit si les éléments sont éternels. Si c'est le cas, le timeout sera ignoré et l'élément n'est jamais expiré ;

- timeToIdleSeconds : c'est le nombre de secondes que l'élément doit vivre depuis sa dernière utilisation. La valeur par défaut est 0, l'élément reste toujours en repos ;

- timeToLiveSeconds : c'est le nombre de secondes que l'élément doit vivre depuis sa création en cache. La valeur par défaut est 0, l'élément vivra éternellement ;

- memoryStoreEvictionPolicy : politique d'éviction :

- LRU - le moins récemment utilisé,

- LFU - moins fréquemment utilisé,

- FIFO - premier entré, premier sorti, l'élément le plus ancien par date de création ;

- diskExpiryThreadIntervalSeconds : nombre de secondes entre deux exécutions du processus de contrôle d'éviction ;

- diskPersistent : permet la mémorisation des objets sur le disque pour une récupération des objets entre deux démarrages de la VM ;

- overflowToDisk : détermine si les objets peuvent être stockés sur le disque en cas d'atteinte du maximum d'éléments en mémoire.

Pour résumer avec une simple formule mathématique :

expirationTime = Math.min((creationTime + timeToLive),(mostRecentTime + timeToIdle))

X - Conclusion

La gestion de caches est une problématique critique pour les applications Web, ainsi que la définition d'un système de caches applicatif est généralement considérée comme relativement complexe à mettre en œuvre par les développeurs. Pour cela, Spring a proposé un système générique de définition de caches complètement transparent, et simple.

Une démo est disponible sous : https://github.com/Zenika/Blogs/tree/master/20140527demoSpringCache.

XI - Remerciements

Cet article a été publié avec l'aimable autorisation de Zenika, experts en technologies Open Source et méthodes Agiles.

Nous tenons à remercier Claude LELOUP pour sa relecture orthographique attentive de cet article et Régis Pouiller pour la mise au gabarit.