---
title: "JAVA : Mise en place d'un cache mémoire."
canonical: "https://blog.kaptus.net/space/BKAP/blog/327876642/JAVA%20%3A%20Mise%20en%20place%20d'un%20cache%20m%C3%A9moire."
format: markdown
---
> Macro (toc)

# 1. Introduction.

Après avoir mis en place le cryptage d’informations (considérées comme sensibles) stockées dans une base de données, je me suis rendu compte que les temps de restitutions de certaines pages dans l’interface de l’application était trop longue (**entre une et deux secondes**).

À chaque affichage d’une donnée cryptée, on la décrypte. Ce qui n’est pas optimal en terme de performance car on peut être amené à décrypter une même donnée plus d’une dizaine de fois dans un intervalle de temps assez court.

L’idée est de mettre en place un cache mémoire pour stocker les données qui ont déjà été décryptées afin d'éviter des temps de calculs inutiles. 

Techniquement, il n’y a rien de révolutionnaire, mais je voulais partager la démarche et le cheminement que j’ai suivi pour arriver au résultat final.

# 2. Le contexte.

Pour une application en cours de développement, j’ai été amené à crypter les données (***que j’ai considéré sensible***) dans ma base de données. Par exemple les adresses électroniques : 

![image-20240414-072539.png](media://e71c1e11-721e-4a79-b7c3-d7a3d8371517)

> 📝 L’algorithme de cryptage choisi est de type [ECC (Elliptic Curve Cryptography)](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography).

Après avoir mis tout ça en place, je me suis rendu compte que la page ci-dessous : 

> Macro (drawio)

mettait plus de **1s côté serveur** pour être traité (**2 à 3s côté navigateur**). La raison est simple, j’ai **16 adresses électroniques** et **11 numéros de téléphone** à décrypter.

Après discussion avec mon fiston ([Nathan TOMASIAN](https://www.linkedin.com/in/nathan-tomasian-642995201/)), il me suggère de mettre un cache en place côté application. Tellement la tête dans le guidon, que je n’ai même pas eu l’idée 🥲 

# 3. Le besoin.

Mon besoin est assez basique. Il faut que :

1. je puisse mettre en cache un objet qui soit accessible par une clé (***qui dans mon cas sera la donnée encryptée***).
2. je puisse limiter le nombre d’objets que je vais stocker afin de ne pas saturer la mémoire utiliser par mon application (en Java) pour éviter un `Out Of Memory`.
3. lorsque je suis en limite de capacité du cache,  je puisse remplacer le plus ancien objets stockés par le nouvel entrant.
4. le cache soit en mémoire et pas sur un support non-volatile.

# 4. L’approche.

La première étape est de regarder ce qui existe en Java pour répondre à mon problème.

J’ai trouvé plusieurs solutions pour mettre en place un cache dans une application Java : 

1. [JCS](https://commons.apache.org/proper/commons-jcs/)
2. [REDIS](https://redis.io/)
3. [HAZELCAST](https://hazelcast.com) (cache distribué)
4. [MEMCACHED](https://memcached.org/) (cache distribué)
5. [CAFFEINE](https://github.com/ben-manes/caffeine)
6. [EHCACHE](https://www.ehcache.org/)
7. …

> 📝 Mais après réflexion, j’ai préféré faire simple et mettre en place un **cache mémoire** directement dans mon application.

> ❌ Je ne peux pas me permettre d’utiliser **un cache qui stockerai les informations sur disque** car je perdrai tout le bénéfice de **la sécurité qui a été mis en place en cryptant les données en base** qui serait décrypté dans sur un disque …

Mon premier travail est de déterminer l’emprunte mémoire du cache afin de définir le nombre d’objet maximum que je vais vouloir stocker.

## 4.1. Queue `FIFO`.

Pour la gestion du nombre maximum d'éléments présent dans le cache, on va utiliser une queue `FIFO (First In, First Out)`, dans laquelle on va stocker les clés suivant leurs ordres d'insertion.

Pour déterminer l’emprunte mémoire du cache, j’ai fait quelques tests avec le code ci-dessous : 

```java
package fr.kaptus.queue;

import fr.kaptus.utilities.Sandbox;
import org.apache.commons.lang3.RandomStringUtils;

import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.random.RandomGenerator;

import static fr.kaptus.utilities.SizeUnitBytes.GB;

public class TestFifoQueueMemorySize {

    private static final int NB_ELEMENTS = 500000;
    private static final int MIN_SIZE = 56 ;
    private static final int MAX_SIZE = 564 ;
    public static void main(String[] args) {
        Sandbox.printMemory(GB);
        Sandbox.start();

        LinkedList<String> fifoQueue = new LinkedList<>();
        // Adding elements to the queue
        for (int i = 0; i < NB_ELEMENTS; i++) {
            fifoQueue.add(RandomStringUtils.randomAlphabetic(RandomGenerator.getDefault().nextInt(MIN_SIZE, MAX_SIZE)));
        }

        Sandbox.printMemory(GB);

        Sandbox.stop();
        System.out.println(Sandbox.getTimeMessage(TimeUnit.MILLISECONDS));
    }
}
```

Je rempli la queue de type `LinkedList` avec ***500K chaines de caractères*** dont la ***taille est comprise entre 56 et 564*** (c’est la taille min et max de mes données encryptées). 

Le résultat : 

![image-20240415-115815.png](media://78e88be2-3958-4ded-8c15-8b8ba31b5d4a)


> 📝 ***Soit environ 200Mo de mémoire occupée***

Ce qui reste plus que raisonnable pour mon application.

## 4.2. `LinkedHashMap`.

En investiguant un peu, je découvre que l’objet `LinkedHashMap` est beaucoup plus adapté adapté à mes besoins.

Pour déterminer l’emprunte mémoire du cache, j’ai fait quelques tests avec le code ci-dessous : 

```java
package fr.kaptus.queue;

import fr.kaptus.utilities.Sandbox;
import org.apache.commons.lang3.RandomStringUtils;

import java.util.LinkedHashMap;
import java.util.concurrent.TimeUnit;
import java.util.random.RandomGenerator;

import static fr.kaptus.utilities.SizeUnitBytes.GB;

public class TestLinkedHashMapMemorySize {

    private static final int NB_ELEMENTS = 500000;
    private static final int MIN_SIZE_KEY = 56;
    private static final int MAX_SIZE_KEY = 564;
    private static final int MIN_SIZE_VALUE = 10;
    private static final int MAX_SIZE_VALUE = 60;

    public static void main(String[] args) {
        Sandbox.printMemory(GB, true);
        Sandbox.start();

        LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>(NB_ELEMENTS);
        // Adding elements to the LinkedHashMap
        for (int i = 0; i < NB_ELEMENTS; i++) {
            String stringKey = "-" + i + "-" + RandomStringUtils.randomAlphabetic(RandomGenerator.getDefault().nextInt(MIN_SIZE_KEY, MAX_SIZE_KEY));
            String stringValue = "-" + i + "-" + RandomStringUtils.randomAlphabetic(RandomGenerator.getDefault().nextInt(MIN_SIZE_VALUE, MAX_SIZE_VALUE));
            linkedHashMap.put(stringKey, stringValue);
        }
        Sandbox.printMemory(GB);
        Sandbox.stop();
        System.out.println(Sandbox.getTimeMessage(TimeUnit.MILLISECONDS));
    }
}
```

L’espace mémoire utilisé par ***500K chaines de caractères*** dont la ***taille de la clé est comprise entre 56 et 564*** et ***la taille des valeurs stockées sont comprises entre 10 et 60*** est de : 

![image-20240415-115650.png](media://6aad5612-7661-420f-8ea1-32d0cb9f420a)

> 📝 ***Soit environ 400Mo à 450Mo de mémoire occupée***

Ce qui reste plus que raisonnable pour mon application.

> 📝 3/9/2024 → J’ai fait des tests de suppression et d’ajout pour valider le fonctionnement `FIFO` de l'objet `LinkedHashMap` et ça fonctionne très bien.

> ℹ️ 3/9/2024 → Pendant l’implémentation de la solution, je me suis dis que d’autres personnes avaient certainement eu la même approche que moi et qu’ils y avaient probablement apportées une solution. Du coup je suis tombé sur l’article :   
> ℹ️ [https://www.linkedin.com/pulse/building-lru-cache-java-simple-approach-using-weberton-faria-0cglc](https://www.linkedin.com/pulse/building-lru-cache-java-simple-approach-using-weberton-faria-0cglc) qui répond exactement à mon besoin.  
> ℹ️ J’implémente donc la solution avec ma touche personnelle.

## 4.3. Implémentation d’un cache `LinkedHashMap` de type `LRU`.

`LRU` veut dire `Least Recently Used`. Globalement c’est le principe de la mise en cache mémoire d’information de type `FIFO`. Comme la mémoire n'est jamais infinie sur un serveur, on est obligé de limiter le nombre d'éléments qui vont être stockés dans le cache. Pour cela, on impose une limite à l'objet qui va nous servir de cache et lorsque l'on ajoute un élément alors que le cache a atteint sa limite, on supprime l'élément le plus âgé pour y insérer le nouvel élément. Du coup, la queue se décale vers le haut.

La classe `LRUCache` : 

```java
 package com.webminut.cache;

import java.util.LinkedHashMap;
import java.util.Map;

class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private static final int DEFAULT_CAPACITY = 10000;
    private final int capacity;

    public LRUCache() {
        super(DEFAULT_CAPACITY, 0.75f, true);
        this.capacity = DEFAULT_CAPACITY;
    }

    public LRUCache(int capacity) {
        /*
         * capacity     : La capacité initiale qui ne pourra plus être modifiée.
         * loadFactor   : facteur de charge utilisé lors de la réorganisation des enregistrements.
         * accessOrder  : permet d'indiquer qu'on veut conserver l'ordre dans lequel les objets sont ajoutés.
         *                Très important dans le cas d'une utilisation de type cache.
         */
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > this.capacity;
    }
}
```

Mon singleton pour mettre en cache mes données cryptées : 

```java
package com.webminut.cache;

public final class CryptoCache {
    private static final int DEFAULT_CAPACITY = 100000;

    private final LRUCache<String, String> cryptoMemoryCache = new LRUCache<>(DEFAULT_CAPACITY);

    private static final CryptoCache INSTANCE = new CryptoCache();
    private CryptoCache() {
        super();
    }

    public static CryptoCache getInstance() {
        return INSTANCE ;
    }

    public void put(String key, String value) {
        /*
         * On limite le nombre de valeurs du cache à DEFAULT_CAPACITY.
         * Cette limitation est géré par l'objet LRUCache.
         */
        this.cryptoMemoryCache.put(key,value) ;
    }

    public String get(String key) {
        return this.cryptoMemoryCache.get(key) ;
    }
}
```

> ℹ️ 3/9/2024 → La taille initiale de mon cache sera de **100 000 objets**. J’ai pu valider avec mes tests que je pourrai aller jusqu'à **500 000 objets** sans trop pénaliser mon application. La mise en place d'**une pagination sera nécessaire pour avoir des performances convenables ?**

Prise en compte du cache dans mon code. J’ai une méthode `decryptString` qui est appelée chaque fois qu’il faut décrypter une donnée. Je la modifie pour prendre en en compte la gestion du cache : 

```java
 public static String decryptString(String decryptBase64Data)
            throws NoSuchAlgorithmException,
            InvalidKeySpecException,
            InvalidAlgorithmParameterException,
            IllegalBlockSizeException,
            NoSuchPaddingException,
            BadPaddingException,
            InvalidKeyException {
        // On regarde si l'élément est dans le cache.
        CryptoCache cryptoCache = CryptoCache.getInstance();
        String decryptedString = cryptoCache.get(decryptBase64Data);
        if (null == decryptedString) {
            /*
             * S'il n'est pas dans le cache.
             * - On décrypte l'élément
             * - On l'ajoute dans le cache
             */
            decryptedString = getEc25519Key().decryptMessageBase64(decryptBase64Data);
            cryptoCache.put(decryptBase64Data, decryptedString);
        }
        return decryptedString;
}
```

> ℹ️ 3/9/2024 → Les tests sont concluant, on gagne ***un facteur compris entre 9 et 15*** pour l’affichage de la page des `ssl-m!nut`.
> ℹ️ 
> ℹ️ Avant l’installation du cache :
> ℹ️ 
> ℹ️ ![image-20240414-162923.png](media://78407308-34fe-4559-b953-bd0bb752d6e2)
> ℹ️ 
> ℹ️ Après l’installation du cache :
> ℹ️ 
> ℹ️ ![image-20240414-163031.png](media://d5288416-38b6-4b80-b605-52027cb8b92a)

## 4.4. Chargement du cache lors du démarrage de TOMCAT.

Après avoir déployé sur le serveur de production, il restait un problème à résoudre. Lorsque le cache est vide, le premier affichage de la page était toujours aussi long. L’idée est de mettre en place un système permettant d’alimenter le cache lors  du démarrage du serveur d’application.

Le démarrage prendra plus de temps, mais l’affichage de la page sera plus fluide même lors du premier accès.

> 📝 3/9/2024 → Mon serveur d’application est **Apache TOMCAT 9.0.87**

Pour ça, on va utiliser `ServletContextListener` qui permet d’exécuter un programme au lancement et à l’arrêt du serveur d’application.

La classe qui va hérité de `ServletContextListener` : 

```java
package com.webminut.listener;

import com.webminut.database.*;
import com.webminut.servlets.GenericWebminutServlet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.UnavailableException;
import javax.sql.DataSource;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;

public class WebminutServletContextListener implements ServletContextListener {
    private static final Logger LOGGER = LogManager.getLogger(WebminutServletContextListener.class);

    @Override
    public void contextDestroyed(ServletContextEvent arg0) {
        LOGGER.debug("WebminutServletContextListener destroyed");
    }

    //Run this before web application is started
    @Override
    public void contextInitialized(ServletContextEvent arg0) {
        System.out.println("WebminutServletContextListener started");
        String value;
        Connection connexion = null;
        try {
            DataSource dataSource = GenericWebminutServlet.getDataSource();
            connexion = dataSource.getConnection();
            List<SslAlertEmail> sslAlertEmailList = SslAlertEmail.getAllSslAlertEmail(connexion);
            for (SslAlertEmail sslAlertEmail : sslAlertEmailList) {
                value = sslAlertEmail.getString(SslAlertEmail._EMAIL);
            }
            List<SslAlertSms> sslAlertSmsList = SslAlertSms.getAllSslAlertSms(connexion);
            for (SslAlertSms sslAlertSms : sslAlertSmsList) {
                value = sslAlertSms.getString(SslAlertSms._MOBILE);
            }
            List<Company> companyList = Company.getAllCompany(connexion);
            for (Company company : companyList) {
                for (int index : Company.ENCRYPTED_INDEX_COLUMN_NAMES) {
                    value = company.getString(index);
                }
            }
            List<Contact> contactList = Contact.getAllContact(connexion);
            for (Contact contact : contactList) {
                for (int index : Contact.ENCRYPTED_INDEX_COLUMN_NAMES) {
                    value = contact.getString(index);
                }
            }
            List<PostalAddress> postalAddressList = PostalAddress.getAllPostalAddress(connexion);
            for (PostalAddress postalAddress : postalAddressList) {
                for (int index : PostalAddress.ENCRYPTED_INDEX_COLUMN_NAMES) {
                    value = postalAddress.getString(index);
                }
            }
        } catch (UnavailableException |
                 SQLException |
                 InvocationTargetException |
                 InstantiationException |
                 IllegalAccessException |
                 NoSuchMethodException e) {
            LOGGER.debug(e.getMessage(), e);
        } finally {
            if (connexion != null) {
                try {
                    connexion.close();
                } catch (SQLException e) {
                    LOGGER.debug(e.getMessage(), e);
                }
            }
        }
    }
}
```

Ce code est vraiment spécifique à mon application. En fait, j’ai un mapping objets à ma sauce et je maintiens un tableau (`ENCRYPTED_INDEX_COLUMN_NAMES`) qui me permet de savoir quelles sont les colonnes dans les tables de ma base qui sont cryptées. Pour chaque colonnes cryptées que j’essaie de lire avec la méthode `getString`, je rempli le cache mémoire de mon application.

> 📝 4/14/2024 → Pour l’instant, je n’ai pas pris en compte la taille limite du cache car les données dans la base sont bien en dessous. Mais après la mise en production de l’application, je modifierai mon code pour ne pas aller au-delà de la limite imposée.

Pour prendre en compte le lancement du chargement du cache au démarrage du serveur d’application, il faut ajouter dans le fichier `web.xml` les lignes ci-dessous : 

```xml
     <listener>
        <listener-class>
            com.webminut.listener.WebminutServletContextListener
        </listener-class>
    </listener>
```

Juste après le bloc `filter-mapping`.

Après avoir relancé le serveur d’application, le cache se rempli avec les éléments présent dans la base :

![image-20240414-164806.png](media://71bd7484-afca-4f34-a79d-1732a25a6e4c)

Et effectivement l’affichage de ma page est beaucoup plus rapide.

# 5. En conclusion.

C'était très intéressant et très instructif de travailler sur ce sujet. C’est une problématique que je n’avais jamais abordée de bout en bout. J’ai été amené au cours de mon expérience de développeur de travailler avec un cache distribué (REDIS, HAZELCAST) mais qui avait déjà été préconiser et installer par d’autres équipes.  
La solution mise en place n’est certes pas parfaite et demandera des ajustements lorsque l’application sera en production et qu’il y aura (je me le souhaites 🙂 ) beaucoup de clients qui généreront beaucoup de connexion. Mais dans un premier temps elle réponds parfaitement à mes besoins et à mes attentes.

> 📝 3/11/2024 → Après avoir déployé l’application sur le serveur de production, j’ai posé la question suivante à ChatGPT : 
> 📝 
> 📝 ![image-20240414-165854.png](media://81562f8b-0b4a-4fcd-b271-f6e9c2670465)
> 📝 
> 📝 Et la réponse a été très pertinente : 
> 📝 
> 📝 ![image-20240414-170057.png](media://7545341b-1cd5-4db9-87b1-84d14be0365c)
> 📝 
> 📝 J’aurais pu gagner du temps si j’ai pensé à la faire dés le départ. Mais je suis plutôt satisfait de ma démarche car ça **ma permit de monter en compétence sur le sujet**. Ce qui n’aurait pas été le cas si j’avais utilisé directement ChatGPT.

# 6. Liens utiles.

| **Date** | **Lien** | **Commentaire** |
| --- | --- | --- |
| 2/14/2024 | [https://www.delftstack.com/howto/java/fifo-queue-java/](https://www.delftstack.com/howto/java/fifo-queue-java/) |  |
| 1/19/2024 | [https://www.linkedin.com/pulse/building-lru-cache-java-simple-approach-using-weberton-faria-0cglc](https://www.linkedin.com/pulse/building-lru-cache-java-simple-approach-using-weberton-faria-0cglc) | Exactement ce dont j’ai besoin. 👍 |
| 1/8/2024 | [https://www.baeldung.com/java-hashmap-load-factor](https://www.baeldung.com/java-hashmap-load-factor) | Un explication de l’utilisation de l’attribut load factor. |
| 10/25/2023 | [https://ioflood.com/blog/java-queue/](https://ioflood.com/blog/java-queue/) |  |
| 8/24/2023 | [https://medium.com/@furkanalniak/java-caching-pro-tricks-for-optimal-performance-d1a1eb5f5cc9](https://medium.com/@furkanalniak/java-caching-pro-tricks-for-optimal-performance-d1a1eb5f5cc9) | Article intéressant sur l’utilisation de différents cache en Java. |
| 7/5/2014 | [https://mkyong.com/servlet/what-is-listener-servletcontextlistener-example/](https://mkyong.com/servlet/what-is-listener-servletcontextlistener-example/) | Un exemple de configuration du `ServletContextListener` dans Apache TOMCAT. |