IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Tutoriel pour comprendre la précompilation des pages JSP sous Tomcat

Image non disponible

Ce tutoriel se propose de vous expliquer le mécanisme de précompilation des pages JSP sous Tomcat.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez Donner une note à l´article (5).

Article lu   fois.

Les deux auteurs

Site personnel

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Même s'il est propre au conteneur web, le sujet de la compilation des pages JSP est assez récurrent sur les projets, cela donne l'impression d'avoir une application plus lente compte tenu du fait que celle-ci intervient lors premier accès à l'application. Du point de vue du client, c'est une notion difficile à appréhender, car il désire une application immédiatement opérationnelle et rapide, sans pour autant se préoccuper des caractéristiques techniques. On cherche donc à occulter simplement ce phénomène. Pour cela, nous allons voir ce qu'est qu'une JSP, comment cela fonctionne, ainsi que les solutions disponibles pour remédier à ce problème. Ensuite, nous implémenterons une solution simple et générique qui va en plus nous permettre de comprendre comment fonctionnent les JSP ainsi que d'autres notions en Java. D'ici la fin de cet article, vous serez en mesure de régler définitivement ce problème.

Pour bien comprendre le sujet, regardons déjà les différentes solutions existantes, cela va nous permettre de comprendre un peu plus le sujet et nous donner des idées. Pour réduire le périmètre de recherche, nous utiliserons Tomcat comme conteneur web, nous allons donc regarder comment Tomcat gère la compilation.

II. La solution officielle

Pour remédier à la solution de la compilation des fichiers JSP, la fondation Apache propose une solution basée sur un script ant dans sa documentation, au chapitre JSPs et paragraphe "Web Application Compilation". Il y est expliqué qu'il est nécessaire d'exécuter une tâche ant qui est fournie dans la documentation et non dans le produit Tomcat.

Cette tâche réalise plusieurs étapes :

  • génération du code Java à partir des fichiers JSP ;
  • compilation du code Java en bytecode Java ;
  • génération du fichier "generated_web.xml" contenant la correspondance des URL avec les servlets.

Ce qui est intéressant dans ce script, c'est l'import au début du fichier catalina-tasks.xml. Celui-ci utilise les librairies fournies dans Tomcat pour compiler les JSP. Au début du script, on importe un autre script ant déjà présent dans votre Tomcat, dans le répertoire bin/ : catalina-tasks.xml. Ce script spécifie les librairies utilisées pour la compilation des fichiers JSP. Jetons un œil à la version de ce script pour Tomcat 7.0.50 pour comprendre son fonctionnement :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
<project name="catalina-tasks">
    <description>Catalina Ant Manager, JMX and JSPC Tasks</description>
    <!-- set catalina.home if it's not already set -->
    <dirname property="catalina.home.bin.dir" file="${ant.file.catalina-tasks}" />
    <property name="catalina.home" value="${catalina.home.bin.dir}/.." />
    <typedef resource="org/apache/catalina/ant/catalina.tasks">
        <classpath>
            <fileset file="${catalina.home}/bin/tomcat-juli.jar" />
            <fileset file="${catalina.home}/lib/tomcat-api.jar" />
            <fileset file="${catalina.home}/lib/tomcat-util.jar" />
            <fileset file="${catalina.home}/lib/jasper.jar" />
            <fileset file="${catalina.home}/lib/jasper-el.jar" />
            <fileset file="${catalina.home}/lib/el-api.jar" />
            <fileset file="${catalina.home}/lib/JSP-api.jar" />
            <fileset file="${catalina.home}/lib/servlet-api.jar" />
            <fileset file="${catalina.home}/lib/catalina-ant.jar" />
            <fileset file="${catalina.home}/lib/tomcat-coyote.jar" />
        </classpath>
    </typedef>
    <typedef resource="org/apache/catalina/ant/jmx/jmxaccessor.tasks">
        <classpath>
            <fileset file="${catalina.home}/lib/catalina-ant.jar" />
        </classpath>
    </typedef>
</project>

Ce script importe des tâches ant contenues dans deux fichiers, catalina.tasks et jmxaccessor.tasks qui se trouvent eux-mêmes dans le fichier catalina-ant.jar.

Pour la compilation des JSP, c'est la première définition qui nous intéresse, c'est-à-dire org/apache/catalina/ant/catalina.tasks. Elle contient les implémentations utilisées pour chaque définition ant et celle qui nous intéresse est jasper=org.apache.jasper.JspC. Si vous voulez connaitre en détail ce que fait la tâche ant jasper, vous pouvez regarder cette classe.

On a donc trois paramètres à définir pour cette tâche :

  • uriroot permet de définir le chemin d'accès aux fichiers JSP ;
  • outputDir permet de définir le répertoire contenant les fichiers Java qui seront générés ;
  • webXmlFragment permet de définir le fichier generated_web.xml généré contenant les déclarations des servlets.

Voici un exemple de fichier generated_web.xml généré :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
<!-- Automatically created by Apache Tomcat JspC. Place this fragment in 
    the web.xml before all icon, display-name, description, distributable, and 
    context-param elements. -->

<servlet>
    <servlet-name>org.apache.jsp.error_JSP</servlet-name>
    <servlet-class>org.apache.jsp.error_JSP</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>org.apache.jsp.error_JSP</servlet-name>
    <url-pattern>/error.jsp</url-pattern>
</servlet-mapping>

Ensuite, à partir de cela, Java compile les fichiers générés (définis dans la tâche compilée).

Par exemple :

Reprenons le contenu du fichier generated_web.xml. On peut constater que notre fichier error.jsp correspond pour l'application à la classe java org.apache.jsp.error_JSP.class. Cela signifie qu'à l'exécution, l'application n'utilise pas le fichier JSP mais directement la classe java issue du fichier JSP. On pourrait tout à fait livrer une application uniquement avec les classes Java et sans les JSP.

Ce script utilise le même moteur de Tomcat, mais pendant la phase de compilation avant exécution de l'application. Si nous n'utilisons pas ce script, le même traitement est exécuté au premier appel de chaque page.

Vous pouvez faire le test :

  • lancez l'application ;
  • allez dans le répertoire work de Tomcat, il est vide ;
  • accédez avec votre navigateur web sur la page error.jsp ;
  • retournez dans le répertoire work et recherchez le fichier org.apache.jsp.error_JSP.java et org.apache.jsp.error_JSP.class.

Avec le script ant, le traitement est identique, mais vous avez constaté en effectuant ce test que la page ne s'affiche pas tout de suite, car Tomcat a effectué tout un traitement avant le rendu de la page :

  • vérification que l'instance n'existe pas ;
  • génération du fichier Java ;
  • génération du fichier Classe ;
  • création de l'instance ;
  • ajout dans le pool de JSP (JSPServlet).

Donc, pour éliminer ce phénomène, il suffirait de lancer le script avant le démarrage de Tomcat.

En fait, ce n'est pas tout à fait le cas : nous avons juste créé les fichiers ".class" et la généré le fichier web.xml. Cependant, nous ne les avons pas intégrés dans notre application, donc il y a encore des étapes à réaliser avant que cela soit opérationnel.

Maintenant, avec ces informations, prenons un cas concret :

  • nous avons une webapp représentée par un simple fichier war ;
  • nous avons le produit Tomcat et on y insère, dans le répertoire webapp, notre fichier war ;
  • nous lançons l'application avec le script startup.sh.

L'idéal serait d'utiliser le script en n'effectuant aucune autre opération supplémentaire pour garder cette procédure.

La seule action possible est le lancement du traitement de compilation au début du script startup.sh (par exemple). Pour cela, nous devons effectuer ces traitements :

  • décompresser tous les fichiers war dans le répertoire webapp ;
  • appeler la tâche ant de compilation ;
  • récupérer le contenu du fichier generated_web.xml et le rajouter dans le fichier web.xml de l'application ;
  • et pour finir lancer le traitement d'origine du fichier startup.sh.

Contrairement à ce qu'on pouvait penser au début, il ne suffit pas de lancer le script ant pour réaliser quelque chose de simple et fonctionnel. Après avoir ajouté tous les traitements, nous avons quelque chose d'opérationnel... enfin pas forcément...

Si notre webapp utilise la nouvelle fonctionnalité apportée par la spécification Servlet 3, qui consiste à mettre les fichiers JSP dans les fichiers jar, notre solution ne fonctionnera pas.

Petite explication :

Une application web est représentée par une archive war. Cette archive a forcément un fichier web.xml, qui décrit le fonctionnement de l'application web, un répertoire WEB-INF/classes et surtout un répertoire WEB-INF/lib, qui peut contenir des fichiers jar.

Les fichiers statiques peuvent se trouver n'importe où dans le fichier war, mais pas directement dans les fichiers jar. C'est pour cela que, sur de nombreux projets, on fusionne plusieurs webapps (maven overlay).

Depuis Servlet 3, il est possible de placer ces fichiers dans un jar du moment que ces fichiers se trouvent dans le répertoire META-INF/resources du jar.

Une fonctionnalité fort pratique quand on a une application modulable, mais qui ne nous facilite pas la tâche pour la compilation avec la méthode officielle.

Cela va rajouter des étapes supplémentaires : nous devons récupérer le contenu du répertoire META-INF/resources de chaque jar de l'application et compiler les fichiers JSP trouvés...

En regardant la solution officielle, cela nous a permis de comprendre le fonctionnement des JSP, ses possibilités et ses inconvénients. Regardons une autre solution possible qui a le même principe, mais pendant la phase de packaging de l'application.

La solution à la compilation :

Cette solution n'a pas la même approche que la précédente.

Le plug-in maven-JSPc-plugin me plait beaucoup, car, comme tout outil maven, il offre la possibilité d'être piloté facilement.

Il permet de réaliser la même tâche que le script ant que nous avons expliqué, à la seule différence qu'il est utilisé à la construction de l'application.

L'idée est séduisante, car la mise en place du plug-in est simplifiée par l'utilisation de maven.

À la compilation des JSP, l'outil a besoin des dépendances utilisées dans la page JSP, par exemple : les scriptlet, les taglibs, etc. qui sont en général déclarées avec le scope “runtime”.

Malheureusement, le plug-in maven n'exploite pas les dépendances déclarées en runtime et l'exécution du plug-in échoue.

À part mettre toutes les dépendances avec le scope par défaut, ce qui n'est pas pratique, ce plug-in n'est pas utilisable.

De plus, ce plug-in utilise une version spécifique de Jasper. Même si l'implémentation de ce dernier ne bouge pas souvent, cela comporte un risque. Par exemple, si un jour, il y a une migration de version de Tomcat, nous serons peut-être dans l'obligation de recompiler la webapp...

Nous venons de lister deux solutions existantes qui sont assez différentes.

  • Une solution officielle qui fonctionne, mais qui contraint à fournir du travail pour la mettre en place.
  • Une solution très simple d'utilisation, mais qui ne fonctionne pas dans tous les cas.

Ces deux solutions utilisent toujours le moteur de compilation de Tomcat Jasper.

Essayons, grâce aux connaissances que l'on vient d'acquérir, de proposer une solution compatible pour toutes les versions de Tomcat et qui permette à l'utilisateur de réaliser le minimum d'actions possibles.

III. Une solution alternative

Avant de coder, essayons de poser chaque élément en fonction de ce que nous venons de voir et de trouver un début de réponse :

Quels sont les besoins ?

  • Ne plus avoir le temps de latence sur la première invocation des pages de notre application.
  • Savoir si on n'a pas de problème de syntaxe dans nos fichiers JSP.
  • Faciliter la mise en place de cette fonctionnalité, quelle que soit l'application web.

La solution est de pré-compiler les pages avant la libération du contexte par le conteneur web. En regardant la spécification Servlet (Chapitre 14), nous apprenons que le point d'entrée d'une application web est le fichier web.xml. Ce fichier permet de déclarer des servlets, des filters et des listeners. Les listeners permettent d'écouter certains événements, il en existe de différentes sortes. Par exemple, le listener ServletContextListener est déclenché au démarrage et à l'arrêt de l'application web.

Donc notre point de départ est :

  • implémenter un listener Web ;
  • celui-ci va récupérer tous les fichiers JSP de l'application ;
  • compiler les fichiers JSP.

Pour mieux comprendre le fonctionnement, regardons comment la compilation est exécutée par défaut avec Tomcat.

En cherchant un peu dans le produit Tomcat, on tombe sur le fichier web.xml et on découvre un passage intéressant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
<servlet>
    <servlet-name>JSP</servlet-name>
    <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
    <init-param>
        <param-name>fork</param-name>
        <param-value>false</param-value>
    </init-param>
    <init-param>
        <param-name>xpoweredBy</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>3</load-on-startup>
</servlet>

<!-- The mappings for the JSP servlet -->
<servlet-mapping>
    <servlet-name>JSP</servlet-name>
    <url-pattern>*.jsp</url-pattern>
    <url-pattern>*.jspx</url-pattern>
</servlet-mapping>

Chaque URL qui se termine par .jsp ou .jspx exécute en réalité la Servlet JSPServlet.

En regardant le code de cette Servlet, on découvre le fonctionnement du rendu d'une page. Prenons la page test.jsp, et effectuons deux tests. Le premier test à la première invocation, le second test à la seconde invocation. La page est accessible avec l'URL : http:\\localhost:8080\compile\WEB-INF\JSP\test.jsp.

À la première invocation :

  1. L'utilisateur accède avec son navigateur à l'URL : http:\\localhost:8080\compile\WEB-INF\JSP\test.jsp ;;
  2. Tomcat intercepte l'appel et exécute la servlet JSPServlet car l'URL se termine par ".jsp" ;
  3. JSPServlet regarde dans son pool si une instance représentant test.jsp est créée ;
  4. JSPServlet génère le fichier Java et le compile ensuite, car aucune instance n'existe dans son pool. Le fichier généré est lui-même une servlet ;
  5. JSPServlet instancie la nouvelle servlet qu'il vient de compiler et la place dans son pool ;
  6. JSPServlet exécute la nouvelle servlet et envoie le rendu au navigateur.

À la deuxième invocation :

  1. L'utilisateur accède avec son navigateur à l'URL : http:\\localhost:8080\compile\WEB-INF\JSP\test.jsp ;
  2. Tomcat intercepte l'appel et exécute la servlet JSPServlet car l'URL se termine par ".jsp" ;
  3. JSPServlet regarde dans son pool si une instance représentant test.jsp est créée;
  4. JSPServlet récupère l'instance de la servlet test.jsp et l'exécute pour rendu au navigateur.

La phase de compilation de la JSP se trouve dans la classe JspServletWrapper. Cette classe invoque ensuite l'implémentation que nous avons vue au préalable dans le script ant. Cette classe possède une méthode service avec un commentaire décrivant le traitement pour chaque étape :

  1. compile ;
  2. (Re)load servlet class file ;
  3. handle limitation of number of loaded Jsps ;
  4. service request.

Ici, nous n'avons besoin que des deux premières étapes, puisque nous voulons juste compiler et charger la servlet. Et en regardant de plus près le code, nous trouvons ce commentaire :

 
Sélectionnez
1.
2.
3.
4.
// If a page is to be precompiled only, return.
    if (precompile) {
      return;
    }

Cela signifie qu'il y a un paramètre qui permet juste de compiler les pages.

On continue notre recherche pour retrouver à quoi correspond ce booléen et on trouve la réponse dans la classe JspServlet de la méthode "preCompile". En lisant le code, on comprend rapidement à quoi cela sert et il y a même un commentaire très intéressant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
/**
* <p>Look for a <em>precompilation request</em> as described in
* Section 8.4.2 of the JSP 1.2 Specification. <strong>WARNING</strong> -
* we cannot use <code>request.getParameter()</code> for this, because
* that will trigger parsing all of the request parameters, and not give
* a servlet the opportunity to call
* <code>request.setCharacterEncoding()</code> first.</p>
*
* @param request The servlet request we are processing
*
* @exception ServletException if an invalid parameter value for the
* <code>jsp_precompile</code> parameter name is specified
*/

On nous indique le numéro du chapitre de la spécification expliquant le fonctionnement.

En résumé :

L'accès d'une page avec le paramètre jsp_precompile, par exemple, http:\\localhost:8080\compile\WEB-INF\JSP\test.jsp?jsp_precompile, permet de lancer le traitement de compilation en n'effectuant que les deux premières étapes. Si on reprend notre exemple de tout à l'heure, cela donne :

  1. L'utilisateur accède avec son navigateur à l'URL : http:\\localhost:8080\compile\WEB-INF\JSP\test.jsp?jsp_precompile ;
  2. Tomcat intercepte l'appel et exécute la servlet JSPServlet car l'URL se termine par ".jsp" ;
  3. JSPServlet regarde dans son pool si une instance représentant test.jsp est créée ;
  4. JSPServlet génère le fichier Java et le compile ensuite, car aucune instance n'existe dans son pool. Le fichier généré est lui-même une servlet ;
  5. JSPServlet instancie la nouvelle servlet qu'il vient de compiler et la place dans son pool.

Donc, en conclusion de cette partie, nous devrons implémenter un listener pour récupérer le chemin d'accès à chaque "JSP" et "JSPx" et invoquer chaque fichier en ajoutant le paramètre jsp_precompile.

Nous devrons donc réaliser un listener qui récupère les chemins d'accès de chaque fichier "JSP" de l'application en cours de démarrage et les compiler.

III-A. Récupérer toutes les JSP

Pour pouvoir compiler les fichiers, il est nécessaire dans un premier temps de les trouver. Reprenons notre listener web, l'interface ServletContextListener comporte une méthode qui nous intéresse, contextInitialized possédant une instance de ServletContextEvent.

Nous devons réaliser notre solution en partant de cela. En regardant la Javadoc de ServletContextEvent, nous avons la méthode getResourcesPaths de ServletContext qui permet de récupérer la liste des ressources à partir d'un chemin de l'application web. La racine d'une application web commence par "/" et une ressource qui se termine par "/" signifie que ce n'est pas un fichier, mais un autre répertoire. Nous pouvons réaliser facilement notre algorithme :

  • se placer à la racine ;
  • rechercher à partir de ce point ;
  • compilation de chaque fichier se terminant par "JSP" et "JSPx" ;
  • pour chaque accès terminant par "/", on reprend à l'étape 2.

Nous devons donc réaliser une méthode qui sera appelée récursivement :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
static Set<String> findFilesInDirectory(final ServletContext servletContext, final String dirPath, final String... fileExtensions) {
    final Set<String> files = new HashSet<String>();

    for (final String path : servletContext.getResourcePaths(dirPath)) {

      if (path.endsWith("/")) {
        final Set<String> findFilesInDirectory = findFilesInDirectory(servletContext, path, fileExtensions);
        files.addAll(findFilesInDirectory);
      } else {
        for (final String extension : fileExtensions) {
          if (path.endsWith("." + extension)) {
            files.add(path);
          }
        }
      }
    }
    return files;
  }

Cette méthode retourne une liste comportant le chemin d'accès aux fichiers. On spécifie à cette méthode le type d'extension recherché et la zone de recherche.

Dans notre cas :

 
Sélectionnez
1.
final Set<String> JSPs = JspCompileHelper.findFilesInDirectory(servletContext, "/", "JSP", "JSPx");

III-B. Invoquer chaque ressource

Maintenant que nous avons le chemin d'accès à tous les fichiers, nous devons les compiler. En regardant la Javadoc de ServletContextEvent, nous découvrons la méthode getRequestDispatcher qui prend un paramètre qui commence par "/". C'est exactement ce type de chemin que nous avons récupéré, il suffira d'itérer sur le "Set".

Par exemple :

Nous accédons à notre page avec un navigateur via l'URL http:\\localhost:8080\compile\WEB-INF\JSP\test.jsp , par contre cela correspond à /WEB-INF/JSP/test.jsp avec la méthode utilisée. Cela va nous faciliter le travail :

  • pas besoin de récupérer le context path de l'application ;
  • pas besoin de récupérer le port d'écoute HTTP, ni le host.

La méthode getRequestDispatcher retourne un RequestDispatcher qui propose deux méthodes : forward et include. Nous allons utiliser la méthode include. Pour l'instant, le développement de notre solution ne pose aucun souci, par contre la méthode include fonctionne avec deux paramètres :

  • une instance de ServletRequest en réalité de HttpServletRequest ;
  • une instance de ServletResponse en réalité de HttpServletResponse.

Malheureusement, il n'y a aucune solution pour récupérer ou créer ces instances... Nous devons créer notre propre implémentation.

III-C. Implémentation de HttpServletRequest et HttpServletResponse

Nous pouvons générer une implémentation de ces interfaces rapidement avec notre IDE, il ne nous reste plus qu'à coder les méthodes vides. C'est une solution rapide et simple, mais malheureusement cela pose plusieurs problèmes :

  • nous n'avons pas forcément besoin de toutes les méthodes décrites dans les interfaces ;
  • notre implémentation sera propre à la version de l'interface utilisée à la compilation.

L'idéal serait d'avoir une implémentation dynamique créée pendant l'exécution de l'application, cela permettra de ne plus être lié spécifiquement à une version... En Java, cela est possible avec les proxys fournis par l'API standard.

III-C-1. Implémentation dynamique

Un proxy est une implémentation d'une ou de plusieurs interfaces créées à l'exécution. Chaque méthode des interfaces sera redirigée vers une méthode unique, qui fournira des instances permettant de savoir quelle méthode a été appelée avec quels paramètres.

Cette méthode unique est définie par l'interface InvocationHandler avec la méthode invoke. Il suffira d'implémenter la méthode invoke pour implémenter toutes les méthodes du proxy.

III-C-2. L'interface InvocationHandler

Prenons l'interface HttpServletRequest et la méthode getQueryString. Nous invoquons cette méthode de notre proxy. En réalité, l'appel est redirigé vers la méthode invoke, qui a toujours trois paramètres :

  • proxy : instance de la classe générée dynamiquement ;
  • method : instance qui contient toutes les infos de la méthode appelante ;
  • args : tableau d'objets qui contient tous les paramètres de la méthode appelante.

Dans notre cas :

  • method fournit le nom de la méthode getQueryString, les paramètres de cette méthode, le type de retour, etc., etc., etc. ;
  • args fournit null, car dans notre cas, getQueryString ne possède aucun paramètre en argument.

Voici l'implémentation du Proxy pour "HttpServletRequest"

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
static HttpServletRequest createHttpServletRequest() {
    final InvocationHandler handler = new InvocationHandler() {

      public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
        final String methodName = method.getName();

        if (methodName.equals("getQueryString")) {
          return "jsp_precompile";
        }
        if (methodName.equals("isAsyncSupported")) {
          return false;
        }
        if (methodName.equals("isAsyncStarted")) {
          return false;
        }

        return null;
      }
    };

    return (HttpServletRequest) Proxy.newProxyInstance(JspCompileHelper.class.getClassLoader(),
        new Class<?>[]{HttpServletRequest.class}, handler);
  }

Notre besoin est très simple, nous récupérons le nom de la méthode avec : final String methodName = method.getName();. Ainsi il est facile de retourner l'objet approprié en fonction de la méthode d'origine appelée :

  • getQueryString : retourne "jsp_precompile" ;
  • isAsyncSupported : retourne false ;
  • isAsyncStarted : retourn false ;
  • toutes les autres méthodes : retournent null.

Ensuite, la dernière ligne permet d'instancier le proxy :

 
Sélectionnez
1.
2.
(HttpServletRequest) Proxy.newProxyInstance(JspCompileHelper.class.getClassLoader(),
 new Class<?>[]{HttpServletRequest.class}, handler);

La création du proxy s'effectue avec la méthode statique Proxy.newProxyInstance. Ensuite, cette méthode requiert certains paramètres obligatoires :

  • ClassLoader loader : permet de savoir dans quel classloader sera chargé le proxy.
  • Class<?>[] interfaces : permet de définir toutes les interfaces utilisées par le proxy.
  • InvocationHandler h : instance de l'implémentation de invoke. C'est en réalité ce code qui sera exécuté à chaque appel.

Si nous n'avions pas utilisé cela, l'implémentation ressemblerait à cela avec tous les inconvénients de l'évolution de l'interface :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
public class TestHttpServletRequest implements HttpServletRequest {

 @Override
 public String getQueryString() {
 return "jsp_precompile";
 }
 
 @Override
 public boolean isAsyncStarted() {
 return false;
 }

 @Override
 public boolean isAsyncSupported() {
 return false;
 }
 
 @Override
 public Object getAttribute(String name) {
 return null;
 }

 @Override
 public Enumeration<String> getAttributeNames() {
 return null;

etc.

Cette solution fonctionnera, mais le code sera très long pour pas grand chose, et surtout pas modulable en fonction de l'API... Si vous avez remarqué dans l'exemple, la méthode getQueryString retourne jsp_precompile qui est le paramètre décrit dans la spécification pour compiler les JSP sans le rendu. Avec cela, nous ne serons pas obligés d'ajouter le paramètre pour chaque JSP, notre implémentation fournira automatiquement ce paramètre. Cela simulera le fait que l'URL possède ce paramètre même si ce n'est pas le cas :)

En ce qui concerne les deux autres méthodes rajoutées, cela est nécessaire depuis Servlet 3, cela permet de définir que la requête est synchrone. Il est nécessaire de les définir, car la méthode include de RequestDispatcher utilise ces méthodes et il est impossible de retourner null dans ces cas.

Il se peut qu'avec les futures versions de Tomcat, leur API change légèrement, nous aurons alors juste à ajouter les nouvelles conditions dans la méthode invoke pour gérer ces futurs changements, tout en restant compatibles avec les anciennes versions, vu que ces nouvelles conditions seront ignorées avec les anciennes versions.

III-D. Implémentation du Listener

La problématique de la requête et de la réponse a été résolue, il est désormais simple de réaliser le reste de l'implémentation. Sachant que nous avons découpé le travail en plusieurs parties, il suffit de les réunir dans le listener :

  • récupération du chemin d'accès aux fichiers JSP et JSPx ;
  • création d'une instance de HttpServletRequest et HttpServletResponse ;
  • appel de chaque fichier avec l'instance de request et response.
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
  public void contextInitialized(final ServletContextEvent servletContextEvent) {

    final ServletContext servletContext = servletContextEvent.getServletContext();

    // Récupere tous les fichiers JSP et JSPx présent dans l'application.
    final Set<String> JSPs = JspCompileHelper.findFilesInDirectory(servletContext, "/", "JSP", "JSPx");

    // Création d'un requete et d'une réponse pour la compilation
    final HttpServletRequest request = JspCompileHelper.createHttpServletRequest();
    final HttpServletResponse response = JspCompileHelper.createHttpServletResponse();

    for (final String JSP : JSPs) {
      final RequestDispatcher requestDispatcher = servletContext.getRequestDispatcher(JSP);

      try {
        servletContext.log("Compiling : " + servletContext.getContextPath() + JSP);
        requestDispatcher.include(request, response);
      } catch (final Exception e) {
        // Exception est déjà tracée par le logger de tomcat.
        // Tomcat 7.0.50 - ApplicationDispatcher.invoke Line 772
      }
    }
  }

Voilà notre implémentation de notre listener, finalement cela a été plutôt simple. Pour l'utiliser, il suffit de déclarer ce listener dans votre fichier "web.xml"... et même cela, on peut le simplifier depuis Servlet 3.

III-E. Web Fragments

Le descripteur de déploiement (web.xml) permet de configurer l'application. C'est un fichier qui se trouve dans le répertoire "WEB-INF" de votre application. Depuis Servlet 3, il est possible de le découper en plusieurs fichiers, ces fichiers supplémentaires se nomment web-fragment.xml et se trouvent dans le répertoire META-INF de chaque jar si cela est nécessaire :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
<web-fragment xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd"
    version="3.0">

    <listener>
        <display-name>Jsp Compile Listener</display-name>
        <listener-class>fr.ms.tomcat.listener.jsp.JspCompileListener</listener-class>
    </listener>

</web-fragment>

Avec ce procédé, il est encore plus aisé de paramétrer l'application. Au lieu de modifier le descripteur de l'application pour prendre en compte la compilation, il suffit d'ajouter le jar, au démarrage il sera pris en compte automatiquement.

III-F. On peut faire mieux

Nous avons enfin notre composant totalement prêt, il permet de compiler toutes les JSP pendant le démarrage de l'application, mais il est possible d'optimiser ce traitement. J'ai utilisé ce composant sur une application Java sur un Raspberry et contrairement à un serveur de production, chaque compilation de JSP peut durer plusieurs secondes.

Avec une application contenant une centaine de fichiers, j'ai pratiquement multiplié le démarrage du Tomcat par deux sur un Raspberry. Chaque listener défini dans l'application s'initialise et passe la main au listener suivant. Notre listener compile les JSP pendant cette phase d'initialisation donc pendant ce temps, l'application n'est pas encore disponible.

Il serait peut-être judicieux de mettre cette compilation en tâche de fond.

III-G. Compilation en tâche de fond

Actuellement, notre listener est lancé dans le thread principal. Quand son traitement se termine, le thread poursuit son exécution avec le listener suivant et ainsi de suite. Il serait judicieux de laisser ce thread récupérer les chemins d'accès aux fichiers et de déléguer ensuite la compilation à un autre thread. Cela permettra au thread principal de continuer ses tâches.

Depuis Java 5, une implémentation de pool de threads est fournie. Celle-ci est paramétrable facilement en fonction du besoin. Pour nous simplifier la tâche, Java 5 fournit une classe permettant de créer rapidement un pool de threads avec une configuration par défaut.

La classe Executors nous fournit plusieurs méthodes statiques qui retournent une implémentation de pool de threads en fonction du besoin. Nous voulons dédier le traitement de compilation à ce pool de threads. Sachant que nous ne voulons pas surcharger ce traitement pendant le démarrage du Tomcat, nous pouvons utiliser un pool monothread. La classe Executors offre la méthode newSingleThreadExecutor qui répond parfaitement à notre besoin.

La plupart des méthodes de la classe Executors retournent toutes une instance de Executor. Cela met à disposition une méthode execute qui reçoit une instance de Runnable. Il suffit d'implémenter la méthode run pour que cela soit exécuté par Executor... Plutôt simple comme fonctionnement. Après l'explication, la pratique.

Déplaçons juste la compilation que nous avons mise dans notre listener dans une implémentation de Runnable :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
class JspCompileRunnable implements Runnable {

  private final ServletContext servletContext;
  private final String path;

  private final HttpServletRequest request;
  private final HttpServletResponse response;

  /**
   * Créer une instance de {@link Runnable} permettant de réaliser un include sur un ressource.
   * 
   * @param servletContext La {@link ServletContext} à utiliser pour la compilation.
   * @param path Le chemin d'accès de la ressource.
   * @param request la {@link HttpServletRequest requete} à utiliser pour la compilation.
   * @param response la {@link HttpServletRequest réponse} à utiliser pour la compilation.
   */
  JspCompileRunnable(final ServletContext servletContext, final String path, final HttpServletRequest request,
      final HttpServletResponse response) {
    this.servletContext = servletContext;
    this.path = path;
    this.request = request;
    this.response = response;
  }

  public void run() {

    final RequestDispatcher requestDispatcher = servletContext.getRequestDispatcher(path);

    if (requestDispatcher == null) {
      return;
    }

    try {
      servletContext.log("Compiling : " + servletContext.getContextPath() + path);
      requestDispatcher.include(request, response);
    } catch (final Exception e) {
      // Exception est déjà tracée par le logger de tomcat.
      // Tomcat 7.0.50 - ApplicationDispatcher.invoke Line 772
    }
  }
}

Chaque instance de notre Runnable correspond à un seul fichier JSP. Elles seront toutes poussées dans le pool de threads, et celui-ci va exécuter chaque instance. Sachant que nous désirons monopoliser le minimum de ressource, il ne comportera qu'un seul thread, donc chaque JSP sera compilée à tour de rôle.

Désormais dans notre listener, nous retirons la compilation et nous la remplaçons par la création du pool de threads et la création de chaque Runnable pour chaque JSP :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
  public void contextInitialized(final ServletContextEvent servletContextEvent) {

    final ServletContext servletContext = servletContextEvent.getServletContext();

    // Récupere tous les fichiers JSP et JSPx présent dans l'application.
    final Set<String> JSPs = JspCompileHelper.findFilesInDirectory(servletContext, "/", "JSP", "JSPx");

    // Création un executor avec un seul thread daemon et la priorité la plus basse.
    final ThreadFactory threadFactory = new JspCompileThreadFactory(servletContext.getContextPath());
    final Executor executorService = Executors.newSingleThreadExecutor(threadFactory);

    // Création d'une requête et d'une réponse pour la compilation
    final HttpServletRequest request = JspCompileHelper.createHttpServletRequest();
    final HttpServletResponse response = JspCompileHelper.createHttpServletResponse();

    for (final String JSP : JSPs) {
      final Runnable task = new JspCompileRunnable(servletContext, JSP, request, response);
      executorService.execute(task);
    }
  }

D'après la spécification JSP, la compilation des pages JSP est thread safe. Si l'utilisateur accède à une page et si celle-ci est en train d'être compilée, Tomcat gérera automatiquement cet aspect.

Cette implémentation permet d'accéder tout de suite à l'application même si la compilation n'est pas terminée.

Par contre et pour rendre la chose encore plus confortable et compréhensible, on peut renommer le thread avec par exemple le nom du context en cours de déploiement et surtout définir ce thread qui est en tâche de fond comme un thread "daemon".

III-H. Thread Factory

Un Thread daemon est un thread qui n'a pas besoin d'être terminé si l'application s'arrête. Un thread peut être exécuté en tâche de fond. Ainsi, si l'application se ferme, tous les threads non daemon terminent leur traitement en cours et les threads daemon sont purement et simplement supprimés.

C'est exactement ce que l'on désire : si le programme s'arrête alors qu'il est en phase de compilation d'une page, il n'est pas nécessaire d'attendre que la compilation se termine. On peut aussi définir une priorité pour chaque thread, il y a plusieurs niveaux, mais les plus utilisées sont :

Cela nous permet de définir la priorité la plus basse pour notre tâche de compilation, ainsi le temps de démarrage de notre application ne sera pas affecté. Pour ce faire, il est nécessaire de définir une fabrique de Thread et pour nous aider, utilisons celle par défaut fournie par l'API standard :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
class JspCompileThreadFactory implements ThreadFactory {

  private final ThreadFactory threadFactory = Executors.defaultThreadFactory();

  private final String name;

  JspCompileThreadFactory(final String name) {
    this.name = name;
  }

  public Thread newThread(final Runnable r) {
    final Thread newThread = threadFactory.newThread(r);

    if (name != null) {
      newThread.setName("JSPCompile " + name);
    }

    newThread.setDaemon(true);
    newThread.setPriority(Thread.MIN_PRIORITY);

    return newThread;
  }
}

Rajoutons cela à notre pool de threads :

 
Sélectionnez
1.
2.
final ThreadFactory threadFactory = new JspCompileThreadFactory(servletContext.getContextPath());
Executor executorService = Executors.newSingleThreadExecutor(threadFactory);

Voilà, nous avons quelque chose d'opérationnel, que ce soit l'utilisation sur un serveur de production, ou simplement un Raspberry. Il suffit d'avoir un conteneur Web Tomcat et de rajouter ce jar à notre webapp : simple, non ?

Bien sûr, cet article aurait pu être bien plus long, on aurait pu rajouter encore :

  • La gestion sur d'autres conteneur web.
  • Utiliser les properties listener pour rajouter le traitement en tâche de fond ou synchrone.
  • Utiliser Callable au lieu de Runnable pour afficher le taux de réussite de la compilation.

Je vous laisse le soin d'apporter votre petite touche. Pour éviter les copier-coller, l'intégralité du code est disponible sous github.

Pour les personnes désirant juste utiliser la fonctionnalité, le code compilé est disponible directement sur le repository central Maven :

 
Sélectionnez
1.
2.
3.
4.
5.
<dependency>
    <groupId>com.github.marcosemiao.tomcat.listener</groupId>
    <artifactId>JSP-compile-listener</artifactId>
    <version>1.0.0</version>
</dependency>

Désormais, avec ce listener, nous ne serons plus jamais embêtés par la compilation des JSP. De plus, le plus intéressant, c'est que nous avons découvert de nouvelles notions :

  • ce qu'est un script ant ;
  • comment fonctionne Jasper ;
  • comment fonctionne une webapp ainsi que les web fragment ;
  • l'implémentation dynamique.

Malheureusement, cette implémentation ne fonctionne plus à partir de Tomcat 8, car depuis cette version, il est impossible de récupérer un RequestDispatcher dans un listener, car le context n'est pas encore complètement initialisé et cela provoque un NullPointerException. Il n'est plus possible de compiler pendant le démarrage de l'application, nous sommes désormais obligés d'attendre la fin du démarrage du Tomcat.

Je vous laisse chercher une solution au lieu de la décrire dans cet article, sinon il sera vraiment trop long. Pour les impatients, la solution sera disponible bientôt sur github.

IV. Conclusion et remerciement

Cet article a été publié avec l'aimable autorisation de SOAT, société d'expertise et de conseil en informatique.

Nous tenons à remercier ced pour la relecture orthographique et Mickaël Baron pour la mise au gabarit.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Copyright © 2015 Soat. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.