4. Comunicación

En este tema veremos los elementos que podemos utilizar para establecer una comunicación entre los distintos servlets de una aplicación web, así como la comunicación entre los servlets y otros elementos de la aplicación web.

4.1 Contexto global de los servlets

Vamos a comenzar estudiando el contexto de los servlets (Servlet Context). Este objeto de contexto es propio de cada aplicación web, es decir, tendremos un objeto ServletContext por aplicación web, por lo que nos servirá para comunicar los servlets de dicha aplicación.

public void init(ServletConfig config)

En la inicialización del servlet (método init), se nos proporcionará un objeto ServletConfig como parámetro. Mediante este objeto podemos:

String nombre = config.getServletName();
String valor_param = config.getInitParameter(nombre_param);
Enumeration nombres_params = config.getInitParameterNames();
ServletContext context = config.getServletContext();

Esta última función es la más importante, ya que nos permite acceder al objeto de contexto global de la aplicación, con el que podremos realizar una serie de operaciones que veremos a continuación.


Figura 1. Contexto global de los servlets

Tanto el objeto ServletConfig como ServletContext pueden ser obtenidos directamente desde dentro de nuestro servlet llamando a los métodos getServletConfig y getServletContext respectivamente, definidos en GenericServlet, y por lo tanto disponibles en cualquier servlet.

4.1.1 Atributos de contexto

Dentro del objeto de contexto de nuestra aplicación podremos establecer una serie de atributos, que serán globales dentro de ella. Estos atributos son un conjunto de pares <nombre, valor> que podemos establecer y consultar desde los distintos servlets de nuestra aplicación web. El nombre del atributo será una cadena de texto (String), mientras que el valor podrá ser cualquier objeto java (Object).

Para consultar el valor de un atributo utilizaremos:

Object valor = context.getAttribute(nombre);

Daremos valor a un atributo con:

context.setAttribute(nombre, valor);

Podemos también eliminar un atributo:

context.removeAttribute(nombre);

Lo cual dejará el atributo a null, igual que si nunca le hubiesemos asignado un valor. Por último, con

Enumeration enum = context.getAttributeNames();

Obtenemos la lista de nombres de atributos definidos en el contexto.

Hay que hacer notar en este punto, que el objeto de contexto a parte de ser propio de cada aplicación web, es propio de cada máquina virtual Java. Cuando trabajemos en un contexto distribuido, cada máquina ejecutará una VM distinta, por lo que tendrán también objetos de contexto diferentes. Esto hará que si los servlets de una aplicación se alojan en máquinas distintas, tendrán contextos distintos y este objeto ya no nos servirá para comunicarnos entre ellos. Veremos más adelante formas alternativas de comunicación para estos casos.

4.1.2 Parámetros de inicialización

El objeto ServletConfig nos proporcionaba acceso a los parámetros de inicialización del servlet en el que nos encontramos. Con ServletContext, tendremos acceso a los parámetros de inicialización globales de nuestra aplicación web. Los métodos para obtener dichos parámetros son análogos a los que usabamos en ServletConfig:

String valor_param = context.getInitParameter(nombre_param);
Enumeration nombres_params = context.getInitParameterNames();

4.1.3 Acceso a recursos estáticos

Este objeto nos permite además acceder a recursos estáticos alojados en nuestro sitio web. Utilizaremos los métodos:

URL url = context.getResource(nombre_recurso);
InputStream in = context.getResourceAsStream(nombre_recurso);

El nombre del recurso que proporcionamos será una cadena que comience por "/", lo cual indica el directorio raiz dentro del contexto de nuestra aplicación, por lo tanto serán direcciones relativas a la ruta de nuestra aplicación web.

El primer método nos devuelve la URL de dicho recurso, mientras que el segundo nos devuelve directamente un flujo de entrada para leer dicho recurso.

Hay que señalar que esto nos permitirá leer cualquier recurso de nuestra aplicación como estático. Es decir, si proporcionamos como recurso "/index.jsp", lo que hará será leer el código fuente del JSP, no se obtendrá la salida procesada que genera dicho JSP.

Podemos también obtener una lista de recursos de nuestra aplicación web, con:

Set recursos = context.getResourcePaths(String ruta);

Nos devolverá el conjunto de todos los recursos que haya en la ruta indicada (relativa al contexto de la aplicación), o en cualquier subdirectorio de ella.

4.1.4 Redirecciones

Si lo que queremos es acceder a recursos dinámicos, el método anterior no nos sirve. Para ello utilizaremos estas redirecciones. Utilizaremos el objeto RequestDispatcher que nos proporciona ServletContext.

Hemos de distinguir estas redirecciones de la que se producen cuando ejecutamos

response.sendRedirect();

Con sendRedirect lo que estamos haciendo es devolver al cliente una respuesta de redirección. Es decir, será el cliente, quien tras recibir esta respuesta solicite la página a la que debe redirigirse.

Con RequestDispatcher es el servidor internamente quien solicita el recurso al que nos redirigimos, y devuelve la salida generada por éste al cliente, pero todo ello de forma transparente al cliente. En cliente no sabrá en ningún momento que se ha producido una redirección.

Para obtener un objeto RequestDispatcher podemos usar los siguientes métodos de ServletContext:

RequestDispatcher rd = context.getRequestDispatcher(ruta);
RequestDispatcher rd = context.getNamedDispatcher(ruta);

Como ruta proporcionaremos la ruta relativa al contexto de nuestra aplicación, comenzando por el carácter "/", del recurso al que nos queramos redirigir. También podemos obtener este objeto proporcionando una ruta relativa respecto al recurso actual, utilizando para ello el método getRequestDispatcher del objeto ServletRequest, en lugar de ServletContext:

RequestDispatcher rd = request.getRequestDispatcher(ruta);

Podemos utilizar el RequestDispatcher de dos formas distintas: llamando a su método include o a forward.

rd.include(request, response);

El método include incluirá el contenido generado por el recurso al que redireccionamos en la respuesta, permitiendo que se escriba este contenido en el objeto ServletResponse a continuación de lo que se haya escrito ya por parte de nuestro servlet. Se podrá llamar a este método en cualquier momento. Lo que no podrá hacer el recurso al que redireccionamos es cambiar las cabeceras de la respuesta, ya que lo único que estamos haciendo es incluir contenido en ella. Cualquier intento de cambiar cabeceras en la llamada a include será ignorado.

Si hemos realizado la redirección utilizando un método getRequestDispatcher (no mediante getNamedDispatcher), en la petición del servlet al que redireccionamos podremos acceder a los siguientes atributos:

javax.servlet.include.request_uri
javax.servlet.include.context_path
javax.servlet.include.servlet_path
javax.servlet.include.path_info
javax.servlet.include.query_string

Con los que podrá consultar la ruta desde donde fué invocado.

rd.forward(request, response);

El método forward sólo podrá ser invocado cuando todavía no se ha escrito nada en la respuesta del servlet. Esto es así porque esta llamada devolverá únicamente la salida del objeto al que nos redirigimos. Si esto no fuese así, se produciría una excepción IllegalStateException. Una vez el método forward haya devuelto el control, la salida ya habrá sido escrita completamente en la respuesta.

Si el recurso al que redireccionamos utiliza direcciones relativas, estás direcciones se considerarán relativas al servlet que ha hecho la redirección, por lo que si se encuentran en rutas distintas se producirá un error. Tenemos que hacer que las direcciones sean relativas a la raiz del servidor para que funcione correctamente (direcciones que comiencen por "/").

4.1.5 Otros métodos

La clase ServletContext nos proporciona otros métodos de utilidad, que podremos consultar accediendo a su documentación JavaDoc.

Un método de interés es log, que nos permite escribir texto en el fichero de log del servlet:

context.log(mensaje);

Esto será util para tener un registro de eventos que ocurren en nuestra web, o bien para depurar errores.

4.1.6 Listeners de contexto

Existen objetos que permanecen a la escucha de los distintos eventos que pueden ocurrir en el objeto de contexto de servlets, ServletContext.

Un primer listener, es el ServletContextListener, que nos permitirá dar respuesta a los eventos de creación y destrucción del contexto del servlet. El código para este listener será como sigue a continuación:

import javax.servlet.*;

public class MiContextListener implements ServletContextListener {

    public void contextDestroyed(ServletContextEvent sce) {
// Destruccion del contexto
    }

    public void contextInitialized(ServletContextEvent sce) {
// Inicialización del contexto
    }
}

Esto nos será de gran utilidad si necesitamos inicializar ciertas estructuras de datos que van a utilizar varios servlets. De esta forma el contexto se habrá inicializado antes de que los servlets puedan ejecutarse.

Si lo que queremos es saber cuando se ha añadido, eliminado, o modificado alguno de los atributos del contexto global, podemos utilizar un listener ServletContextAttributeListener. Los métodos que deberemos definir en este caso son los siguientes:

import javax.servlet.*;

public class MiContextAttributeListener implements ServletContextAttributeListener {

    public void attributeAdded(ServletContextAttributeEvent scae) {
// Se ha añadido un nuevo atributo
    }

    public void attributeRemoved(ServletContextAttributeEvent scae) {
// Se ha eliminado un atributo
    }

    public void attributeReplaced(ServletContextAttributeEvent scae) {
// Un atributo ha cambiado de valor
    }
}

Para hacer que estos objetos se registren como listeners y permanezcan a la escucha, deberemos registrarlos como tales en el descriptor de despliegue de la aplicación. Deberemos añadir un elemento <listener> para cada objeto listener que queramos registrar:

<listener>
<listener-class>MiContextListener</listener-class>
</listener>

<listener>
<listener-class>MiContextAttributeListener</listener-class>
</listener>

4.2 Otros métodos de comunicación entre servlets

Como hemos dicho anteriormente, el contexto de los servlets tiene el inconveniente de que se creará uno por cada VM. Por lo tanto, si el contenedor de los servlets se encuentra distribuido en varias máquinas, y los tenemos alojados en distintas VM, accederán a contextos distintos, aunque pertenezcan a una misma aplicación web.

Por lo tanto, en estos casos el contexto no nos servirá para comunicar todos los servlets de la aplicación web. Si queremos hacer una comunicación totalmente global, tendremos que utilizar otros métodos, como almacenar los datos en una base de datos, en sesiones, o bien en Enterprise Java Beans (EJB).

4.3 Servlets y JSPs

Los servlets y los JSPs son tecnologías complementarías. Cada una de ellas es más apropiada para realizar ciertas tareas. Por lo tanto, lo más adecuado será integrar ambas tecnologías, y realizar con cada una las tareas más apropiadas para ella.

Los servlets serán adecuados cuando se requiere mucha programación. Por el contrario, los JSPs serán más apropiados para generar HTML, ya que son mejores para separar el código y la presentación.

Podremos integrar ambas tecnologías, realizando la programación en el servlet, y redirigiendo al JSP adecuado para que produzca la presentación, utilizando el RequestDispatcher visto anteriormente, bien por el método forward o include.

Puede ser necesario que el servlet proporcione cierta información al JSP, ya que el servlet en el procesamiento puede haber establecido ciertos datos que el JSP deberá presentar. Una forma para proporcionar estos datos es establecerlos como atributos en el objeto ServletRequest:

MiClase valor = generaDatos();
request.setAttribute("nombre", valor);

Este valor podrá ser cualquier objeto Java (Object). En el JSP podremos obtener dicho objeto de la petición:

<% MiClase valor = (MiClase)request.getAttribute("nombre"); %>

Podemos también incluir estos datos en ámbito del contexto de la aplicación o de la sesión, y obtenerla en el JSP como un bean. En el ámbito de la aplicación (contexto) haremos lo siguiente:

context.setAttribute("nombre", valor);

Que puede ser importado por el JSP como bean de la siguiente forma:

<jsp:useBean id="nombre" class="MiClase" scope="application"/>

Si queremos incluir la información en el ámbito de la sesión, lo haremos de forma similar:

session.setAttribute("nombre", valor);

Que puede ser importado por el JSP como bean de la siguiente forma:

<jsp:useBean id="nombre" class="MiClase" scope="session" />

4.4 Servlets y Applets

Vamos a ver a continuación distintas formas de usar los applets como front-end de los servlets. De igual forma, podremos hacer una aplicación stand-alone que actue como front-end del servlet.

La forma más sencilla de invocar la funcionalidad de un servlet desde un applet es llamando a showDocument para, mediante un petición GET, invocar el servlet y que el navegador muestre la salida generada por dicho servlet. Ocurre de igual forma que cuando ejecutamos un link a un servlet en el navegador, siendo en este caso el applet el que ha ejecutado el link llamando al método showDocument.

getAppletContext().showDocument(url); 

Con esto lo único que conseguiremos es hacer que el navegador muestre el contenido generado por el servlet, invocando dicha acción desde el applet. Por lo tanto, para esto si será necesario encontrarnos dentro en un applet que se encuentre en un navegador web. En una aplicación stand-alone no podremos.

HTTP Tunneling

Si queremos tener control desde el applet de lo que genera el servlet deberemos utilizar otro método para obtener el resultado. Este segundo método se denomina HTTP tunneling. Lo que haremos será construir un objeto URL con la URL del servlet que queramos utilizar, y abrir una conexión con dicha URL para realizar la petición y recibir el resultado generado por el servlet.

Una vez hemos construido la URL abriremos una conexión con dicha URL e indicamos que no utilice la caché, para que siempre se solicite al servidor que genere nuevamente el contenido:

URLConnection con = url.openConnection();
con.setUseCaches(false);

Ahora podemos establecer las cabeceras de la petición que queramos:

con.setRequestProperty("header", "valor");

Entonces ya podemos crear la conexión con la URL:

InputStream in = con.getInputStream();

Es en este momento cuando se establece la conexión con el servlet y éste genera el resultado. Mediante el flujo de entrada que obtenemos podremos leer desde nuestro applet el contenido generado. Este flujo de entrada podrá ser de cualquier tipo, según el tipo de contenido generado por el servlet. Podemos leer directamente la secuencia de bytes, o transformarlo a un flujo de carácteres (BufferedReader por ejemplo).

Paso de objetos

También podremos hacer que el servlet nos devuelva objetos. Si el servlet serializa un objeto y lo escribe en la respuesta (utilizando un ObjectOutputStream), desde el applet podremos utilizar un ObjectInputStream para leer dicho objeto:

ObjectInputStream ois = new ObjectInputStream(in);
MiClase obj = (MiClase)ois.readObject();

Por último, una vez leido todo el contenido, cerraremos el flujo de entrada:

in.close();

Para que el servlet devuelva un objeto deberá especificar como tipo del contenido generado el siguiente tipo MIME:

application/x-java-serialized-object

Y lo único que tendrá que hace entonces será obtener un ObjectOutputStream a partir del flujo de salida de la respuesta, y escribir el objeto en él:

MiClase result = generaObjetoResultante();
response.setContentType("application/x-java-serialized-object");
ObjectOutputStream oos = new ObjectOutputStream(response.getOutputStream());
oos.writeObject(oos);
oos.flush();

Hemos visto que el applet podrá realizar una petición GET al servlet, y como resultado podrá devolvernos cualquier tipo de datos, pudiendo incluso enviarnos un objeto. Sin embargo, lo que no podremos hacer de esta forma es enviar objetos del applet al servlet, ya que los parámetros proporcionados en una petición GET están en la misma URL, y por lo tanto tienen que ser texto.

Petición POST

Para poder pasar objetos como parámetros tendremos que hacer una petición POST. Para ello la conexión que abrimos con la URL debe ser de lectura/escritura:

URLConnection con = url.openConnection();
con.setUseCaches(false);
con.setDoOutput(true);

A continuación crearemos un array de bytes donde escribiremos la información de los parámetros que pasamos por POST:

ByteArrayOutputStream baos = new ByteArrayOutputStream(512);

Podemos usar distintos tipos de flujos sobre este objeto, según vayamos a escribir carácteres (PrintWriter) u objetos (ObjectOutputStream) como parámetros.

Una vez escrito este contenido, deberemos establecer una cabecera con la longitud de dicho contenido:

con.setRequestProperty("Content-Length", String.valueOf(baos.size()));

Por cuestión de compatibilidad entre navegadores, será conveniente establecer manualmente el tipo de contenido:

connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

Y en este momento ya podremos escribir los parámetros en el flujo de salida de la conexión:

baos.writeTo(connection.getOutputStream());

Una vez hecho esto ya podemos obtener el flujo de entrada de la conexión, para leer el contenido que genera el servlet de la misma forma que lo haciamos en el caso de la petición GET.