Mettre en place un cache sur spring avec Caffeine

A l'heure des microservices, des applications distribuées, du tout REST, etc... on appele de plus en plus de web-services dans nos applications. Cependant, un appel d'un webservice peut être couteux en temps et/ou ressource.

Dans le cas où la réponse a cet appel ne varie pas ou peu dans le temps, il peut être interessant de mettre en place un systeme de cache.

Un cache ? Qu'est-ce ?

Le cache est un espace de stockage qui peut prendre diverses formes qui a pour but de fournir des données rapidement.

pour cela, voici le déroulé :

  • notre application demande une ressource
  • on regarde dans le cache si il y a une ressource correspondante qui y est stockée
    • Si non, on va récupérer la ressource depuis sa source originale, et on stocke une copie dans le cache
    • Si oui, on renvoie la ressource en cache

Ainsi, à partir du moment où on a récupéré notre asset une première fois, on ne retournera plus que la valeur en cache. Pour éviter que le cache ne soit trop en décalage avec des mises à jour de la ressource réelle, il existe des mécanismes de purge du cache, ainsi qu'un système d'expiration.

Mise en place avec spring boot et Caffeine

Dépendances

Pour mettre en place un cache, il faut une implémentation. Dans notre cas, nous utiliserons Caffeine (mais d'autres implémentations existent). Pour cela, on veut rajouter 2 dépendances à norte projet.

Pour les utilisateurs de Maven, on va donc ajouter ces 2 dépendances:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.7.0</version>
</dependency>

Configuration

On peut donc désormais configurer notre application. Pour cela on doit ajouter à une classe de configuration Spring (annotée @Configuration) l'anotation @EnableCaching. Le plus simple est de créer une classe de configuration dédiée à la conf du cache.

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class CacheConfig {}

Dans cette classe, on va y ajouter la déclaration de notre cache sous la forme d'un bean Spring.

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class CacheConfig {
    public final static String CACHE_NAME = "cacheOne";

    @Bean
    public Cache cacheOne() {
        return new CaffeineCache(CACHE_NAME, Caffeine.newBuilder()
                .expireAfterWrite(20, TimeUnit.MINUTES)
                .build());
    }
}

On déclare ainsi un nouveau cache, qui a le nom cacheOne, et un TTL (Time to live) de 20 minutes. Cela signifie que la ressource mise dans mon cache sera conservée pendant 20 minutes, puis purgée.

Utilisation du cache

Maintenant, notre cache est prêt à être utilisé.

Pour cela, je vais dans le DAO qui effectue une opération couteuse

@Repository
public class ExternalWebserviceDao {

    public Data getMyExpensiveData() {
        //Method duration : 3 minutes
    }
}

et j'annote ma méthode avec l'annotation @Cacheable en donnant comme valeur le nom du cache à utiliser :

@Repository
public class ExternalWebserviceDao {

    @Cacheable(CacheConfig.CACHE_NAME)
    public Data getMyExpensiveData() {
        //Method duration : 3 minutes
        //With cache : less than 1s
    }
}

Avec ceci, l'implémentation réelle de ma méthode ne sera appelée que pour la mise en cache, puis tous les autres appels a cette méthode renverront le contenu mis en cache (jusqu'a expiration de celui-ci). Ainsi, au lieu des 3 minutes de traitement habituelles, le résultat sera quasi instantané.