Web listeners et filtres

Les servlets ne sont pas les seuls composants gérés par le conteneur Web, ce dernier a également la responsabilité de gérer le cycle de vie des listeners et des filtres Web.

Les listeners Web

Un listener (écouteur) est un terme couramment utilisé en Java pour désigner un objet qui sera notifié lors d’une modification de son environnement. Dans les patrons de conception (Design Patterns) Objet, on l’appelle plus généralement Observateur.

Pour le conteneur Web Java EE, un listener designe une classe qui implémente une des interfaces définies dans l’API servlet.

Le principe d’utilisation est le même pour tous les types de listeners Web. Si l’application souhaite être avertie d’un événement particulier survenant pour un ServletContext (c’est-à-dire pour l’ensemble de l’application Web), une requête ou une session HTTP alors elle doit fournir une implémentation d’un listener. Une méthode de ce dernier sera appelée par le conteneur à chaque fois que l’événement concerné surviendra lors de la vie de l’application.

Les différents types de listeners sont représentés dans l’API Servlet par les interfaces Java suivantes :

javax.servlet.ServletContextListener

Permet d’écouter les changements d’état du ServletContext. Le conteneur Web avertit l’application de la création du ServletContext grâce à la méthode contextInitialized et de la destruction du ServletContext grâce à la méthode contextDestroyed. Il est important de comprendre que le ServletContext représente l’application Web. Donc un ServletContextListener est un moyen de réaliser des traitements au moment du lancement de l’application Web et/ou au moment de son arrêt.

javax.servlet.ServletContextAttributeListener

Permet d’écouter les changements d’état des attributs stockés dans le ServletContext (les attributs de portée application). Le conteneur avertit l’application de l’ajout d’un attribut (appel à la méthode ServletContextAttributeListener.attributeAdded, de la suppression d’un attribut (appel à la méthode ServletContextAttributeListener.attributeRemoved et de la modification d’un attribut (appel à la méthode ServletContextAttributeListener.attributeReplaced.

javax.servlet.ServletRequestListener

Permet d’écouter l’entrée et/ou la sortie d’une requête de l’environnement de l’application Web. Le conteneur avertit l’application de l’entrée d’une requête à traiter grâce à la méthode requestInitialized et de la sortie de la requête grâce à la méthode requestDestroyed.

javax.servlet.ServletRequestAttributeListener

Permet d’écouter les changements d’état des attributs stockés dans la HttpServletRequest (les attributs de portée requête). Le conteneur avertit l’application de l’ajout d’un attribut (appel à la méthode ServletRequestAttributeListener.attributeAdded, de la suppression d’un attribut (appel à la méthode ServletRequestAttributeListener.attributeRemoved et de la modification d’un attribut (appel à la méthode ServletRequestAttributeListener.attributeReplaced.

javax.servlet.http.HttpSessionListener

Permet d’écouter la création et la suppression d’une HttpSession. Le conteneur avertit l’application de la création d’une session grâce à la méthode sessionCreated et de la suppression d’une session grâce à la méthode sessionDestroyed. La suppression d’une session signifie que soit elle a été invalidée par l’application elle-même grâce à la méthode HttpSession.invalidate soit elle est arrivée à expiration et le conteneur a décidé de l’invalider.

javax.servlet.http.HttpSessionAttributeListener

Permet d’écouter les changements d’état des attributs stockés dans la HttpSession (les attributs de portée session). Le conteneur avertit l’application de l’ajout d’un attribut (appel à la méthode HttpSessionAttributeListener.attributeAdded, de la suppression d’un attribut (appel à la méthode HttpSessionAttributeListener.attributeRemoved et de la modification d’un attribut (appel à la méthode HttpSessionAttributeListener.attributeReplaced.

Il existe également deux autres listeners Web : Le HttpSessionActivationListener et le HttpSessionBindingListener. Ils ne sont pas décrits ici car ils sont réservés à des usages plus avancés de l’API Servlet.

Déclaration des listeners Web

Une classe implémentant une ou plusieurs interfaces la désignant comme un listener Web doit également être déclarée auprès du conteneur Web. Pour cela, il suffit d’ajouter l’annotation @WebListener à la classe :

Exemple de ServletRequestListener
package dev.gayerie;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class MyServletRequestListener implements ServletRequestListener {

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        // ...
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        // ...
    }
}

Si l”on ne souhaite pas utiliser une annotation, il est également possible de déclarer un listener dans le fichier de déploiement web.xml grâce à la balise listener.

Déclaration d’un listener dans le fichier web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app
  xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
  xmlns="https://java.sun.com/xml/ns/javaee"
  xsi:schemaLocation="https://java.sun.com/xml/ns/javaee
                      https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  version="3.0">

    <listener>
        <listener-class>dev.gayerie.MyServletRequestListener</listener-class>
    </listener>

</web-app>

Exemple d’utilisation d’un listener

L’exemple (simple) ci-dessous consiste en un ServletContextListener dont le rôle est de réaliser un log applicatif signalant respectivement le lancement et l’arrêt de l’application Web :

Exemple de ServletRequestListener

package dev.gayerie;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class LoggingListener implements ServletContextListener {
  @Override
  public void contextInitialized(ServletContextEvent sce) {
    sce.getServletContext().log("## Lancement de l'application ##");
  }

  @Override
  public void contextDestroyed(ServletContextEvent sce) {
    sce.getServletContext().log("## Arrêt de l'application ##");
  }
}

Les filtres de Servlet

Il est parfois intéressant d’effectuer des opérations avant et/ou après l’invocation de la servlet. Il s’agit souvent d’opérations communes à un ensemble de requêtes d’une application Web.

Un filtre de Servlet est une classe implémentant l’interface Filter. Un filtre a son propre cycle de vie. Une fois créé, le conteneur initialise le filtre en appelant sa méthode Filter.init et il signalera la destruction du filtre en appelant sa méthode Filter.destroy. L’opération de filtrage est réalisée grâce à la méthode Filter.doFilter.

Exemple d’implémentation d’un filtre Web
package dev.gayerie;

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;

public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // ...
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                                                                   throws IOException, ServletException {
        // ...
    }

    @Override
    public void destroy() {
        // ...
    }

}

Déclaration des filtres

La déclaration d’un filtre Web auprès du conteneur se fait soit par l’annotation @WebFilter soit dans le fichier de déploiement web.xml.

Comme pour une Servlet, un filtre est associé à un ou plusieurs motifs d’URL (URL pattern) indiquant au conteneur pour quelles requêtes HTTP le filtre doit être appelé.

Déclaration d’un filtre Web par annotation
package dev.gayerie;

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.annotation.WebFilter;

@WebFilter({"/subpart/*", "/otherpart/*"})
public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // ...
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                                                            throws IOException, ServletException {
        // ...
    }

    @Override
    public void destroy() {
        // ...
    }

}

Si on ne souhaite pas utiliser une annotation, il est également possible de déclarer un listener dans le fichier de déploiement web.xml grâce aux balises filter et filter-mapping.

Déclaration d’un filtre dans le fichier web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app
  xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
  xmlns="https://java.sun.com/xml/ns/javaee"
  xsi:schemaLocation="https://java.sun.com/xml/ns/javaee
                      https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  version="3.0">

  <filter>
    <filter-name>MyFilter</filter-name>
    <filter-class>dev.gayerie.MyFilter</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>MyFilter</filter-name>
    <url-pattern>/subpart/*</url-pattern>
  </filter-mapping>

  <filter-mapping>
    <filter-name>MyFilter</filter-name>
    <url-pattern>/otherpart/*</url-pattern>
  </filter-mapping>
</web-app>

Il est également possible de déclarer qu’un filtre doit être utilisé pour des requêtes traitées par des Servlets spécifiques plutôt que d’utiliser un modèle d’URL.

Implémentation d’un filtre

L’opération de filtrage est réalisée par la méthode Filter.doFilter.

Principe général d’implémentation d’un filtre
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                                                    throws IOException, ServletException {
    // réaliser des opérations avant le traitement de la requête

    // appeler l'élément suivant dans la chaîne de filtrage
    chain.doFilter(request, response);

    // réaliser des opérations après le traitement de la requête
}

Si plusieurs filtres doivent être déclenchés pour le traitement d’une requête, alors l’appel à chain.doFilter(...) permet de passer au filtre suivant. Un fois le dernier filtre appelé, l’appel à chain.doFilter(...) passera au traitement normal de la requête (Servlet, JSP ou ressource statique).

Il est recommandé d’implémenter des filtres de manière à ce qu’ils soient indépendants les uns des autres. En effet, si plusieurs filtres sont appelés pour le traitement d’une requête, l’ordre dans lequel ces filtres seront appelés n’est pas prédictible s’ils ont été déclarés avec l’annotation @WebFilter. En revanche, ils seront appelés dans l’ordre des balises filter-mapping s’ils ont été déclarés à partir du fichier de déploiement web.xml

Note

Une implémentation de filtre peut très bien ne pas appeler chain.doFilter(...) et choisir de générer directement une réponse.

Cas d’utilisation de filtres

Deux exemples d’implémentation de filtres simples mais efficaces.

Gestion de l’UTF-8

Un cas facilement compréhensible est celui d’une application Web qui poste les données de tous ses formulaires HTML en UTF-8. Nous avons vu que par défaut, le conteneur Web utilise l’encodage ISO-8859-1 (Latin-1). Il est donc nécessaire de positionner le bon encodage grâce à la méthode ServletRequest.setCharacterEncoding(String). Cette opération répétitive est source d’oubli (et donc de bug). Il serait préférable de garantir que cette méthode soit systématiquement appelée avant chaque traitement de Servlet. Ce type de comportement peut très facilement être implémenté au moyen d’un filtre Web.

Filtre UTF-8
package dev.gayerie;

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.annotation.WebFilter;

@WebFilter("/*")
public class Utf8RequestEncodingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                                                                throws IOException, ServletException {
        request.setCharacterEncoding("UTF-8");
        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }

}

Génération de log

Il peut être intéressant de garder une trace des paramètres HTTP reçus lors des tests ou pour des statistiques. Le filtre ci-dessous écrit dans les logs du serveur le nom et la valeur de tous les paramètres reçus :

Filtre de log de paramètres
package dev.gayerie;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

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.annotation.WebFilter;

@WebFilter("/*")
public class LogFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                                                                    throws IOException, ServletException {
        request.getServletContext().log("parameters received: " + parametersToString(request));
        chain.doFilter(request, response);
    }

    private List<String> parametersToString(ServletRequest request) {
        List<String> parameters = new ArrayList<>();
        request.getParameterMap().forEach((k, v) -> parameters.add(k + "=" + Arrays.toString(v)));
        return parameters;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }

}

Prudence

L’utilisation conjointe des deux filtres ci-dessus peut poser problème. En effet, la méthode request.setCharacterEncoding(...) dans la classe Utf8RequestEncodingFilter doit être appelée avant que les paramètres de la requête ne soient accédés. Le filtre Utf8RequestEncodingFilter doit donc être placé avant le filtre LogFilter. Malheureusement, cela ne peut pas être garanti par l’utilisation de l’annotation @WebFilter.

On peut imaginer des traitements bien plus complexes grâce aux filtres : contrôle des droits d’accès (autorisation), optimisation d’image, chiffrement des données…