Améliorez les performances avec Spring-Ehcache: Cache & @Cacheable

Le recours à l'emploi de caches dans nos projets permet d'améliorer significativement les performances.

logo-ehcache.png

Dans ce billet contrairement à celui-ci nous ne codons rien dans la partie métier pour éviter le couplage.

Spring sait rendre les choses simples plus simples et les choses complexes possibles.

Un cache sert à optimiser les accès aux données néanmoins ce n'est la solution miracle à tous les problèmes de performances.

Mais une fois mis en œuvre à bon escient, on se demande comment peut-on s'en passer.

Donc depuis Spring 3.1, implémenter une gestion de cache devient possible sans trop écrire du code.

La démo ci-après, sans aborder les détails d'implémentation de la couche DAO, présente un exemple concret de mise en (EH)cache.

Sachez néanmoins que les illustrations données reposent sur une implémentation basée sur spring-data.

Spring offre une implémentation light de mise en cache qui ne peut satisfaire les besoins de nos projets.

C'est pour cette raison qu'ici nous utilisons Ehcache.

L'annotation @Cacheable de Spring apposée sur les méthodes (finder par exemple) suffit.

L'AOP Spring rend la gestion du cache facile à l'instar des transactions déclaratives.

Ehcache est une solution de cache Open Source.

La solution Ehcache peut être utilisée en JEE comme en JSE.

Hibernate utilise Ehcache.

Passons à la mise en place de notre démo qui est un projet maven simple.

Commençons par créer un projet maven à partir d'une template quickstart.

Ensuite suivre les étapes suivantes.

ETAPE 1. Ajouter les dépendances du pom

Pour la mise en place de ehcache, ajouter ces lignes dans le fichier pom.xml:

<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-ehcache</artifactId>
  <version>4.1.7</version>
  <scope>compile</scope>					
</dependency>

ETAPE 2. Configurer Spring

Le fichier de configuration de spring doit être configuré en ajoutant ces lignes où les noms sont explicités.

<!-- ehcache -->
 <!-- ehcache -->
   <cache:annotation-driven cache-manager="ehcacheManager"/>
   <bean id="ehcacheManager"
     class="o.s.cache.ehcache.EhCacheCacheManager"
     p:cacheManager-ref="ehcacheMFB"/>

     <bean id="ehcacheMFB" 
      class="o.s.cache.ehcache.EhCacheManagerFactoryBean"
     p:configLocation="ehcache.xml"
     p:cacheManagerName="ehcacheManager"
     p:shared="true"/>

NOTES.

Ici nous avons mis de côté le principe convention over configuration pour mieux détailler la configuration.

L'abréviation o.s désigne org.springframework.

Dans le bean ehcacheMFB nous faisons référence à ehcache.xml qui sera détaillé ci-après.

ETAPE 3. Configurer Ehcache

La configuration de ehcache est centrée dans le fichier ehcache.xml déjà mentionné dans la configuration de spring.

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
 <defaultCache eternal="false" maxElementsInMemory="100" 
    overflowToDisk="false" />
 <cache name="cacheLivs"
  maxElementsInMemory="10000" eternal="false" 
  timeToIdleSeconds="300"
  timeToLiveSeconds="600"
  overflowToDisk="false" />
</ehcache>

ETAPE 4. Ecrire la classe du model (entité)

La classe Livraison est l'unique classe entité du model.

package fr.netapsys.springehcache.model.common;

import java.util.Date;
import javax.persistence.*;

@Entity
public class Livraison  {
	@Id @GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;
	private String libelle;
	private String quadriRegion;
	
	@Temporal(TemporalType.DATE)
	private Date dateLivraison;
	@Temporal(TemporalType.DATE)
	private Date dateDernInteg;
	@Temporal(TemporalType.DATE)
	
      /*omis ge(se)tters et toString*/

Vous remarquez l'absence d'annotations @Cache sur l'entité car nous ne souhaitons pas utiliser le cache de second niveau d'Hibernate.

ETAPE 5. Ecrire l'interface du service

Voici l'interface de la classe nommée ILivraisonService qui comporte des méthodes de recherche (finder).

Ces méthodes sont annotées avec l'annotation de spring @Cacheable en indiquant comme attribut le nom du cache fourni à l'étape 3.

public interface ILivraisonService {
	
	@Cacheable(value="livraisons")
	Livraison findOne(long id);
	
	@Cacheable(value="livraisons")
	List<Livraison> findAll();
	
	Livraison save(Livraison liv);
}

Seules les deux méthodes findOne et findAll sont annotées avec @Cacheable en indiquant le nom du cache configuré dans ehcache.xml.

ETAPE 6. Ecrire l'implémentation du service

Ici aucun détail d'implémentation n'est abordée pour la couche DAO.

La classe de service s’appuiera sur l'interface DAO.

Le code de la classe LivraisonService est simple:

package fr.netapsys.springehcache.service;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import fr.netapsys.springehcache.model.common.Livraison;
import fr.netapsys.springehcache.repository.ILivraisonRepository;

@Service
public class LivraisonService implements ILivraisonService {

 @Autowired private ILivraisonRepository livRepo;

 /**** Methodes Finder *****/
 @Override
 public List<Livraison> findAll() {
 	return livRepo.findAll();
 }
 @Override
 public Livraison findOne(long id) {
	return livRepo.findOne(id);
 }
/**** Methodes de MAJ *****/

 @Override
 public Livraison save(Livraison liv) {
 	return livRepo.save(liv);
 }	
 @Override
 public void delete(Livraison liv) {
	livRepo.delete(liv);
 }	
}

Notez que l'interface ILivraisonRepository n'apporte rien à notre démo.

Pour les curieux, mon billet sur Spring-Data donne les détails sur cette implémentation utilisant Spring-DATA (voir Partie1 et partie2).

ETAPE 7. Désactiver le cache de second niveau Hibernate (Optionnelle)

Si vous utilisez l'implémentation Hibernate pour votre couche DAO, il serait utile lors la définition du bean entityManagerFactory de mettre la propriété suivante à false:

<prop key="hibernate.cache.use_second_level_cache">false</prop>

ETAPE 8. Tester

Voici le code du test JUnit :

package fr.netapsys.springehcache.tests;
import static junit.framework.Assert.assertSame;
import static junit.framework.Assert.assertTrue;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import fr.netapsys.springehcache.model.common.Livraison;
import fr.netapsys.springehcache.service.ILivraisonService;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations=
     {"classpath*:config/applicationContext.xml" })
public class CacheableLivraisonTest
{
    @Autowired ILivraisonService livService;
    
  @Test public void testFindOne(){
	/*** fixer la valeur de l'id en fonction de la BD**/
	final int id=89;
	Livraison liv1=livService.findOne(id);
	/** assret suivante depend du contenu de la BD***/
	assertTrue(liv1.getLibelle().contains("pdl"));
	
	Livraison liv2=livService.findOne(id);
	
	assertSame(liv1,liv2);
	
	liv2=livService.findOne(id);
	
	assertSame(liv1,liv2);
 }
}

Il ne reste qu'à lancer la commande mvn test.

Vous constatez alors que le second appel à findOne ne génère pas un appel à la base de données.

L'assertion assertSame met en évidence que c'est la même instance récupérée du cache.

Avec mon implémentation de la couche DAO basée sur spring-data (JPA), j'obtiens les traces suivantes en ayant au préalable configuré log4j:

DEBUG (AbstractFallbackCacheOperationSource.java:105) 
   Adding cacheable method 'findOne' with attribute:
 [CacheableOperation[public abstract 
   fr.netapsys.springehcache.model.common.Livraison 
  fr.netapsys.springehcache.service.ILivraisonService.findOne(long)]
  caches=[cacheLivs] | condition='' | key='']
INFO  (EhCacheManagerFactoryBean.java:100) 
  Initializing EHCache CacheManager
Hibernate: 
  /* load fr.netapsys.springehcache.model.common.Livraison */ 
 select
      livraison0_.id as id0_0_,
        livraison0_.dateData as dateData0_0_,
        livraison0_.dateDernInteg as dateDern3_0_0_,
        livraison0_.dateFichierEns as dateFich4_0_0_,
        livraison0_.dateFichierInt as dateFich5_0_0_,
        livraison0_.dateLivraison as dateLivr6_0_0_,
        livraison0_.dateLivraisonSoc as dateLivr7_0_0_,
        livraison0_.libelle as libelle0_0_,
        livraison0_.quadriRegion as quadriRe9_0_0_ 
    from
        public.Livraison livraison0_ 
    where
        livraison0_.id=?
TRACE (BasicBinder.java:83) binding parameter [1] as [BIGINT] - 89

TRACE (BasicExtractor.java:72) 
   Found [2013-02-22] as column [dateData0_0_]
....
TRACE (BasicExtractor.java:72) 
     Found [PDL] as column [quadriRe9_0_0_]
..
TRACE (CacheAspectSupport.java:294) Computed cache key 89 
  for operation CacheableOperation
 [public abstract fr.netapsys.springehcache.model.common.Livraison 
  fr.netapsys.springehcache.service.ILivraisonService.findOne(long)] 
  caches=[cacheLivs] | condition='' | key=''
TRACE (CacheAspectSupport.java:294) Computed cache key 89 for 
 operation CacheableOperation
 [public abstract fr.netapsys.springehcache.model.common.Livraison 
 fr.netapsys.springehcache.service.ILivraisonService.findOne(long)] 
  caches=[cacheLivs] | condition='' | key=''
INFO  (EhCacheManagerFactoryBean.java:133) 
     Shutting down EHCache CacheManager

Les traces sont bien parlantes surtout à la fin.

Vous pouvez voir que le premier appel de findOne a généré une requête select * from Livraison where id=?

Mais le second appel à findOne se contente de chercher le résultat depuis le cache cacheLivs.

Concluons:

Il suffit d'ajouter les dépendances maven, d'annoter les méthodes concernées, de configurer le cacheManager(Factory) d'Ehcache et de laisser l'AOP Spring opérer.

Le réglage du cacheManager(Factory) est d'important pour garantir la cohésion des données mais ceci est un autre sujet.

@Enjoy

3 commentaires

  1. Deux autres avantages à l’utilisation du cache via spring:
    – les annotations sont découplées d’ehcache et permettent notamment d’utiliser une gestion simple de cache via des maps si l’on ne souhaite pas embarquer ehcache.
    – il permet de gérer le cache de manière déclarative directement dans le contexte spring via l’utilisation d’un aspect, permettant de mettre en place un cache sur l’appel de méthodes de services dont on ne veut ou ne peut pas modifier le code source en ajoutant des annotations.

  2. Bonjour,
    parfait comme explication.
    J’ai une question s-v-p
    est ce que ehcache support la cache de les class fille je vous donne un exemple
    si par exemple qu’on a une class mère animal est une class fille chat
    est ce qu’il y’a une possibilité de cacher seulement la class fille  » chat  » sans cacher Animal
    je suis en stage fin d’étude est je bloquer

    Merci.

  3. Merci,
    Pour répondre rapidement, oui y a toujours un moyen.
    SI je comprends bien la question, vous pourriez cacher la classe fille en mettant l’annotation @Cacheable uniquement sur la classe fille.
    Aussi, avec la config aop pour pouvez sinon cibler ce que vous voulez.
    Sinon, vous pouvez facilement personnaliser/créer votre propre annotation pour votre cas particulier.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.