Gérer le Cache-Control HTTP dans une application Web Java EE avec Tomcat

Dans cet article, nous allons voir comment optimiser encore plus le temps de chargement de vos applications/pages web grâce à Cache-Control. Le Cache-Control est en-tête du protocole HTTP (protocole utilisé par un navigateur et un serveur web pour communiquer, entre autres). Cet en-tête permet de demander à un navigateur web de place dans sa mémoire un fichier, afin d’éviter que celui-ci le re-telecharge à chaque fois du serveur. Comme à notre habitude, nous verrons cela en deux temps: d’abord l’explication du Cache-Control dans le protocole HTTP, puis son implémentation dans une application web Java EE. Nous verrons deux façons de faire, la voie simple avec juste un petit réglage et une amorce vers un réglage plus fin, en utilisant un Listener.

Les En-Tête de gestion de Cache dans le protocole HTTP

Le champ Cache-Control a la forme: Cache-Control: attribute=value. Les attributs possibles sont assez nombreux. Nous utiliserons ici l’attribut max-age. Cet attribut prend comme valeur la durée de vie maximale en cache du fichier, en secondes.

Il existe beaucoup d’attributs pour Cache-Control, Wikipedia offre une très bonne liste de ces attributs et des valeurs possibles.


Exemple de requêtes HTTP contenant Cache-Control


The Easy Way: configurer uniquement web.xml

Si vous avez besoin d’un réglage assez peu fin, vous pouvez utiliser le réglage intégré à Tomcat et à tout bon conteneur web. Dans le fichier web.xml, il suffit d’ajouter cette balise afin d’ajouter une expiration pour le cache:

[code lang= »xml »]
<web-app id=’myId’>

<cache-mapping url-regexp= »
expires=’10’/>
<!– ou bien –>
<cache-mapping url-pattern=’*.jpg’
expires=’15m’/>

</web-app>
[/code]

Le suffixe de expires définit l’unité de temps:

  • s: secondes
  • m: minutes
  • h: heure
  • D: jours

Je dois vous avouer qu’avec cette méthode, j’ai eu quelques résultats hasardeux (cette commande est implémentée par certains serveurs, mais pas par Tomcat). C’est pour cela que je préfère la Hard Way.

The Hard Way: implémentation de Cache-Control avec un Listener

Notre implémentation va, ni plus, ni moins ajouter un champ dans les headers HTTP émis par notre serveur. Pour cela, nous allons utiliser créer un Listener, puis le configurer via web.xml (fichier standard paramétrant toute application web.

Création du Listener

Nous allons créer un Listener afin d’ajouter automatiquement un header à chaque requete.

[code lang= »java »]/**
*
*/
package com.diilo.ncs.skeleton.web;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

/**
* This filter adds an expire date for caching in HTTP header.
*
* @author Eric Vialle / 5.freshminutes.it
*/
public class HeaderFilter implements Filter {

/** Logger Log4J. */
private final static Logger    LOG                    = Logger.getLogger(HeaderFilter.class);

/** Name of the parameter, defining the duration in minutes, in web.xml */
private final static String    HEADER_CONFIG_LABEL    = « durationSecondes »;

/** Field’s name of the HTTP header. */
private final static String    HEADER_GET_KEY        = « Cache-Control »;

/** template for the valueHeader of this HTTP header field. */
private final static String    HEADER_INSTRUCTIONS    = « max-age= »;

/** value for the field defined in HEADER_GET_KEY and that will be added to any HTTP header. */
private String                valueHeader;

/**
* Initialization of the Filter.
*/
public void init(FilterConfig filterConfig) throws ServletException {

String headerParam = filterConfig.getInitParameter(HEADER_CONFIG_LABEL);
if (headerParam == null) {
LOG.warn(« No headers were found in the web.xml (init-param) for the HeaderFilter ! »);
} else {

this.valueHeader = HEADER_INSTRUCTIONS + headerParam;

LOG.info(« The following headers were registered in the HeaderFilter –  » + HEADER_GET_KEY +  » :  »
+ this.valueHeader);
}
}

/**
* Called at each request.
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException {

if (valueHeader != null) {
// We add our field to the HTTP header
((HttpServletResponse) response).setHeader(HEADER_GET_KEY, this.valueHeader);
}
chain.doFilter(request, response);
}

/**
* Called when the Filter is destroyed.
*/
public void destroy() {
this.valueHeader = null;
}

}[/code]

Installation du Listener

L’installation se fait par le paramétrage du fichier web.xml. Le fichier web.xml suivant indique la classe de notre Listener (dans notre cas: com.diilo.ncs.skeleton.web.HeaderFilter) qui sera appelé pour toutes les requêtes finissant par .jpg, .png, .gif, c’est à dire à toutes les images. Dans le champs durationSeconds, nous avons indiquée quel sera la durée de notre cache en secondes.

[code lang= »xml »]<?xml version= »1.0″ encoding= »UTF-8″?>
<web-app id= »diiloncswebserver » version= »2.5″
xmlns= »http://java.sun.com/xml/ns/j2ee » xmlns:xsi= »http://www.w3.org/2001/XMLSchema-instance »
xsi:schemaLocation= »http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_5.xsd »>

<!– Add cache to files –>
<filter>
<description>Set HTTP headers for a mapping.</description>
<filter-name>HeaderFilter</filter-name>
<filter-class>com.diilo.ncs.skeleton.web.HeaderFilter</filter-class>
<init-param>
<description>Add an Expires Header</description>
<param-name>durationSecondes</param-name>
<param-value>2592000</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>HeaderFilter</filter-name>
<url-pattern>*.jpg</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
<filter-mapping>
<filter-name>HeaderFilter</filter-name>
<url-pattern>*.png</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
<filter-mapping>
<filter-name>HeaderFilter</filter-name>
<url-pattern>*.gif</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>

….

</web-app>[/code]

Sources: