1. Introduction

Il existe des outils de mapping objet (comme JDO, Hibernate) qui facilitent le stockage des objets dans une base de données. Toutefois ils sont plus complexes à mettre en place, et leur apprentissage demande de déjà connaître le fonctionnement de l'API JDBC. Dans des projets de moindre amplitude, leur utilisation ne se justifie pas. Les Servlets travaillent alors directement avec la base de données. Lors de l'accès à une base de données depuis une Servlet, l'établissement de la connexion peut prendre quelques secondes. Ce délai augmente le temps de réponse de votre Servlet.
La gestion d'un pool de connexions par Tomcat permet de réduire ce temps puisque les connexions à la base de données sont déjà établies et gérées par Tomcat qui fournit à la Servlet des objets DataSource. Les pools de connexions dans Tomcat utilisent les classes du pool de connexions DBCP de Jakarta Commons. Toutefois il est possible d'utiliser n'importe quel autre pool qui implémente l'interface javax.sql.DataSource.
Il est aussi envisageable que vous décidiez de gérer vous-même votre pool de connexions comme c'est faisable pour Struts (qui utilise aussi DBCP Jakarta Commons). Toutefois cela n'est pas recommandé (et devrait disparaitre pour Struts avec la version 2), puisque le conteneur peut s'en charger et donc vous éviter de mélanger les couches applicatives.
Les sources de ce tutoriel sont téléchargeables à l'adresse suivante : http://christophej.developpez.com/fichiers/pooltomcat/TutoPool.zip

2. Contexte

Ce tutorial a été rédigé avec Tomcat 4.1.29, une base de donnée MySQL 4.0.15, le pilote JDBC mysql-connector-java-3.0.15. Sur la base de données MySQL, le tutorial utilise une base appelée "base" avec un user "user" et un password "password". La base contient une table "table1" avec deux champs. Le jar contenant le driver est placé dans $CATALINA_HOME/common/lib L'affichage est généré par une Servlet tutorial.TutoPool qui est placée dans le contexte TutoPool de Tomcat.

Je vais commencer par parler de la configuration minimale, puis j'aborderai les autres paramètres.

3. Configuration minimale

La configuration pour l'utilisation du pool de connexion se fait en deux endroits :

  • dans le fichier web.xml du contexte
  • dans le fichier server.xml du répertoire conf de Tomcat

3.1. Le fichier web.xml

Ce fichier est le descripteur de l'application web. On y déclare le nom JNDI (Java Naming and Directory Interface) sous lequel on cherchera l'objet DataSource. Par convention, ces noms suivent le sous-contexte jdbc (relatif au contexte de nommage standard java:comp/env qui est la racine de toutes les ressources fournies). Cette déclaration se fait après la section servlet-mapping.

 
Sélectionnez

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> 
<web-app> 
	<display-name>TutoPool</display-name> 
	<servlet>
		<servlet-name>TutoPool</servlet-name>
		<servlet-class>tutorial.TutoPool</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>TutoPool</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
	<resource-ref>
		<description>
			reference a la ressource BDD pour le pool
		</description>
		<res-ref-name>jdbc/TutoPool</res-ref-name>
		<res-type>javax.sql.DataSource</res-type>
		<res-auth>Container</res-auth>
	</resource-ref> 
</web-app>

3.2. Le fichier server.xml

Il s'agit ici de configurer la "resource factory" de Tomcat. Cette configuration peut aussi être faite dans le fichier META-INF\context.xml de l'archive war utilisée pour le déploiement de l'application web sur les versions 5.x de Tomcat.
J'ai repris ici toute la partie relative au contexte de l'application web TutoPool. Cette section est à placée avec les autres déclarations de contexte entre les balises <Host> du fichier server.xml.

 
Sélectionnez

<Context path="/TutoPool" 
	reloadable="true" 
	docBase="\TutoPool" >

	<Resource
		name="jdbc/TutoPool"
		auth="Container"
		type="javax.sql.DataSource"/>

	<ResourceParams name="jdbc/TutoPool">
		<parameter>
			<name>username</name>
			<value>user</value>
	 	</parameter>
		<parameter>
			<name>password</name>
			<value>password</value>
		</parameter>
	 	<parameter>
			<name>driverClassName</name>
			<value>org.gjt.mm.mysql.Driver</value>
	 	</parameter>
		<parameter>
			<name>url</name>
			<value>jdbc:mysql://localhost:3306/base</value>
		</parameter>
	</ResourceParams>		 
</Context>

On retrouve dans la section concernant le Context, la déclaration de la ressource puis les paramètres minimums nécessaires à cette ressource : le nom d'utilisateur, le mot de passe, le driver et la chaîne de connexion à la base de données.

3.3. Le code de la Servlet

Une utilisation typique des pools de connexions se fait en récupérant l'objet DataSource dans l'init de la Servlet. Les connexions sont ensuite obtenues par la méthode getConnection(). Il ne faut pas oublier de libérer ces connexions après utilisation.
Cette Servlet d'exemple se contente d'afficher à l'écran le contenu de la table table1.

 
Sélectionnez

package tutorial;

import java.io.*;
import java.sql.*;
import javax.naming.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.sql.*;

public class TutoPool extends HttpServlet {

	private DataSource ds; //la source de données

	protected void doGet(
		HttpServletRequest request,
		HttpServletResponse response)
		throws ServletException, IOException {

		response.setContentType("text/html");
		PrintWriter out = response.getWriter();

		out.println("<html><head></head>");
		out.println("<body>");
		
		Connection con=null;
		Statement s=null;
		ResultSet rs=null;

		try {
			//récupération de la Connection depuis le DataSource
			con = ds.getConnection();
			s = con.createStatement();
			rs = s.executeQuery("SELECT * FROM table1");
			while (rs.next()) {
				out.println(rs.getString(1) + "      ");
				out.println(rs.getString(2) + "<br/>");
			}
		} catch (SQLException e) {
			response.sendError(500, "Exception sur l'accès à la BDD " + e);
		}finally {
			if (rs != null)
			{
				try {
					rs.close();
				} catch (SQLException e) {}
				rs = null;
			}
			if (s != null) {
				try {
					s.close();
				} catch (SQLException e) {}
				s = null;
			}
			if (con != null) {
				try {
					con.close();
				} catch (SQLException e) {}
				con = null;
			}
		}
		out.println("</body>");
		out.println("</html>");
		out.close();
	}

	public void init() throws ServletException {
		try {
			//récupération de la source de donnée
			Context initCtx = new InitialContext();
			ds = (DataSource) initCtx.lookup("java:comp/env/jdbc/TutoPool");
		} catch (Exception e) {
			throw new UnavailableException(e.getMessage());
		}
	}
}

Le résultat est visible par l'appel dans un navigateur de http://localhost:8080/TutoPool/ (8080 étant le port qui est configuré par défaut lors de l'installation de Tomcat).

L'appel à la méthode Connection.close() ne ferme pas vraiment la connexion avec la base de données mais la libère pour qu'elle retourne dans le pool.

4. Paramétrage avancé

Il est possible d'ajouter un certain nombre de paramètres dans le fichier server.xml. Ces paramètres vont influencer sur différentes caractéristiques du pool de connexions.

4.1. Le nombre de connexions

Trois paramètres permettent d'influencer la gestion du nombre de connexions dans le pool :

  • maxActive : le nombre maximum de connexions qui peuvent être allouées depuis le pool. Sa valeur par défaut est 8, une valeur de 0 indique "sans limite". Il faut être sûr que la base de données autorisera ce nombre de connexions.
  • maxIdle : le nombre maximum de connexions inactives qui peuvent être dans le pool. Sa valeur par défaut est de 8, une valeur de 0 indique "sans limite"
  • maxWait : le temps d'attente maximum en millisecondes pour l'obtention d'une connexion. Passé ce délai, une exception est levée. Sa valeur par défaut est -1 qui correspond à un temps infini

4.2. La validité des connexions

Un certain nombre de paramètres sont dédiés aux contrôles de la validité des connexions. Le plus important est validationQuery qui est la requête de validation utilisée pour contrôler la connexion avant de la retourner au client. Si elle est définie, elle doit être de type Select et retourner au moins une ligne. Classiquement, la requête est "SELECT 1". Les autres paramètres déterminent les conditions de contrôle de cette validité (en donnant la connexion, au retour de la connexion, après un certain temps dans le pool...)

4.3. La fuite de connexions

L'utilisation de JDBC pouvant vite devenir complexe, il arrive que l'on oublie de fermer une connexion. Cette connexion ne revient donc pas dans le pool, on parle alors de "fuite du pool de connexion" (pool leaking). Ce phénomène peut aboutir à un blocage de l'application web par manque de connexions disponibles. On peut configurer Tomcat pour qu'il récupère ces connexions et laisse une trace de la pile du code qui a ouvert la connexion et ne l'a jamais fermée. Par défaut cette fonction n'est pas activée. Si vous l'activez, Tomcat effectue cette recherche quand le nombre de connexions inactives (disponible dans le pool) est inférieur à 2. Les paramètres sont :

  • removeAbandoned : par défaut à false, c'est lui qui permet l'activation de cette fonction. Si on le met à true, une connexion est considérée comme abandonnée et éligible à la récupération si elle est inactive depuis plus longtemps que le removeAbandonedTimeout.
  • removeAbandonedTimeout : C'est le temps en secondes (par défaut 300), avant qu'une connexion inactive soit considérée comme abandonnée.
  • logAbandoned : par défaut à false. Si positionné à true, Tomcat ajoute une trace de la pile de l'application qui a abandonné la connexion.

Attention la lecture d'un ResultSet n'est pas considérée comme utilisation de la connexion. Si votre traitement est trop long, le serveur peut vous reprendre la connexion et entrainer la levée d'une exception

5. Mise en application

5.1. Préparation

Pour démontrer l'efficacité du paramétrage du nombre de connexions et du système de récupération des connexions, il faut modifier le fichier server.xml en ajoutant les paramètres suivants après la chaîne de connexion :

 
Sélectionnez

	<parameter>
		<name>maxActive</name>
		<value>8</value>
	</parameter>
	<parameter>
		<name>maxIdle</name>
		<value>8</value>
	</parameter>
	<parameter>
		<name>maxWait</name>
		<value>10000</value>
	</parameter>
	<parameter>
		<name>validationQuery</name>
		<value>select 1</value>
	</parameter>
	<parameter>
		<name>removeAbandoned</name>
		<value>true</value>
	</parameter>
	<parameter>
		<name>removeAbandonedTimeout</name>
		<value>20</value>
	</parameter>
	<parameter>
		<name>logAbandoned</name>
		<value>true</value>
	</parameter>

Puis dans le code de la Servlet, le bloc finally permettant la fermeture de ResultSet, de Statement et de Connection.

5.2. Test

Lors du test de la servlet, on rafraîchit la fenêtre du navigateur 8 fois de façon rapide. La huitième fois, le serveur met 10 secondes à répondre (maxWait) et fini par renvoyer une page d'erreur :

 
Sélectionnez

Exception sur l'accès à la BDD org.apache.commons.dbcp.SQLNestedException: 
Cannot get a connection, pool exhausted, cause: Timeout waiting for idle object

Quelques secondes plus tard (environ 20 secondes correspondant au removeAbandonnedTimeout), l'application web est de nouveau disponible.
Si on regarde la fenêtre dos du lancement de Tomcat, on voit le message suivant se répéter autant de fois qu'il fallait pour récupérer les connexions :

 
Sélectionnez

DBCP object created 2004-10-14 12:23:29 by the following code was never closed:
java.lang.Exception
at org.apache.commons.dbcp.AbandonedTrace.setStackTrace(AbandonedTrace.java:202)
at org.apache.commons.dbcp.AbandonedObjectPool.borrowObject(AbandonedObjectPool.java:121)
at org.apache.commons.dbcp.PoolingDataSource.getConnection(PoolingDataSource.java:140)
at org.apache.commons.dbcp.BasicDataSource.getConnection(BasicDataSource.java:518)
at tutorial.TutoPool.doGet(TutoPool.java:26)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:740)
.....

Ce message nous indique clairement que c'est la Servlet TutoPool qui avait pris une connexion à la ligne 26 et qui ne l'avait pas rendue.

6. Compléments d'informations

6.1. Remarque sur la déclaration

La déclaration du DataSource peut aussi se faire par l'intermédiaire de la console d'administration de Tomcat.

Image non disponible

Comme c'est une GlobalNamingResource qui est ajoutée au server.xml, elle n'est pas limitée à un seul Context et il faut déclarer un lien pour chaque Context qui peut y acceder. Cette solution permet d'éviter la section <resource-ref> du fichier web.xml et simplifie l'écriture du server.xml à :

 
Sélectionnez

<Context path="/TutoPool" 
	reloadable="true" 
	docBase="\TutoPool" >

	<ResourceLink
		name="jdbc/TutoPool"
		global="jdbc/TutoPool"
		type="javax.sql.DataSource"/>
		
</Context>

Attention, cette interface ne permet pas l'utilisation de certains paramêtres comme la gestion de fuite de connexions.

6.2. En savoir plus

Un certain nombre d'autres paramètres peuvent être réglés tels que l'isolation des transactions, l'autocommit et les PreparedStatement. Pour plus de détails sur la configuration, vous pouvez consulter les pages suivantes :
http://jakarta.apache.org/commons/dbcp/configuration.html
http://jakarta.apache.org/commons/dbcp/apidocs/org/apache/commons/dbcp/BasicDataSource.html

7. Conclusion

Dans ce tutoriel, nous venons de voir la gestion d'un pool de connexions à une base de données par Tomcat. Cette fonctionnalité permet de réduire le temps de réponse de votre servlet. Elle est aussi capable de compenser d'éventuelles erreurs de programmation en empechant des fuites de pool de connexions.

Remerciement : Merci à ridan pour la relecture et Ricky81 pour les suggestions d'améliorations.