Retrouvez cet article dans : Linux Magazine 87
1. Récupérer les paramètres de déploiement
Généralement, on retrouve ces paramètres dans un fichier 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 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 ws.order=http://192.168.0.10:8080/orderPuis, 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.propertiesEt 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.
1.1 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épertoire1.2 Un script d’intégration
Après quelques semaines d’intégration difficile, une grande décision est prise : générons 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 comprend 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).
2. 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 objet arborescente dont chaque répertoire peut utiliser une technologie différente. Certains répertoires 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 EJB. 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.2.1 Indépendance avec les drivers
La portabilité universelle de Java entraîne que les applications doivent être le plus indépendantes 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 a 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ée. 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 redé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.2.2 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 URL port = (URL)new InitialContext().lookup("java:comp/env/ws.order");
Pour savoir comment contacter le serveur JNDI, l’application utilise le fichier <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 fichier
<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 a besoin d’informations complémentaires. Par exemple, que l’application Web a besoin d’avoir un accès à une base de donnée, à 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 signalera un problème si les paramètres correspondants n’existent pas. L’application refusera alors de se déployer. Si le développeur a 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 <?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

3. Organiser un serveur JNDI
Il existe différents contextes JNDI normalisés. Le contexte

Figure 3 : architecture JNDI

3.1 Plusieurs instances d’un composant
Que faire si une application désire plusieurs instances d’un même composant d’un framework ? Par exemple, une servlet jouant le rôle de ReverseProxy est proposée par un framework. Cette servlet propage la requête de l’utilisateur vers un autre serveur de l’entreprise. Sur invocation de <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 de la servlet est simple. Il tolère l’absence du paramètre de servlet 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 redirections, mais une souplesse maximum.
4. Sauver un objet dans le serveur JNDI
C’est joli tout 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émantant les interfaces 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 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 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 5. Initialiser le serveur JNDI
Très bien, mais où appeler l’API permettant d’enregister 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ée. Certains providers JNDI sont par nature persistants. Nous allons voir ce que propose Tomcat et JBoss pour initialiser le serveur JNDI.5.1 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 c<!-- 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 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 = 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
5.2 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<?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 de les déclarer dans un serveur JNDI.
Pour une grappe d’objets complexe, vous pouvez concevoir un schéma XML permettant de la décrire, puis l’enrichir d’annotations 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 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 <?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ées, 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 6. 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.

- 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ée, JMS, Javamail, etc.)
- souplesse du déploiement (plusieurs instances de l’application dans le même serveur d’application, paramètres partagés entre composants, serveurs, département ou entreprise) ;
- facilité d’incrémentation 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.
- 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ée.
Retrouvez cet article dans : Linux Magazine 87





Laissez une réponse
Vous devez avoir ouvert une session pour écrire un commentaire.