Pourquoi utiliser JNDI ?

Très souvent, les paramètres informant sur l'environnement d’exécution sont présents dans des fichiers properties, agrégés dans les archives WAR ou EAR ou installés dans des répertoires spécifiques du serveur d’application. Cette approche est gênante pour plusieurs raisons. Nous allons voir pourquoi et comment utiliser les serveurs JNDI pour améliorer les procédures d’installations, de mise à jour, de déploiement, de partages de paramètres entre nœud d’un clusters, etc. dans le respect des normes J2EE.
Par Philippe PRADOS - 2006
www.prados.fr
Parmi les différentes familles de paramètres : statique, framework, JVM, déploiement et applicatif, certains permettent à l’application d’avoir connaissance de son contexte d’exécution. Par exemple, l’application peut connaître les drivers et la localisation de la base donnée, d’un serveur JMS ou JavaMail, d’EJB, de services Web, la localisation physique de certains fichiers, etc. Toutes ces informations sont indispensables à l’application et dépendent du contexte d’exécution. Ces paramètres sont à valoriser lors de l’installation de celle-ci.
Récupérer les paramètres de déploiement
Généralement, on retrouve ces paramètres dans un fichier.properties. L’application récupère celui-ci et pioche les paramètres dont elle a besoin.
package fr.prados;
class MaClass
{
MaClass()
{
Properties prop=new Properties();
prop.load(getClass().getResourceAsStream("params.properties"));
String p=prop.getProperty("jms.serveur");
...
}
}
Notez que l’extension .properties viens du fait que les développeurs utilisent généralement, à tort, la classeRessourceBundle pour récupérer ce fichier. Cette classe sert à l’internationalisation ! Elle permet d’avoir plusieurs fichiers de paramètres, un par langue. Je ne pense pas que la localisation physique d’un serveur dépende de la langue de l’utilisateur ;-) L’impact négatif au niveau des performances n’est pas négligeable. En fait, toutes les extensions du fichier sont possibles en utilisant le code proposé. Il est même préférable d’en utiliser une autre, pour ne pas mélanger les fichiers d'internationalisation des fichiers de paramétrages. Le suffixe.conf serait le bienvenu. Le suffixe.properties étant réservé à l’internationalisation de l’application. Par tradition, continuons avec l’extension.properties.
Le fichierparams.properties doit être présent dans un répertoirefr/prados, accessible par le classloader. En demandant à la classe de récupérer la ressource, le code utilise implicitement le package de la classe. Placer ce fichier dans ce répertoire et non dans la racine permet d’éviter d’avoir deux fichiersparams.properties servant à différents frameworks dans le même répertoire. Les conventions de nommage de Java pour éviter les conflits s'appuient sur une inversion d’un nom de domaine DNS, d’où le nom du package un peu prétentieuse que j’utilise. Elle correspond à un nom de domaine qui m’appartient. Voilà tout.
Très souvent, les paramètres de déploiement sont présents dans la racine de l’application.
package fr.prados;
class MaClass
{
MaClass()
{
Properties prop=new Properties();
prop.load(getClass().getResourceAsStream("/params.properties"));
String p=prop.getProperty("jms.serveur");
...
}
}
ou
package fr.prados;
class MaClass
{
MaClass()
{
Properties prop=new Properties();
prop.load(getClass().getClassLoader()
.getResourceAsStream("params.properties"));
String p=prop.getProperty("jms.serveur");
...
}
}
C’est une approche sympathique et simple pour récupérer des paramètres. Le fichier params.properties possède alors les informations pour invoquer un service Web par exemple.
ws.order=http://192.168.0.10:8080/order
Puis, vient la phase d’intégration. Il faut maintenant packager l’application, créer un WAR ou un EAR. En toute logique, les ressources de l’application seront présentes dans l’archive. On retrouvera les fichiers suivants :
fr/prados/MaClass.class fr/prados/params.properties
Et c’est à ce moment que l’on se rend compte qu’il n’est plus possible de modifier les paramètres sans générer à nouveau l’archive.
Externalisons les paramètres
La première correction consiste à faire en sorte que le fichier de paramètre n’est plus porté par l’archive. Il est simplement sorti de celle-ci et livré à l’application dans un répertoire du serveur d’application. C’est simple, il suffit d’ajouter dans le CLASSPATH un répertoire où trouver les fichiers de paramètres. Il y a d’ailleurs souvent un répertoire déjà disponible. En créant le sous-répertoire fr/prados il est facile d’y placer le fichier de paramètres. Le processus d’intégration doit séparer les ressources à sortir des archives, des ressources à faibles volatilités qui seront intégrées dans les archives. Par exemple, les messages en différentes langues seront intégrés dans les archives, alors que les paramètres seront déportés. (Là, il peut devenir pertinent d’utiliser des extensions différentes ;-)
Pour installer l’application, il faut copier et modifier les fichiers de paramètres dans le bon répertoire du serveur d’application, les modifier puis déployer l’archive. C’est simple non ?
Oui et non. En fait, ce n’est plus un déploiement standard. Les archives de Java sont là pour permettre un déploiement facile, systématique et portable entre différents serveurs d’application. Une simple application Web peut être testée sous Tomcat, puis installée avec d’autres composants sous JBoss, qui intègre Tomcat. Cela permet au développeur d’être plus productif. Le démarrage de Tomcat est beaucoup plus rapide qu’un démarrage de JBoss.
Il faut également pouvoir installer plusieurs applications dans le même serveur d’application sans interférence. Oui mais voilà, ce n’est plus le cas maintenant que le fichier de paramètre est visible par toutes les applications. Que faire si chaque application désire des valeurs différentes ? Par exemple, le fichier Log4j peut être spécialisé par application. Si je suis la procédure d’installation, consistant à recopier des fichiers dans un répertoire du serveur d’application, ne vais-je pas casser une autre application ?
Souvent, pour des raisons de déverminage, il faut pouvoir installer plusieurs instances de la même application dans le même serveur d’application, éventuellement avec des versions différentes. Les classes loaders se chargent d’isoler les applications. Par exemple, des développeurs travaillent sur un composant invoqué par une application Web. N’ayant pas les ressources suffisantes, ils ne souhaitent pas ou ne peuvent pas installer sur leurs postes un serveur d’application pour accueillir l’application Web ou d’autres composants (EAI, base de données, etc). Un serveur peut alors proposer plusieurs instances de l’application Web, chacune paramétrée pour invoquer la version du composant sur les postes de chaque développeur. Comment faire cela si chaque version utilise le même fichier de paramètre ? Installer autant de serveurs d’application que de développeur ?
Que faire lors d’une mise à jour de l’application ? Écraser le fichier de paramétrage et l’adapter une nouvelle fois au contexte d’exécution en n’oubliant aucun paramètre ou bien laisser la version actuelle ? « Elle doit être bonne ». Une fois sur deux un paramètre n’a pas été reporté dans le nouveau fichier convenablement ou la version précédente n’est plus à jour. Bilan : découverte de bugs (souvent déjà rencontrés), perte de temps d’analyse et d’investigation, utilisation inutile des ressources de développement, etc. Les développeurs du composant ne peuvent plus le faire évoluer car ils doivent soutenir les intégrateurs pour trouver pour la n-ième fois le même bug de déploiement.
Un script d’intégration
Après quelques semaines d’intégration difficile, une grande décision est prise : Générer une archive par serveur cible. Ainsi, l’installation ne posera plus de problème. Un processus d’intégration est organisé pour pouvoir valoriser « facilement » ces paramètres avant de générer les archives des différents composants de l’application suivant les différents serveurs d’exécutions. Cela se passe généralement comme ceci : un fichier modèle de paramètre doit être maintenu par le développeur. Les paramètres dépendant du déploiement sont alors valorisés avec des variables.
jms.serveur=${jms_serveur}
Un méta-super script reprend tous les fichiers de paramètres, les modifient « automatiquement » avant de générer des WAR et des EAR pour la livraison. Le script génère des versions différentes pour l’intégration, la pré-production et la production. Souvent, il faut également générer des versions spécifiques de certains composants pour permettre aux développeurs de faire évoluer un des maillons de la chaîne de traitement.
Le développeur d’un composant possède alors deux fichiers proches mais différents. Un fichier pour tester son code, avec des valeurs en dur, et un fichier avec les variables à valoriser lors de l’intégration. Lors de l’ajout d’un paramètre, il ne doit surtout pas oublier de modifier les deux fichiers. La loi de Murphy étant ce qu’elle est, inévitablement, les deux fichiers se désynchronisent rapidement. Et c’est généralement le fichier utilisé pour l’intégration qui est erronée. « Je ne comprends pas, chez moi ça fonctionne ! »
Si un nouveau fichier de paramètre est créé par un développeur, celui-ci doit en informer l’équipe d’intégration qui doit alors modifier le processus super automatique. Souvent il s’entend répondre : « Pas pour le moment, on est en retard. On n’intègre pas ta nouvelle version, car on doit modifier notre script et ce n’est pas possible sans risque ». Le développeur attendra et constatera plus tard que son composant est bien intégré, mais les paramètres ne sont pas correctement valorisés ou tout simplement absents.
Si vous avez déjà travaillé sur des projets importants, il est probable que vous reconnaissez ces situations.
Pourquoi tous ces problèmes, pourquoi ces pertes de temps ? Pour une seule raison. Le développeur a trouvé un code « simple » pour récupérer des paramètres et l’utilise systématiquement pour tous, quel que soit son usage. En effet, les trois lignes de code sont faciles à écrire, mais le coût pour le projet est considérable. Le développeur n’a pas pensé une seconde à l’impact négatif de son choix sur la chaîne complète de production. Cela est amplifié par le fait que personne n’a conscience que le problème vient de si loin : des trois lignes du développeur. Sans broncher, l’équipe d’intégration a rédigé consciencieusement des scripts pour modifier les paramètres suivant le contexte de déploiement, pour générer les archives automatiquement, etc. J’ai vu des projets où l’intégration n’était pas possible en moins d’une semaine ! Comment tester dans ces conditions ? Tout cela pour trois misérables lignes de code ! L’intégration aurait dû le refuser.
Après avoir brossé le portrait sans complaisance de situations réelles, nous allons pouvoir étudier la solution : JNDI (Java Naming Directory Interface).
Pourquoi utiliser JNDI ?
JNDI est la solution proposée par J2EE pour exporter, partager, centraliser les paramètres d’une application dans le cadre d’un projet ou d’une entreprise. Il s’agit d’une base de données objets arborescente dont chaque répertoire peut utiliser une technologie différente. Certains répertoire sont spéciaux et permettent d’exploiter la même technologie pour des paramètres spécifiques à une application ou à un serveur d’application.
JNDI est généralement utilisé pour découvrir ou publier des EJBs. Cela s’effectuant sans code particulier. Les développeurs connaissent son existence sans en exploiter les concepts et les fonctionnalités. JNDI offre plus que cela.
Indépendance avec les drivers
La portabilité universelle de Java entraîne que les applications doivent être le plus indépendante possible des drivers J2EE utilisés pour communiquer avec l’environnement. Une application doit ignorer le type ou la version d’une base de donnée dont elle à besoin, le type de serveur JMS exploité, si la messagerie est POP3, IMAP, Lotus, etc.
Les serveurs d’applications permettent de déclarer des sources de données indépendamment des applications. Ces sources sont déclarées dans le serveur JNDI par le serveur d’application. L’application n’a plus qu'à y piocher pour obtenir une instance DataSource dont elle ignore la provenance. Ainsi, elle n’a pas à intégrer un driver particulier dans son archive. Cela est généralement interdit dans les licences des bases de données ! Seul le serveur d’application connaît et propose les différents drivers d’accès à la base de données.
S’il est nécessaire d’effectuer une migration d’un driver, généralement pour des raisons de sécurité, l’administrateur devra mettre à niveau le serveur d’application sans re-déployer toutes les applications.
Il est également possible de partager des ressources entre différentes applications du même serveur d’application. Par exemple, plusieurs applications partageront le même pool de connexion à la base de données afin de garantir le nombre limite de connexion vers la base.
Autres utilisations
Les serveurs JNDI ne servent pas uniquement à obtenir des EJB ou des DataSource. Pour récupérer un port d’un service Web, une ligne suffit.
String port = (String)new InitialContext().lookup("java:comp/env/ws.order");
Le préfixe java:comp/env indique que nous désirons obtenir un paramètre n’appartenant qu’au composant courant. L’application web par exemple.
La différence avec l’approche exploitant des fichiers.properties, est que les objets récupérés par le JNDI peuvent être de tous types, et non uniquement des chaînes de caractères qu’il faut encore interpréter.
URL port = (URL)new InitialContext().lookup("java:comp/env/ws.order");
Pour savoir comment contacter le serveur JNDI, l’application utilise le fichierjndi.properties du serveur d’application. Celui-ci est initialisé pour exploiter le serveur JNDI intégré. Pour utiliser un serveur JNDI centralisé, il faut intervenir sur ce fichier de paramètre. À défaut, il faut valoriser des paramètres lors de la construction de l’instance InitialContext().
Mais comment valoriser le paramètre ws.order exigé par l’application pour fonctionner ? Il y a plusieurs approches, plus ou moins riches, puissantes et faciles.
La première possibilité est d’utiliser le marqueur<env-entry/> dans le fichier web.xml.
<env-entry>
<env-entry-name>ws.order</env-entry-name>
<env-entry-type>java.lang.String</env-entry-type>
<env-entry-value>http://192.168.0.10:8080/order</env-entry-value>
</env-entry>
Cette approche est pratiquement aussi simple qu’avec le fichierparam.properties. Elle permet, moyennant une syntaxe plus complexe, de valoriser différents objets de différents types. Seuls les types primitifs sont acceptés. Nous utilisons une chaîne et non une URL.
Cette version présente les mêmes inconvénients : les paramètres sont portés par l’archive WAR, dans le fichier web.xml. Il n’est pas facile de les modifier lors de l’installation. Elle est jouable si le service Web utilisé est un service sur un site public, ne pouvant déménager. Parfois les serveurs d’applications permettent d’intervenir sur ces paramètres avec un utilitaire externe avant de déployer l’application.
Les spécifications J2EE permettent d’aller plus loin. Dans le fichier web.xml ou application.xml, il est possible de signaler que l’on souhaite utiliser une variable JNDI particulière.
<resource-ref >
<description>WebService Order</description>
<res-ref-name>ws.order</res-ref-name>
<res-type>java.net.URL</res-type>
<res-auth>Application</res-auth>
</resource-ref>
Cela permet de signaler au serveur d’application que le composant à besoin d’information complémentaire. Par exemple, que l’application Web à besoin d’avoir un accès à une base de données, à un serveur JMS ou JavaMail, etc. L’archive résultante ne possède pas de valeur pour ces paramètres. Elle signale simplement qu’elle en a besoin.
Lors de l’installation de l’application dans le serveur d’application, celui-ci signale un problème si les paramètres correspondants n’existent pas. L’application refusera alors de se déployer. Si le développeur à pris les précautions nécessaires, il n’est plus possible d’oublier un paramètre avant d’installer l’application.
Le serveur d’application doit alors offrir un objet pour le paramètre ws.order et il doit être du type java.net.URL.
Généralement, le serveur d’application mappe la clef JNDI de l’application vers une clef globale du serveur d’application. Chaque serveur utilise une approche différente. Par exemple, pour le serveur JBoss, l’application web doit posséder un fichier jboss-web.xml, permettant d’indiquer comment sont associés les clefs JNDI.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jboss-web PUBLIC "-//JBoss//DTD Web Application 2.3V2//EN"
"http://www.jboss.org/j2ee/dtd/jboss-web_3_2.dtd" >
<jboss-web>
<resource-ref>
<res-ref-name>ws.order</res-ref-name>
<jndi-name>ws.order</jndi-name>
</resource-ref>
</jboss-web>
Ce fichier indique que la clef locale à l’application java:comp/env/ws.order est mappée sur la clef JNDI globale/ws.order. D’autres techniques existent pour les différents serveurs d’application du marché. Les consoles d’administrations permettent généralement de modifier le mapping. Ainsi, la même application peut être installée en plusieurs exemplaires dans le même serveur d’application, chacune possédera un mapping différent. La figure suivante montre comment, avec Tomcat, associer une clé locale à une application avec une clef globale JNDI. Il ne faut pas oublier de cliquer sur « Commit changes » pour que les modifications soient persistantes.

La figure suivante montre comment valoriser une clef globale avec un type simple.

Organiser un serveur JNDI
Il existe différents contextes JNDI normalisés. Le contexte java: fait référence à une instance de JVM. Les objets présents sous ce contexte ne sont pas visibles par les autres JVM. Le contexte java:comp fait référence aux paramètres partagés par les différents composants de l’application (l’archive EAR). Cela permet de partager des paramètres entre une application Web et un EJB. Le contexte java:comp/env est visible par la tâche courante. Généralement, cela se traduit par une visibilité par le composant uniquement, l’application Web ou le serveur EJB. Ainsi, la même clef peut être utilisée pour deux composants d’une même application. Chaque clef ayant une valeur différente.
La racine est visible pour toutes les applications accédant au serveur JNDI. Certaines implantations proposent des branches particulières permettant de rendre persistant les objets déclarés et de les retrouver après un réamorçage du serveur JNDI.
Il est facile d’organiser l’arborescence JNDI pour refléter l’organisation physique de l’application ou de l’entreprise. Par exemple, une branche peut contenir les paramètres spécifiques à une application et partagés par tous les composants, même répartis dans différents serveurs. Sous celle-ci, il est facile de proposer d’autres branches permettant de spécialiser les paramètres pour chaque nœud d’un cluster par exemple.
D’autres branches peuvent donner l’accès à des systèmes partagés par différentes applications. Par exemple, une base de données de clients peut être utilisée par différentes applications de l’entreprise et être déclarée dans la racine d’un serveur JNDI.
Chaque élément déclaré dans un serveur JNDI peut être un objet ou une référence vers une autre clef. Lors de la lecture de la clef, la référence est automatiquement résolue et l’objet cible est retourné à l’application. C’est l’équivalent des liens physiques ou symboliques des gestionnaires de fichiers Unix. Avec cette technologie, il est possible de proposer des paramètres spécifiques à un composant d’une application, ceux-ci référencent des paramètres plus globaux de l’application (tableau suivant). La résolution de la clef java:comp/env/MaBase, aboutie de proche en proche à utiliser la base OracleThinDataSource déclaré au niveau du cluster. Cette résolution est transparente pour l’application.
| Clef | Valeur |
| java:comp/env/MaBase | Référence java:comp/ApplicationDS |
| java:comp/ApplicationDS | Référence java:ServeurDS |
| /ClusterDS | Objet OracleThinDataSource |
Clef
Valeur
java:comp/env/MaBase
Référence java:comp/ApplicationDS
java:comp/ApplicationDS
Référence java:ServeurDS
/ClusterDS
Objet OracleThinDataSource
Il est même possible de déclarer une branche avec un wrapper vers une autre technologie, par exemple, vers un serveur LDAP ou un autre serveur JNDI. Ainsi, un serveur JNDI peut être installé pour référencer les paramètres de l’entreprise. Un autre serveur JNDI est ajouté par département, avec une référence vers le serveur JNDI de l’entreprise. Un serveur Ldap peut servir à enregistrer des Datasources, etc. Avec un modèle objet particulier dans le serveur LDAP, il est possible d’y placer des objets javas. Toutes les organisations sont imaginables.

Il existe de nombreux providers.
| Provider | Technologie |
| COS Naming Service | CORBA Naming Service |
| DNS | Domain Naming System |
| DSML | Directory Services Markup Language v1 et v2 |
| File System | Gestions de fichiers |
| LDAP Service Provider | Annuaire |
| Mirror | Pour obtenir une copie d’un serveur JNDI distant |
| Novell | NDS |
| RMI Registry Service Provider | Base RMI |
| Windows Registry | Base de registre Windows |
| Xnam | Pour récupérer des objets présent dans des fichiers XML |
Provider
Technologie
COS Naming Service
CORBA Naming Service
DNS
Domain Naming System
DSML
Directory Services Markup Language v1 et v2
File System
Gestions de fichiers
LDAP Service Provider
Annuaire
Mirror
Pour obtenir une copie d’un serveur JNDI distant
Novell
NDS
RMI Registry Service Provider
Base RMI
Windows Registry
Base de registre Windows
Xnam
Pour récupérer des objets présent dans des fichiers XML
Le MBean org.jboss.naming.ExternalContext de JBoss permet d’enregistrer un contexte JNDI dans un autre, de déclarer un répertoire utilisant une autre technologie ou étant physiquement éloigné.
Certains serveurs JNDI savent fonctionner en Cluster. Les informations sont partagées par les différentes instances. Le client JNDI détecte automatiquement la perte d’un serveur pour interroger un autre membre de la grappe. Vu l’importance du serveur JNDI pour l’exécution de toutes les applications, il est indispensable d’utiliser un serveur JNDI en Cluster.
Comment chaque serveur d’application peut-il identifier le serveur JNDI à utiliser ? C’est le problème de la poule et de l'œuf. Il est absurde d’indiquer la localisation physique d’un serveur JNDI dans un serveur JNDI ! Par où commencer ? Chaque serveur d’application J2EE propose un fichierjndi.properties indiquant les paramètres permettant de contacter un serveur JNDI. C’est le seul fichier à modifier pour que toutes les applications de tous les serveurs de l’entreprise partagent les mêmes paramètres via le même serveur JNDI. Cette approche est préférable à ajouter des propriétés lors de la construction d’une instance InitialContext().
Un framework qui utilise le JNDI pour découvrir son contexte d’exécution doit utiliser une clef proche de lui (java:comp/env par exemple). Ainsi, l’intégrateur à toute latitude pour organiser les paramètres comme il le souhaite. Par lien successif, il peut transformer un lien local en lien entreprise.
Plusieurs instances d’un composant
Que faire si une application désire plusieurs instances d’un même composant d’un framework ?
Par exemple, un servlet jouant le rôle de ReverseProxy est proposé par un framework. Ce servlet propage la requête de l’utilisateur vers un autre serveur de l’entreprise. Sur invocation de http://www.monsite.com/rp/index.html, la servlet branché sur/rp/* propage la requête vers un serveur interne après avoir modifié l’URL, par exemple enhttp://192.168.0.10/compta/index.html. La servlet doit posséder un paramètre de déploiement lui indiquant comment transformer l’URL. Le prefixhttp://192.168.0.10/compta doit lui être communiqué via une clef JNDI. Par de problème, la servlet propose d’utiliser la clef java:comp/env/redirectURL pour trouver ce paramètre.
Oui mais voilà, l’application évolue et il faut maintenant utiliser deux instances de la servlet ReverseProxy, chacune dirigeant l’utilisateur vers deux serveurs différents. Pour résoudre cela, il faut utiliser un paramètre de servlet permettant d’indiquer la clef à utiliser. Plusieurs instances sont alors utilisables dans la même application.
Fichier web.xml:
<webapp>
...
<servlet>
<servlet-name>ReverseProxyCompta</servlet-name>
<display-name>Reverse proxy for old application</display-name>
<servlet-class>name.prados.philippe.ReverseProxyServlet</servlet-class>
<init-param>
<param-name>redirectURL</param-name>
<param-value>java:comp/env/redirectURL-Compta</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>ReverseProxyWS</servlet-name>
<display-name>Reverse proxy for web services</display-name>
<servlet-class>name.prados.philippe.ReverseProxyServlet</servlet-class>
<init-param>
<param-name>redirectURL</param-name>
<param-value>java:comp/env/redirectURL-WS</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>ReverseProxyCompta</servlet-name>
<url-pattern>/compta/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>ReverseProxyWS</servlet-name>
<url-pattern>/ws/*</url-pattern>
</servlet-mapping>
</webapp>
Le code d’initialisation du servlet est simple. Il tolère l’absence du paramètre de servlet redirectURL et exploite alors la clef par défaut.
public void init() throws ServletException
{
try
{
String JNDIRedirectURL = getInitParameter("redirectURL");
if (JNDIRedirectURL==null)
JNDIRedirectURL="java:comp/env/redirectURL";
this.redirectURL o= (URL)new InitialContext().lookup(JNDIRedirectURL);
}
catch (NamingException e)
{
throw new ServletException(e);
}
...
}
Nous avons beaucoup de redirection, mais une souplesse maximum.
Sauver un objet dans le serveur JDNI
C’est joli tous cela, mais comment initialiser un serveur JNDI avec les différentes valeurs des paramètres ? Une API très simple permet de le faire :
URL url=new URL("http://192.168.0.10:8080");
InitialContext rootCtx = new InitialContext();
rootCtx.bind("ws.order", url);
Quels sont les objets pouvant être mémorisés dans un serveur JNDI ? Tous les objets implémentant les interfaces Serializable ou Referenceable. L’interface Referenceable permet d’initialiser un objet à partir de clef/valeur, plutôt qu'à partir d’une sérialisation. Cela permet de modifier plus facilement les paramètres d’un objet en éditant un fichier XML ou à l’aide d’une interface utilisateur. Il est en effet très difficile d’intervenir sur le contenu d’un objet sérialisé sans en posséder la classe.
Pour implémenter l’interface Referenceable, l’objet doit répondre à la méthode getReference(), retournant une instance Reference contenant toutes les informations permettant la re-construction.
public class HAJMSQueueConnectionFactory implements ConnectionFactory,Referenceable
{
...
public Reference getReference() throws NamingException
{
Reference ref = new Reference(HAJMSQueueConnectionFactory.class.getName(),
HAJMSQueueBindFactory.class.getName(), null);
ref.add(…
ref.add(…
ref.add(…
return ref;
}
}
Généralement, la référence indique la classe de l’instance à générer et une classe périphérique implémentant l’interface ObjectFactory. D’autres paramètres sont ajoutés, permettant d’initialiser l’instance, comme par exemple, la localisation d’un serveur JMS, la taille du pool, s’il faut utiliser des stratégies particulières pour se reconnecter au serveur, etc. La classe ObjectFactory associé doit répondre à la méthode getObjectInstance(). Cette méthode récupère la référence et peut y puiser tous les paramètres nécessaires à la construction d’une instance ou d’un proxy.
public class HAJMSQueueBindFactory implements ObjectFactory
{
public Object getObjectInstance(final Object obj, final Name name,
final Context ctx, final Hashtable<?, ?> environment) throws Exception
{
if (obj instanceof Reference)
{
Reference ref = (Reference) obj;
ObjectRefAddr objRef = (ObjectRefAddr) ref.get("object");
…
return new HAJMSQueueConnectionFactory(prop);
}
}
}
Pour enregistrer une instance, il faut procéder comme précédemment :
ConnectionFactory cf=new HAJMSQueueConnectionFactory(...
InitialContext rootCtx = new InitialContext();
rootCtx.bind("/jms/MyJms",cf);
Si un répertoire /jms n’est pas présent, il faut le construire.
InitialContext rootCtx = new InitialContext(); rootCtx.createSubcontext("/jms"); rootCtx.bind("/jms/MyJms",cf);
Donc, après avoir enregistré une instance dans le serveur JNDI, il est possible d’en récupérer une copie lors de la résolution de la clef JNDI.
ConnectionFactory jmsFactory = (ConnectionFactory)new InitialContext().lookup("/jms/MyJms");
L’invocation du lookup entraîne l’extraction de la référence sur l’objet, puis l’invocation de l’ObjectFactory associé pour lui demander de construire l’instance, avant de la renvoyer à l’application.
Cela semble complexe mais les objets que l’on souhaite placer dans un serveur JNDI sont déjà capable de gérer cela. Par exemple, des constructeurs de connexions pour JavaMail, JMS, JDBC, etc. Ils sont tous compatibles avec les interfaces Serializable ou Referenceable.
Nous recherchons dans un serveur JNDI essentiellement des connecteurs vers d’autres technologies ou des chaînes de caractères avec les informations de connexion ou des noms de fichiers.
Si vous écrivez un driver JMS, JavaMail ou JDBC, vous devez alors répondre à ces interfaces pour accepter d’être installé dans un serveur JNDI.
Initialiser le serveur JNDI
Très bien, mais où appeler l’API permettant d'enregistrer un objet dans le JNDI ? Dans l’application ? Il n’en est pas question. Sinon, elle doit connaître la valeur des paramètres et on retombe dans le problème déjà évoqué. Cela doit être valorisé AVANT d’installer l’application. Il s’agit d’initialiser le serveur JNDI, avant que le serveur d’application démarre. Nous voulons paramétrer le serveur d’application, pas l’application. Ce n’est pas la même chose. Plus exactement, nous souhaitons initialiser le serveur JNDI du serveur d’application.
Chaque serveur d’application propose des interfaces et des technologies différentes pour initialiser son serveur JNDI.
Certains serveurs sont capables de persister les objets présents dans certaines branches JNDI. Une étape de l’installation peut alors créer les objets et les déclarer dans le serveur JNDI comme on le ferait d’une base de données. Certains provider JNDI sont par nature persistant.
Nous allons voir ce que proposent Tomcat, JBoss et BEA Weblogic pour initialiser le serveur JNDI.
Tomcat
Prenons dans un premier temps le cas du serveur d’application Web Tomcat. Ce serveur ne propose pas de serveur JNDI. Il simule un serveur JNDI à l’aide d’un paramétrage présent dans le fichier conf/server.xml. Il répond aux APIs sans lancer de serveur écoutant sur un port. Un marqueur <GlobalNamingResources/> permet de valoriser des clefs JNDI. Les sous-clefs <Environment/> et <Resource/> permettent de valoriser un type simple ou un type complexe.
<!-- Global JNDI resources -->
<GlobalNamingResources>
<Environment name="jms.port" type="java.lang.Integer" value="1000"/>
<Resource name="UserDatabase"
auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
Pour pouvoir initialiser un type complexe, il faut indiquer un factory. Il s’agit d’une classe quelconque implémentant l’interface ObjectFactory que nous venons d’étudier.
Proposons un ObjectFactory permettant d’enregistrer un objet dont l’initialisation s’effectue à l’aide d’une liste de propriété. L’objet à construire doit avoir un constructeur recevant une instance Properties. Le paramètre classname permet d’indiquer la classe de l’instance à construire. Il n’est pas ajouté dans l’instance Properties envoyée au constructeur.
public class BindFactory implements ObjectFactory
{
/**
* {@inheritDoc}
*/
public Object getObjectInstance(final Object obj, final Name name,
final Context nameCtx, final Hashtable environment) throws Exception
{
if (obj instanceof Reference)
{
Reference ref = (Reference) obj;
Class cl = Class.forName((String) ref.get("classname").getContent());
Constructor ctr;
try
{
ctr = cl.getConstructor(new Class[] { Hashtable.class });
}
catch (NoSuchMethodException e)
{
ctr = cl.getConstructor(new Class[] { Properties.class });
}
Properties prop = new Properties();
for (Enumeration e = ref.getAll(); e.hasMoreElements();)
{
StringRefAddr r = (StringRefAddr) e.nextElement();
if ("classname".equals(r.getType())) continue;
prop.setProperty(r.getType(), (String) r.getContent());
}
return ctr.newInstance(new Object[]{ prop } );
}
return null;
}
}
Avec cet objet, il est maintenant possible d’installer, dans un serveur JNDI, n’importe quel objet acceptant une liste de propriétés dans son constructeur.
<Resource
name="jms/MyJMS"
auth="Container"
factory="name.prados.philippe.BindFactory"
type="javax.jms.QueueConnectionFactory"
classname="name.prados.philippe.jms.HAJMSConnectionFactory"
description="JMS server"
host="192.168.0.11"
port="1000" />
Avec cette approche, il est possible de valoriser le pseudo serveur JNDI de Tomcat avec les objets que l’on souhaite, et avec les paramètres nécessaires. Déclarer un nouveau serveur JMS exotique ne pose pas de problème. Il suffit de paramétrer Tomcat (et non l’application) pour que toutes les applications puissent utiliser le serveur JMS, sans même avoir connaissance du driver ou de sa localisation physique.
Intégrer un driver exotique qui réponde à l’interface Referenceable consiste à se plonger dans la doc pour trouver la classe de Factory, quels sont les paramètres disponibles et comment les valoriser. Google est alors notre ami.
Lors de la mise à jour de l’application, il suffit de déployer à nouveau l’archive WAR, et c’est tout. Il n’est pas nécessaire de réinstaller la ressource JNDI. Si la ressource n’est pas présente, Tomcat le signale car il ne sait pas résoudre la référence indiquée dans le fichier web.xml.
JBoss
Pour initialiser un serveur JNDI sous JBoss, l’approche est similaire.
JBoss est un conteneur de MBean ( http://java.sun.com/products/JavaManagment/ ). La présence d’un fichier XML dans le répertoire <JBOSS>server\default\deploy permet de lui demander d’instancier un nouveau MBean. JBoss propose justement un MBean permettant de valoriser une clef JNDI. Il est possible de valoriser des types simples ou des types plus complexes comme une instance Properties.
Fichier: MonInit.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mbean PUBLIC "-//JBoss//DTD JBOSS XMBEAN 1.1//EN" "http://www.jboss.org/j2ee/dtd/jboss_xmbean_1_1.dtd">
<server>
<mbean
code="org.jboss.naming.JNDIBindingServiceMgr"
name="corso:name=innerJNDI">
<attribute
name="BindingsConfig"
serialDataType="jbxb"> <jndi:bindings
xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jndi="urn:jboss:jndi-binding-service:1.0"
xs:schemaLocation=
"urn:jboss:jndi-binding-service:1.0 resource:jndi-bindig-service_1_0.xsd"
>
<jndi:binding name="redirectURL">
<jndi:value type="java.net.URL">
http://localhost:8080
</jndi:value>
</jndi:binding>
<jndi:binding name="maps/testProps">
<java:properties xmlns:java="urn:jboss:java-properties"
xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xs:schemaLocation=
"urn:jboss:java-properties resource:java-properties_1_0.xsd">
<java:property>
<java:key>
key1
</java:key>
<java:value>
value1
</java:value>
</java:property>
<java:property>
<java:key>
key2
</java:key>
<java:value>
value2
</java:value>
</java:property>
</java:properties>
</jndi:binding>
</jndi:bindings>
</attribute>
</mbean>
</server>
JBoss utilise une api permettant de générer des instances à partir d’un schéma annoté XML (http://wiki.jboss.org/wiki/Wiki.jsp?page=JBossXB ) Il est ainsi possible de construire n’importe quelles instances, et les déclarer dans un serveur JNDI. Pour une grappe d’objets complexes, vous pouvez concevoir un schéma XML permettant de le décrire, puis l’enrichir d’annotation permettant à JBossXB de construire automatiquement les instances. JAXB, une approche équivalente et normalisée sera présente dans la prochaine version de JBoss.
Une autre approche consiste à rédiger un MBean dont la méthode start() enregistre une instance dans le serveur JNDI. Il faut ensuite packager le code dans une archive spéciale JBoss, d’extension sar, et la déposer dans le répertoire…/deploy.
Pour intégrer un MBean dans JBoss il faut rédiger une interface avec les accesseurs vers les propriétés permettant de construire l’instance à installer dans le serveur JNDI. Il faut ajouter les méthodes sans paramètre start() et stop() et suffixer son nom par MBean.
public interface MyConnectionFactoryMBean
{
public String getJndiName ();
public void setJndiName (String jndiName) throws NamingException;
public String getHostName ();
public void setHostName (String hostName);
public int getPort ();
public void setPort (int port);
public void start () throws Exception;
public void stop ();
}
Puis, rédiger une classe implémentant cette interface.
class MyConnectionFactory implements MyConnectionFactoryMBean
{
...
public void start() throws Exception
{
Object obj=new MaClass(…
// Instance à installer dans le JNDI
InitialContext rootCtx = new InitialContext();
rootCtx.bind(_jndiName,obj);
}
}
Il reste à packager cela dans une archive spéciale JBoss d’extension sar. Cette archive doit posséder, dans la racine, les archives nécessaires à l’objet (les drivers par exemple), les.class du MBean, et un fichier META-INF\jboss-service.xml. Ce fichier permet normalement de déclarer le service. Il est équivalent au fichier MonInit.xml ci-dessus. Mais, comme nous souhaitons ajouter la possibilité de déclarer une ressource spécifique à JBoss, et non en déclarer une, le fichier ne contient qu’un tag vide<server/>. L’archive sar est alors déposée dans le répertoire server/default/deploy.
Pour déclarer une ressource, un fichier XML permet de demander l’instanciation du MBean et de valoriser ses paramètres.
Fichier MaJms-ds.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mbean PUBLIC "-//JBoss//DTD JBOSS XMBEAN 1.1//EN" "http://www.jboss.org/j2ee/dtd/jboss_xmbean_1_1.dtd">
<server>
<mbean code="fr.prados.MyConnectionFactory"
name="monprojet:name=MonObjet,service=Factory">
<attribute name="JndiName">
/jms/MyJms
</attribute>
<attribute name="HostName">
192.0.0.11
</attribute>
<attribute name="Port">
1000
</attribute>
<depends>
jboss:service=Naming
</depends>
</mbean>
</server>
Plusieurs ressources équivalentes peuvent être déclarée, en multipliant les fichiers XML.
L’avantage de cette approche est qu’il est possible de modifier dynamiquement les attributs du MBean à l’aide de la console d’administration de JBoss. Si les méthodes set() du MBean sont correctement écrites, les modifications sont immédiatement utilisables par l’application. De plus, les MBeans sont de plus en plus acceptés par les différents serveurs d’applications. Le code est donc potentiellement réutilisable lors d’une migration vers une autre marque de serveur d’application.
JBoss propose un MBean permettant de consulter l’intégralité d’une base JNDI.

BEA Weblogic
Pour initialiser un serveur JNDI avec Weblogic, il faut ajouter une classe de démarrage. Elle doit implémenter l’interface weblogic.common.T3StartupDef. Celle-ci attend deux méthodes setServices() et surtout startup() . Cette dernière reçoit un nom et un Hashtable avec différents paramètres, valorisable dans la console d’administration de Weblogic.
Nous allons utiliser ces paramètres, comme pour les exemples précédents, pour construire une instance acceptant un Hashtable ou un Properties dans un constructeur.
public class JNDIInitStartup implements weblogic.common.T3StartupDef
{
public void setServices(T3ServicesDef def)
{
// Ignore
}
public String startup(String name, Hashtable args) throws Exception
{
try
{
String jndiName=null;
String className=null;
Properties prop=new Properties();
for (Enumeration e = args.keys(); e.hasMoreElements();)
{
String key=(String)e.nextElement();
String value=(String)args.get(key);
if ("jndiname".equals(key))
{
jndiName=value;
continue;
}
if ("classname".equals(key))
{
className=value;
continue;
}
prop.setProperty(key.trim(),value);
}
final Class cl=Class.forName(className);
Constructor constructor;
try
{
constructor=cl.getConstructor(new Class[]{Properties.class});
}
catch (NoSuchMethodException e)
{
constructor=cl.getConstructor(new Class[]{Hashtable.class});
}
final Object obj=constructor.newInstance(new Object[]{prop});
new InitialContext().bind(jndiName,obj);
return "Init "+name+"with the JNDI key "+ jndiName+" with "+className+"
instance ("+prop+")";
}
catch (Exception x)
{
x.printStackTrace();
return name+": Impossible to init JNDI ("+x+")";
}
}
}
Il reste à placer cette classe dans une archive, à l’ajouter lors du démarrage du serveur, dans le fichiersetDomainEnv.cmd ou setDomainEnv.sh pour pouvoir bénéficier de ces facilités.
Avec la console d’administration de BEA, il est alors facile d’ajouter une nouvelle étape lors du démarrage du serveur et de livrer les paramètres nécessaires à l’initialisation de l’instance à placer dans le JNDI.

Les paramètres sont portés par un marqueur <StartupClass/> dans le fichier config.xml. Il est facile de modifier cela avec des scripts jython adaptés
Par exemple, pour déclarer dans le JNDI, une branche connectée à un serveur LDAP, il faut indiquer de construire une instance InitialContext sous la clef ldap, et livrer les différents paramètres de connexions. Ainsi, l’application peut facilement consulter l’annuaire, en ignorant qu’il sa localisation physique.
{
String dn="uid=Moi";
Context ctx=(Context)new InitialContext().lookup("ldap");
UserDocument user = UserDocument.Factory.newInstance();
UserType utype = UserType.Factory.newInstance();
DirContext people=(DirContext)ctx.lookup("ou=People");
Attributes ldapuser=people.getAttributes(dn);
System.out.println(ldapuser.get("uid"));
System.out.println(ldapuser.get("cn"));
}
Et alors ?
L’installation de l’application s’effectue maintenant en deux étapes ! C’est équivalent à l’approche « j’externalise mes ressources dans un répertoire du serveur d’application ! » En effet. Il faut maintenant paramétrer le serveur d’application pour qu’il puisse proposer toutes les ressources nécessaires.
L’installation s’effectue en deux étapes distinctes :
- Paramétrer le serveur d’application si ce n’est déjà fait
- Installer l’application
L’étape de paramétrage est à faire qu’une seule fois par serveur. Que celui-ci soit en test, pré-production ou production. Si une nouvelle ressource est nécessaire, cela sera détecté lors de l’installation de l’application. Il faudra alors mettre à niveau le serveur d’application. Le script d’installation doit tenir compte de ces montées de version. Souvent, une console permet d’ajouter les paramètres nécessaires.
Cette étape est à rapprocher de l’installation du schéma de la base de données si l’application n’est pas capable de le monter automatiquement.
Si une nouvelle version d’un driver JDBC, JMS, Javamail ou autre est disponible, le script de mise à niveau pourra être rejoué, sans ré installer l’application.
L’étape d’installation de l’application est complètement conforme aux spécifications J2EE. Les JAR, WAR et EAR sont parfaitement indépendants de la plateforme d’exécution. Les développeurs peuvent les livrer directement à l’intégration. Il est donc beaucoup plus facile de migrer une application d’une marque de serveur vers une autre ou d’un environnement à un autre. L’application n’a pas à connaître les choix d’architectures physiques, s’il faut utiliser des paramètres partagés, centralisés, clusterisées, etc. La même application Web peut tourner sans modification sur un petit serveur Tomcat ou dans une ferme de serveur J2EE partageant les paramètres par département et entreprise, connecté à des serveurs LDAP.
Un framework peut utiliser une clef JNDI pour maîtriser son environnement, sans avoir connaissance de son contexte d’exécution. Par exemple, le même framework peut être utilisé dans une application WEB, un EJB, un EAI, un batch, etc, sans avoir à proposer un paramétrage particulier pour ces différentes situations. Une clef JNDI lui permet d’obtenir un accès à la base de données ou à un serveur de mail.
Les gains de cette approche sont :
- séparation des paramètres de l’application des paramètres de déploiement
- Indépendance de l’application avec les versions des drivers des composants externes (base de données, JMS, Javamail, etc.)
- souplesse du déploiement (plusieurs instances de l’application dans le même serveur d’application, paramètres partagés entres composants, serveurs, département ou entreprise)
- Facilité de monté de version (pas besoin de revoir les paramètres ou de générer de nouvelles archives)
- Détection de l’absence de paramètres (par le refus du serveur d’application d’installer une application dans un contexte insuffisant)
- Simplicité du code pour obtenir des paramètres complexes (récupération d’objet simple ou complexe comme des DataSources)
Possibilité d’utiliser les consoles d’administrations des serveurs d’application pour modifier les valeurs des paramètres.
Les inconvénients sont les suivants :
- Il faut utiliser une technologie différente pour initialiser le serveur JNDI suivant le serveur d’application. Ce n’est pas normalisé.
- Il faut un script d’installation permettant d’enrichir le serveur d’application, pour lui permettre d’offrir de nouvelle source de données.
Pensez à déclarer tous les paramètres valorisés lors de l’installation (répertoires, URL, serveur, base de donnée, etc) dans un serveur JNDI. N’utilisez pas le JNDI pour des paramètres applicatifs. Le code à écrire n’est pas plus compliqué que l’accès à un fichier.properties. Le script d’installation est parfois un peu plus complexe, mais étant joué qu’une seule fois, ce n’est pas un problème. L’application peut évoluer, être corrigée, dupliquée, distribuée et installée avec une réduction significative des bugs d’intégrations.
N’oubliez pas que moins il y a de paramètres, plus l’application est stable.
