Ejemplo simple de servidor y cliente JNDI

De ChuWiki

Introducción a JNDI[editar]

JNDI son las siglas de Java Naming and Directory Interface. Básicamente permite a las aplicaciones guardar y recoger datos identificados por un nombre, organizando dichos datos en una estructura que puede ser similar a una estructura de directorios.

¿Por qué es útil JNDI?. Imagina varias aplicaciones java, corriendo en el mismo o en distintos ordenadores. Imagina que esas aplicaciones tiene cada una sus propios ficheros de configuración con datos como parámetros de conexión a base de datos, direcciones IP de la red en la que pueden encontrar diversos servicios ... y en definitiva, cualquier cosa que se te ocurra que pueda ir en un fichero de configuración o de propiedades de una aplicación java. Imagina ahora que muchas de los datos en esos ficheros de configuración o de propiedades son comunes para todas o muchas de estas aplicaciones. Por ejemplo, varias de estas aplicaciones se conectan a una misma base de datos y tienen, en sus ficheros de configuración, la url de conexión a la base de datos, el usuario, la password, etc, etc. Esta claro que es un problema repetir esos datos en muchos ficheros de configuración dentro del mismo ordenador, y el problema es aún mayor si tenemos que ir copiandolos por distintos ordenadores en la red.

JNDI nos ayuda a resolver este problema. Nos basta montar un "servidor" de JNDI en algún sitio y meter todos estos datos de configuración en él, organizados en una estructura de directorios como mejor nos venga. Las aplicaciones conectarán con este servidor JNDI para pedir los datos que necesiten. Si los datos cambian, sólo tenemos que cambiarlos en el servidor JNDI.

Los datos que guardamos en un servidor JNDI pueden ser cualquiera de las clases java básicas (String, Boolean, Integer, ...), pero también cualquier clase java que implemente Serializable. Esto nos permite, por ejemplo, que no sea necesario guardar los datos de conexión a base de datos, sino que podriamos guardar directamente un DataSource a base de datos ya preparado. Las aplicaciones no tendrían que abrir sus propias conexiones, bastaría con que pidieran el DataSource al servidor JNDI y a partir de ahí obtener directamente las conexiones a base de datos.

Habitualmente, los servidores web, como Apache Tomcat, JBoss, etc son servidores JNDI, que permiten a las distintas aplicaciones desplegadas en ellos (ficheros .war o similar) recoger o compartir información, conexiones a base de datos, etc. De hecho, es práctica habitual que el servidor web ponga DataSource ya preparados a los distintos war, evitando que todos ellos abran sus propias conexiones a la base de datos.

Para Java de escritorio no existe un servidor JNDI como tal, pero se puede levantar uno usando alguna librería externa. En este tutorial haremos un ejemplo usando la librería jnpserver.jar de JBoss. Puedes ver el código completo del ejemplo en ejemplo-jndi


Dependencias[editar]

Para el ejemplo, necesitamos las dos dependencias maven siguientes:

		<dependency>
			<groupId>jboss</groupId>
			<artifactId>jnpserver</artifactId>
			<version>4.2.2.GA</version>
		</dependency>
		<dependency>
			<groupId>jboss</groupId>
			<artifactId>jbossall-client</artifactId>
			<version>4.2.2.GA</version>
		</dependency>

jbossall-client nos hace falta para el cliente. jnpserver nos hace falta para el servidor, pero si en el código java que levanta el servidor queremos también meter algunos datos en JNDI, necesitaremos también el jbossall-client. Nos podemos bajar estos jar manualmente del repositorio Maven

aunque si lo hacemos manualmente, tendremos que bajar también todos los jar que estos jar necesitan a su vez, proceso que puede ser tedioso.


El servidor[editar]

Levantar el servidor[editar]

El código para arranar el servidor es bastante sencillo

      System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");

      NamingBeanImpl jnpServer = new NamingBeanImpl();
      jnpServer.start();
      
      Main main = new Main();
      main.setNamingInfo(jnpServer);
      main.setPort(5400);
      main.setBindAddress(InetAddress.getLocalHost().getHostName());
      main.start();

Por medio de una propiedad de clave Context.INITIAL_CONTEXT_FACTORY (cuyo valor es "java.naming.factory.initial"), indicamos el nombre de la clase que va a hacer de factoría de contextos (un concepto extraño de JNDI). En este caso, es una clase de nuestra dependencia jnpserver.

Instanciamos la clase NamingBeanImpl (que es el servidor de JNDI) y la arrancamos llamando al método start(). Con esto ya tenemos nuestro servidor JNDI arrancado ... aunque no es accesible a través de un socket, de momento sólo está accesible dentro de nuestra aplicación java. Si queremos hacerlo accesible desde un socket, tenemos que meterlo dentro de la clase org.jnp.server.Main, también de nuestra dependencia jnpserver.

Instanciamos la clase Main, le pasamos por medio de setNamingInfo() el servidor de JNDI, le indicamos con setPort() a qué puerto debe atender, le indicamos con setBindAddress() a qué IP debe atender (podemos, por ejemplo, poner 127.0.0.1 si solo queremos atender conexiones locales, o nuestra IP de red si queremos atender conexiones de otros ordenadores, que es lo que hace de forma más o menos automática el código InetAddress.getLocalHost().getHostName()). Y finalmente llamamos a start(). Listo, nuestro servidor JNDI está disponible a través de socket en el puerto 5400.

Meter datos iniciales[editar]

En el mismo ejecutable que levanta el servidor (o en otro), podemos ahora empezar a meter datos de configuración para nuestros futuros clientes. El código es sencillo

      Hashtable<String, String> env = new Hashtable<String, String>();
      env.put(Context.INITIAL_CONTEXT_FACTORY,"org.jnp.interfaces.NamingContextFactory");
      env.put(Context.PROVIDER_URL,            "jnp://192.168.1.2:5400");
      Context context = new InitialContext(env);

      context.createSubcontext("config");
      context.bind("/config/applicationName", "MyApp");
      context.bind("/config/clase", new SomeData("pedro",4,new Date()));

Lo primero es instanciar un contexto de JNDI (otra vez el palabrejo). Para ello, instanciamos la clase InitialContext pasando un Hashtable de propiedades (línea 4 del código anterior). En esas propiedades ponemos dos:

  • env.put(Context.INITIAL_CONTEXT_FACTORY,"org.jnp.interfaces.NamingContextFactory"). Esta nos suena, es la misma que pusimos como propiedad al levantar el servidor, con la misma clase.
  • env.put(Context.PROVIDER_URL, "jnp://192.168.1.2:5400"). Aquí indicamos dónde está el servidor JNDI y a qué puerto atiende. Si no ponemos esta propiedad, accederíamos al servidor jnpServer que creamos en el paso anterior, directamente en nuestro propio programa. Si jnpServer lo ha creado otro programa, no tendríamos acceso a él a menos que pongamos Context.PROVIDER_URL.

Una vez que tenemos el contexto, podemos empezar a meter datos. Podemos crear directorios y subdirectorios con el método createSubcontext(). En el código hemos creado un subdirectorio de nombre "config".

También podemos empezar a meter datos, con context.bind(). El primer parámetro es el path dentro de JNDI donde queremos el dato y el nombre del dato. En el ejemplo, estamos metiendo los datos applicationName y clase en e subdirectorio config.

Para applicationName, hemos metido como dato un String "MyApp". Para clase, metemos una instancia de la clase SomeData, que debe implementar Serializable.

El cliente[editar]

En otro ejecutable que queramos acceder a esos datos, pondremos el siguiente código

      final Hashtable<String, String> env = new Hashtable<String, String>();
      env.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
      env.put(Context.PROVIDER_URL, "jnp://192.168.1.2:5400");
      Context context = new InitialContext(env);

      System.out.println("Application name = "
            + context.lookup("java:/config/applicationName"));
      System.out.println("someData = " + context.lookup("java:/config/clase"));

Igual que antes, necesitamos un contexto, instanciando InitialContext con las dos propiedades en cuestión, la Context.INITIAL_CONTEXT_FACTORY y el Context.PROVIDER_URL. Nada nuevo.

Para recuperar datos, usamos el método context.lookup(), pasando como parámetro el path y nombre del dato que queremos. Este método devuelve un Object, así que es cosa nuestra hacer el cast al objeto real que haya detrás. También es necesario que la clase que recibamos esté en nuestro classpath, u obtendremos un error al intentar recuperarla. Es decir, no podemos recoger del servidor clases que no conozcamos previamente (que no estén en nuestro classpath).

Y eso es todo.