Catégorie : Programmation     Tags :      

    Retrouvez cet article dans : Linux Magazine 87

    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 partage de paramètres entre nœuds d’un cluster, etc. dans le respect des normes J2EE.

    Parmi les différentes familles de paramètres (voir l’article http://www.prados.fr/Langage/Java/index.html#Param) : 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.

    1. 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 vient du fait que les développeurs utilisent généralement, à tort, la classe RessourceBundle 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, toute extension du fichier est possible 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’internationalisations 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 fichier params.properties doit être présent dans un répertoire fr/prados, accessible par la classe loader. 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 fichiers params.properties servant à différents frameworks dans le même répertoire. Les conventions de nommage de Java pour éviter les conflits s’appuie sur une inversion d’un nom de domaine DNS, d’où le nom du package un peu prétentieux que j’utilise. Il 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.

    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é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 câsssser 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 composante (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éveloppeurs ?
    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.

    1.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 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 tout type, 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 fichier jndi.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 fichier param.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 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 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ées 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 1 montre comment, avec Tomcat, associer une clef locale à une application avec une clef globale JNDI. Il ne faut pas oublier de cliquer sur " Commit changes " pour que les modifications soient persistantes.

    /img-articles/lm/87/art-5/fig-1.jpg

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

    /img-articles/lm/87/art-5/fig-2.jpg

    3. 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ée client 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 1). La résolution de la clef java:comp/env/MaBase aboutit de proche en proche à utiliser la base OracleThinDataSource déclarée au niveau du cluster. Cette résolution est transparente pour l’application.

    /img-articles/lm/87/art-5/t1.jpg

     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 model object particulier dans le serveur LDAP, il est possible d’y placer des objets java. Toutes les organisations sont imaginables (Figure 3). Il existe de nombreux providers (Tableau 2).

    /img-articles/lm/87/art-5/fig-3.jpg

    Figure 3 : architecture JNDI

    /img-articles/lm/87/art-5/t2.jpg

    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 fichier jndi.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 a toute latitude pour organiser les paramètres comme il le souhaite. Par lien successif, il peut transformer un lien local en lien entreprise.

    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 http://www.monsite.com/rp/index.html, la servlet branchée sur /rp/* propage la requête vers un serveur interne après avoir modifié l’URL, par exemple en http://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 préfixe http://192.168.0.10/compta doit lui être communiqué via une clef JNDI. Pas 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 de la 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 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 Serializable ou Referenceable.
    L’interface Referenceable permet d’initialiser un objet à partir de clef/valeur, plutôt qu’a 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 reconstruction.

    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ée 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à capables 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.

    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 conf/server.xml. Il répond aux API 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és. L’objet à construire doit avoir un constructeur recevant une instance Properties.
    Le paramètre classname permettra 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 = 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, 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.

    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 <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 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 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ètres 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é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 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 (figure 4).

    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.

    L’étape de paramétrage n’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 incrémentation 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ée 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 plate-forme 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étrages partagés, centralisés, clusterisés, etc. La même application Web peut tourner sans modification sur un petit serveur Tomcat ou dans une ferme de serveurs J2EE partageant les paramètres par département et entreprise, connectés à 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ée ou à un serveur de mail.

    /img-articles/lm/87/art-5/fig-4.jpg

    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é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.

    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ée.

    Pensez à déclarer tous les paramètres valorisés lors de l’installation (répertoires, URL, serveur, base de données, 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 n’é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égration.
    N’oubliez pas que moins il y a de paramètre, plus l’application est stable.

    Retrouvez cet article dans : Linux Magazine 87

    Posté par (La rédaction) | Signature : Philippe Prados | Article paru dans

    Laissez une réponse

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