
Annotations de cache (`@Cacheable`, `@CachePut`, `@CacheEvict`, `@Caching`)
Explorez en détail les annotations clés de Spring Cache (@Cacheable, @CachePut, @CacheEvict, @Caching) pour contrôler finement la mise en cache dans vos applications Spring Boot.
Introduction : Contrôle déclaratif du cache
L'abstraction de cache de Spring brille par sa simplicité d'utilisation, principalement grâce à un ensemble d'annotations déclaratives. Ces annotations permettent d'intégrer la logique de mise en cache directement dans votre code métier (typiquement au niveau des méthodes de service) sans avoir à écrire de code d'interaction explicite avec l'API du cache sous-jacent. Spring utilise AOP (Programmation Orientée Aspect) pour intercepter les appels aux méthodes annotées et appliquer le comportement de cache approprié.
Comprendre le rôle et les attributs de ces annotations est fondamental pour implémenter efficacement des stratégies de cache. Les trois annotations principales sont `@Cacheable`, `@CachePut`, et `@CacheEvict`. Une quatrième, `@Caching`, permet de regrouper plusieurs de ces opérations sur une seule méthode.
Nous allons détailler chacune de ces annotations, leurs cas d'usage typiques et leurs options de configuration clés.
@Cacheable : Mettre en cache le résultat d'une méthode
L'annotation `@Cacheable` est sans doute la plus utilisée. Son objectif principal est de mettre en cache le résultat retourné par une méthode. Avant d'exécuter la méthode annotée, Spring vérifie si une entrée correspondant à l'appel (basée sur une clé générée) existe déjà dans le cache spécifié.
- Si une entrée est trouvée (cache hit) : Le résultat stocké dans le cache est retourné directement, et la méthode annotée n'est pas exécutée du tout.
- Si aucune entrée n'est trouvée (cache miss) : La méthode annotée est exécutée normalement. Le résultat retourné est alors stocké dans le cache (associé à la clé calculée) avant d'être renvoyé à l'appelant.
C'est l'annotation idéale pour les méthodes coûteuses qui récupèrent des données qui ne changent pas fréquemment (ex: recherche d'une entité par ID, récupération de données de référence).
Attributs clés :
- `value` ou `cacheNames` (obligatoire) : Un ou plusieurs noms de caches dans lesquels stocker/rechercher l'entrée. Exemple : `cacheNames="produits"` ou `value={"produits", "detailsProduit"}`.
- `key` (optionnel) : Une expression SpEL (Spring Expression Language) pour générer la clé de cache. Par défaut, Spring utilise les paramètres de la méthode. Exemple : `key="#id"` (utilise le paramètre nommé `id`), `key="#utilisateur.username"` (utilise une propriété de l'objet `utilisateur`).
- `keyGenerator` (optionnel) : Nom d'un bean `KeyGenerator` personnalisé si la génération par défaut ou par SpEL ne suffit pas.
- `condition` (optionnel) : Une expression SpEL. Si elle évalue à `false`, la mise en cache est complètement ignorée (la méthode est toujours exécutée, et rien n'est mis en cache). Utile pour ne pas mettre en cache certains appels (ex: `condition="#id > 10"`).
- `unless` (optionnel) : Une expression SpEL évaluée après l'exécution de la méthode. Si elle évalue à `true`, le résultat n'est pas mis en cache, même si la méthode a été exécutée. Utile pour ne pas mettre en cache des résultats non souhaités (ex: `unless="#result == null"` pour ne pas cacher les `null`).
Exemple : Cacher la récupération d'un livre par son ISBN.
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class BookService {
// Simule un accès base de données lent
private Book findBookInDatabase(String isbn) {
try {
Thread.sleep(2000); // Simule 2 secondes de latence
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Accès base de données pour ISBN: " + isbn);
return new Book(isbn, "Un livre pour l'ISBN " + isbn);
}
// Le résultat sera mis en cache dans "books" avec l'isbn comme clé.
// La méthode findBookInDatabase ne sera appelée que si l'entrée n'est pas dans le cache.
@Cacheable(cacheNames = "books", key = "#isbn", unless = "#result == null")
public Book getBookByIsbn(String isbn) {
return findBookInDatabase(isbn);
}
// Exemple avec condition
@Cacheable(cacheNames = "expensiveBooks", key = "#isbn", condition = "#isbn.length() > 10")
public Book getExpensiveBook(String isbn) {
System.out.println("Récupération d'un livre potentiellement cher...");
return findBookInDatabase(isbn);
}
}
// Classe modèle simple
class Book {
private String isbn;
private String title;
public Book(String isbn, String title) { this.isbn = isbn; this.title = title; }
// Getters...
public String getIsbn() { return isbn; }
public String getTitle() { return title; }
}
@CachePut : Mettre à jour le cache sans interférer avec l'exécution
L'annotation `@CachePut` est utilisée lorsque vous voulez vous assurer que la méthode annotée est toujours exécutée et que son résultat est (re)mis en cache, potentiellement en écrasant une valeur existante.
Contrairement à `@Cacheable`, `@CachePut` n'entraîne pas l'omission de l'exécution de la méthode si une entrée existe déjà dans le cache. Son but principal est de mettre à jour le cache après une modification des données sous-jacentes.
Cas d'usage : Typiquement utilisée sur les méthodes qui modifient une entité (par exemple, une méthode `updateUser(User user)`). Après la mise à jour en base de données, vous voulez mettre à jour l'entrée correspondante dans le cache avec la nouvelle version de l'entité.
Attributs clés : Les attributs sont similaires à `@Cacheable` (`value`/`cacheNames`, `key`, `condition`, `unless`). La clé (`key`) est particulièrement importante ici pour cibler l'entrée de cache à mettre à jour. Elle est souvent basée sur l'identifiant de l'objet retourné ou d'un paramètre.
Exemple : Mettre à jour le cache après avoir modifié un livre.
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
@Service
public class BookUpdateService {
// Simule une mise à jour en base de données
private Book updateBookInDatabase(Book book) {
System.out.println("Mise à jour en base de données pour ISBN: " + book.getIsbn());
// ... logique de mise à jour ...
return book; // Retourne l'entité mise à jour
}
// La méthode est TOUJOURS exécutée.
// Le résultat (le livre mis à jour) est placé dans le cache "books"
// avec une clé basée sur l'ISBN du livre retourné.
@CachePut(cacheNames = "books", key = "#result.isbn")
public Book updateBook(Book book) {
return updateBookInDatabase(book);
}
}
Il est crucial que l'expression `key` dans `@CachePut` corresponde à celle utilisée par `@Cacheable` pour la même entité afin de mettre à jour la bonne entrée.
@CacheEvict : Supprimer des entrées du cache
L'annotation `@CacheEvict` est utilisée pour supprimer (évincer) des données du cache. Elle est essentielle pour maintenir la cohérence du cache lorsque les données sous-jacentes sont supprimées ou deviennent invalides.
Cas d'usage :
- Après la suppression d'une entité (ex: méthode `deleteBook(String isbn)`), pour supprimer l'entrée correspondante du cache.
- Après une opération qui invalide un ensemble de données mises en cache (ex: mise à jour de masse, vidage d'une table), pour vider complètement un ou plusieurs caches.
Attributs clés :
- `value` ou `cacheNames` (obligatoire) : Les noms des caches à affecter.
- `key` (optionnel) : Expression SpEL pour identifier l'entrée spécifique à supprimer. Si non spécifié et si `allEntries=false`, utilise la génération de clé par défaut basée sur les paramètres.
- `allEntries` (optionnel, défaut `false`) : Si `true`, supprime toutes les entrées des caches spécifiés, ignorant l'attribut `key`. Utile pour une invalidation complète.
- `beforeInvocation` (optionnel, défaut `false`) : Si `true`, l'éviction se produit avant l'exécution de la méthode. Par défaut (`false`), elle a lieu après l'exécution réussie de la méthode. Mettre à `true` est utile si la méthode elle-même peut échouer mais que vous voulez quand même vider le cache, mais peut être dangereux si l'opération sous-jacente échoue et que le cache est vidé prématurément.
- `condition` (optionnel) : Expression SpEL. L'éviction n'a lieu que si la condition est `true`.
Exemple : Supprimer une entrée après une suppression ou vider un cache.
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
@Service
public class BookRemovalService {
// Simule une suppression en base de données
private void deleteBookFromDatabase(String isbn) {
System.out.println("Suppression en base de données pour ISBN: " + isbn);
// ... logique de suppression ...
}
// Après l'exécution réussie de la méthode, supprime l'entrée
// du cache "books" dont la clé est l'isbn passé en paramètre.
@CacheEvict(cacheNames = "books", key = "#isbn")
public void deleteBook(String isbn) {
deleteBookFromDatabase(isbn);
}
// Après l'exécution réussie, vide complètement le cache "books".
@CacheEvict(cacheNames = "books", allEntries = true)
public void clearBookCache() {
System.out.println("Vidage complet du cache 'books'.");
// Peut-être suite à une opération de mise à jour de masse
}
// Eviction avant invocation (à utiliser avec précaution)
@CacheEvict(cacheNames = "books", key = "#isbn", beforeInvocation = true)
public void riskyDeleteBook(String isbn) {
System.out.println("Tentative de suppression risquée pour ISBN: " + isbn);
if (isbn.length() < 5) { // Simulation d'un échec potentiel de la méthode
throw new IllegalArgumentException("ISBN trop court pour suppression");
}
deleteBookFromDatabase(isbn);
}
}
@Caching : Combiner plusieurs opérations de cache
Parfois, une seule méthode peut nécessiter plusieurs opérations de cache affectant différents caches ou utilisant différentes clés. Par exemple, une méthode qui met à jour un utilisateur pourrait nécessiter :
- De mettre à jour l'entrée de l'utilisateur dans le cache `users` (`@CachePut`).
- De supprimer l'entrée correspondante dans un cache `userRoles` (`@CacheEvict`).
L'annotation `@Caching` permet de regrouper plusieurs annotations `@Cacheable`, `@CachePut`, et/ou `@CacheEvict` pour les appliquer à une seule méthode.
Attributs :
- `cacheable` : Un tableau de `@Cacheable`.
- `put` : Un tableau de `@CachePut`.
- `evict` : Un tableau de `@CacheEvict`.
Exemple : Mettre à jour un utilisateur et vider un cache lié.
import org.springframework.cache.annotation.Caching;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
@Service
public class UserManagementService {
// Simule une mise à jour
private User updateUserInDb(User user) {
System.out.println("Mise à jour de l'utilisateur: " + user.getUsername());
// ...
return user;
}
@Caching(
put = { @CachePut(cacheNames = "users", key = "#user.username") },
evict = {
@CacheEvict(cacheNames = "userSearchResults", allEntries = true), // Vide un cache de recherche
@CacheEvict(cacheNames = "userPermissions", key = "#user.username") // Vide les permissions spécifiques
}
)
public User updateUser(User user) {
return updateUserInDb(user);
}
}
// Classe modèle simple
class User {
private String username;
// Getters, setters...
public String getUsername() { return username; }
}
Dans cet exemple, lorsque `updateUser` est appelée, Spring va d'abord exécuter la méthode, puis effectuer les opérations de cache définies : mettre à jour l'entrée dans le cache `users`, vider entièrement le cache `userSearchResults`, et supprimer l'entrée spécifique dans le cache `userPermissions`.
Conclusion : Déclarer votre stratégie de cache
Les annotations `@Cacheable`, `@CachePut`, `@CacheEvict`, et `@Caching` fournissent un moyen puissant et déclaratif d'intégrer la mise en cache dans vos applications Spring Boot. En les utilisant judicieusement, vous pouvez améliorer considérablement les performances sans polluer votre code métier avec la logique d'interaction du cache.
Le choix de la bonne annotation, la définition précise des clés, et l'utilisation réfléchie des conditions et des options d'éviction sont essentiels pour mettre en place une stratégie de cache efficace et cohérente.