Enterprise Java con J2EE: EJBs, Servlets, JSP
y otras Tecnologías de Servidor (I)
por Pedro Agulló
Soliveres
En los
últimos tiempos Java se ha convertido probablemente en el lenguaje más popular
para la creación de aplicaciones de servidor. De hecho, Sun ha definido una
plataforma específica para el desarrollo de aplicaciones distribuidas, J2EE (Java
2 Enterprise Edition), que proporciona todo lo necesario para la creación
de aplicaciones de gran complejidad destinadas al ámbito corporativo.
J2EE no
es solo un conjunto de APIs para el desarrollo de aplicaciones distribuidas,
sino también una infraestructura en tiempo de ejecución dentro de la cuál se
ejecutan dichas aplicaciones: esta infraestructura la proporciona un tipo
especial de aplicaciones, llamadas Servidores de Aplicación (Application
Servers), tales como WebSphere de IBM, Inprise Application Server de
Borland, etc., que estudiaremos más adelante.
Las aplicaciones
distribuidas suelen tener una estructura en la que se pueden distinguir
esencialmente tres capas:
·
El
interface de usuario: normalmente corre en PCs o terminales individuales
y es típicamente proporcionado por una aplicación independiente. Una alternativa
muy común es el uso de un navegador Internet (browser). Esta es la
estrategia de Amazon, por ejemplo: el interface de usuario a través del
cual escogemos libros u otros productos e introducimos la información necesaria
para poder recibirlos es una serie de páginas HTML, generadas dinámicamente y
enviadas a nuestro navegador.
·
La
lógica del negocio, que encapsula todo lo referente a cómo trabajar con Clientes, Cuentas, Proveedores, etc. Típicamente correrá en
otra máquina remota, y es muy posible que de hecho esta capa esté repartida
entre varias máquinas.
·
La
capa de almacenamiento de datos, posiblemente implementada en otras
máquinas, típicamente mediante un Gestor de Base de Datos, como Oracle, DB2 u
otro.
Nótese
que estas capas pueden disponerse físicamente de muchas maneras, siendo incluso
posible (aunque poco común), que todas las capas se ejecuten en la misma
máquina, o incluso que una capa esté distribuida en varias máquinas: por
ejemplo, como parte de la capa de almacenamiento de datos podemos tener una
base de datos técnica en Oracle corriendo en una estación de trabajo HP,
mientras que la base de datos de facturación puede correr bajo AS/400. La Figura
1 muestra un sistema en el que varias máquinas cliente están accediendo a
información de estas dos bases de datos.
Figura 1.
Aplicación distribuida en que varias aplicaciones cliente acceden a la lógica
del negocio, ubicada en el Servidor1, que a su vez accede a los datos en los
servidores 2 y 3.
Uno de
los principales componentes de J2EE es la tecnología de Enterprise JavaBeans,
que proporciona un modo estándar de desarrollo de componentes de servidor que
encapsulen la funcionalidad del negocio mediante clases tales como Cliente, Artículo, Factura, etc. Para implementar estos
objetos deberemos derivar y extender una serie de clases e interfaces estándar
definidos en el estándar.
Nos
referiremos a los objetos que implementan la lógica del negocio como Enterprise
JavaBeans (EJBs) o simplemente Enterprise Beans. Por ahora, bastará con saber
que en general la funcionalidad en el servidor se implementará mediante estos
Enterprise Beans, los cuáles se ejecutarán dentro de la máquina virtual
proporcionada por un servidor de aplicaciones: más adelante se abordará la
implementación y funcionamiento de los mismos.
El
esquema propuesto por J2EE para aplicaciones distribuidas es el de la Figura
2: el servidor de aplicaciones proporciona una gran cantidad de servicios, como
acceso a bases de datos, servidores de correo, etc. Para implementar la
funcionalidad del negocio simplemente se crearán una serie de Enterprise Beans
(en rojo en la figura), que serán cargados por el servidor de aplicaciones y
podrán acceder a los servicios proporcionados por el mismo, incluyendo acceso a
bases de datos, etc.
Por
otra parte, cualquier aplicación que quiera acceder a la funcionalidad del
negocio podrá acceder transparentemente a los métodos de los beans, sin ni tan
siquiera advertir que estos se ejecutan dentro del servidor de aplicación. Como
veremos más adelante, será posible acceder a esta funcionalidad no solo desde
aplicaciones Java, sino desde cualquier aplicación CORBA, por lo que la
solución proporcionada por J2EE será universalmente accesible. De hecho, como
también veremos más adelante, J2EE va más allá, y proporciona soporte para el
acceso a la funcionalidad del negocio desde páginas HTML (Figura 3).
Figura
2. Interrelación entre
aplicaciones cliente (en verde), código de negocio (en rojo), servidor de
aplicación y otros elementos de una aplicación distribuida basada en J2EE.
Una
aplicación distribuida de cierto tamaño, además de dar respuesta a las necesidades
concretas para la que ha sido diseñada, necesitará enfrentarse a y resolver
toda una serie de cuestiones técnicas que contribuyen significativamente a
aumentar la dificultad del desarrollo: ente estas están el soporte para distribución
de objetos, la necesidad de guardar y recuperar objetos o persistencia
(típicamente utilizando una Base de Datos), el soporte para concurrencia
y seguridad, el soporte para transacciones, y el soporte para
poder encontrar objetos o recursos distribuidos (p.ej., para encontrar una
impresora color, o para poder encontrar a los Clientes, incluso aunque se cambie el
servidor donde residen).
Evidentemente,
cualquier sistema distribuido requerirá algún tipo de mecanismo para llevar a cabo
la comunicación entre objetos en distintas máquinas. Dentro de la arquitectura
J2EE el hecho de que los objetos sean distribuidos resulta bastante
transparente, y el código fuente raramente tendrá que tener en cuenta esto: si
el programador hubiese de codificar el mecanismo de distribución, esto haría el
desarrollo prácticamente imposible.
Dentro
de J2EE el soporte para la distribución se basa en la API RMI de Java (Remote
Method Invocation), implementada sobre IIOP (RMI-IIOP). IIOP (Internet
Inter-ORB Protocol) es un protocolo que permite conseguir interoperabilidad
entre productos CORBA: esto permite que los EJBs sean accesibles desde
cualquier otro componente CORBA, implementado en C++ o cualquier otro lenguaje,
lo que hace a nuestra lógica de negocio universalmente accesible, en lugar de
quedar limitada a aplicaciones Java.
El
hecho de utilizar RMI-IIOP puede permitir también que sea posible acceder desde
cualquiera de nuestros Enterprise Beans a productos CORBA: sin embargo, puede
darse el caso de que esto no sea así, y Sun recomienda usar JavaIDL para este
propósito, también soportado por la plataforma J2EE.
Un
elemento importante de cualquier sistema informático es el almacenamiento y
recuperación de la información: la información sobre clientes, proveedores,
productos, facturas y muchos otros objetos debe poder guardarse y recuperarse
según convenga, y por tanto uno de los elementos importantes dentro de la
plataforma J2EE es el soporte para guardar y recuperar objetos (a la capacidad de
un objeto para guardarse/leerse de un dispositivo de almacenamiento se le llama
persistencia).
El
soporte de persistencia dentro de J2EE se puede llevar a cabo de varios modos.
En primer lugar, algunos servidores de aplicación proporcionan soporte de modo
más o menos automático: por ejemplo, algunos proporcionan herramientas que
permiten especificar que un EJB se guarda como un registro de una tabla
determinada, indicando a qué columna del registro va cada campo del objeto.
También
es posible hacer que sea el propio objeto el que se responsabilice de su
persistencia: para esto se podría utilizar por ejemplo la API JDBC, que
encapsula el acceso a bases de datos relacionales de diversos fabricantes, y
también forma parte de J2EE.
Con
frecuencia es necesario limitar el acceso a partes sensibles de un sistema, y
la plataforma J2EE permite hacer esto de forma excepcionalmente sencilla.
Para
cada método de un Enterprise Bean es posible especificar qué roles
tienen acceso a dicho método: por ejemplo, todas las personas en el rol de
Vendedor más todas las personas en el rol de Administrador pueden tener acceso
al método que devuelve las facturas de un cliente, getFacturas. Cada vez que se llama a un
método de un EJB se verifica si la persona que llama a dicho método tiene
alguno de los roles autorizados, de modo que si no es así se elevará una
excepción.
Para
indicar los roles que tienen permiso para acceder a cada método no es necesario
escribir ningún código fuente, sino que esto se especifica en un archivo en
formato XML, llamado descriptor de despliegue (deployment descriptor),
el cuál se utiliza para esta y otras tareas similares: es de aquí de donde el
servidor de aplicaciones obtendrá la información para realizar los chequeos de
permiso de acceso a cada método.
Nótese
que este sistema de roles es muy sencillo de utilizar. Cuando una aplicación se
distribuya, la persona encargada de instalarla en un servidor de aplicación
tendrá simplemente que decidir qué personas tienen cada rol, normalmente
utilizando una herramienta visual. Si en lugar de trabajar a nivel de roles se
trabajara a nivel de usuario, la persona encargada de la instalación tendría
que decidir para cada método qué usuarios tienen permisos, lo que es mucho más
trabajoso, complicado y confuso.
Otro
aspecto importante de la seguridad es el de la transmisión segura de
información, de modo que datos sensibles tales como un número de Visa no sean
accesibles a cualquier persona que pueda llegar a interceptar la comunicación
(algo relativamente fácil en Internet), lo que normalmente se puede conseguir
mediante encriptación. La especificación actual, EJB 1.1, no indica nada al
respecto: sin embargo, la mayor parte de los servidores de aplicación soportan
comunicaciones seguras, típicamente a través de SSL (Secure Sockets Layer).
Por
último, un aspecto importante de la seguridad es la autentificación, que
permite garantizar que un usuario es quien dice ser, lo que típicamente se
consigue pidiendo una palabra de paso (password). La especificación de
J2EE no dice nada al respecto, dejando a cada servidor de aplicación libre para
proporcionar esta funcionalidad como prefiera.
Si
nuestra aplicación está destinada a la venta de productos a través de Internet,
una vez que dispongamos de todos los productos solicitados por un cliente en el
almacén querremos generar una orden de envío, actualizar las existencias para
reflejar que los productos especificados saldrán del almacén, y por último
cargar la Visa del cliente. Nótese que estas tres operaciones deben tener éxito
a la vez, o bien ninguna de ellas ha de llevarse a cabo: queremos que sean atómicas,
o de lo contrario nos podríamos encontrar con que hemos cargado la Visa del
cliente, pero no se ha generado la orden de envío (quizá porque en ese momento
ha caído la máquina en la que se encuentra la base de datos con los envíos). Al
cliente nunca se le enviarán los productos por los que se le cargó la Visa,
algo que no le hará muy feliz.
En
aplicaciones de pequeña escala normalmente bastará con usar el soporte para
transacciones que suelen ofrecer casi todas las bases de datos, pero en
aplicaciones de cierto tamaño esto no es posible, por el mero hecho de que
numerosos sistemas pueden formar parte de la operación, siendo necesario
coordinarlos todos. De hecho, la mejor forma de hacer esto es dejar que el
fabricante del servidor de aplicaciones se entienda con el fabricante de los
distintos productos, y que sea el propio servidor de aplicaciones el que se encargue
de coordinar a todos los participantes en la operación, tanto si esta tiene
éxito como si no, y esta es la política adoptada en la plataforma J2EE.
Por
supuesto, todavía es responsabilidad del programador indicar que un método debe
ejecutarse dentro de una transacción, pero ni siquiera es necesario que escriba
código para hacerlo: bastará con indicar en el archivo descriptor de despliegue
que dicho método necesita ser ejecutado dentro de una transacción, y el
servidor de aplicaciones se encargará de comenzar una cuando se vaya a ejecutar
dicho método, si es que no hay ya una transacción activa: más adelante
estudiaremos cómo consigue el servidor de aplicaciones llevar a cabo todas
estas acciones de forma transparente.
Por
supuesto, aunque es posible y recomendable delegar en el servidor de
aplicaciones para que sea él el que se encargue de manejar las transacciones a
partir de la información proporcionada en el descriptor de despliegue, existe
la posibilidad de que el programador acceda directamente a la API de manejo de
transacciones, llamada JTA (Java Transaction API).
El uso
de un sistema distribuido implica que numerosos usuarios pueden estar
accediendo a la información (objetos) a la vez, lo que plantea un problema. La
solución puede ser complicada, por lo que es una vez más el servidor de
aplicaciones el que se hace cargo del mismo: por defecto el servidor no
permitirá acceder a los métodos de un objeto más que a un único hilo de control
(thread), de modo que hasta que un cliente no haya terminado de ejecutar
el método no será posible para otro cliente ejecutar ningún otro método de
dicho objeto. Es más, si el método forma parte de una transacción entonces los
métodos del objeto no serán accesible por otro cliente hasta que no se acabe la
transacción.
Para
evitar posibles problemas un EJB no puede crear hilos de control en ninguno de
sus métodos. Además, está prohibido definir un método como synchronized, algo que no tiene sentido
porque no es el usuario el responsable de proporcionar soporte para
concurrencia, sino el servidor de aplicaciones. Adicionalmente, el servidor de
aplicación puede detectar casos en los que se podría producir un bloqueo, y
elevará una excepción en ese caso.
Un elemento
fundamental a la hora de trabajar en entornos distribuidos es la posibilidad de
encontrar el objeto deseado, no importa dónde resida, para lo que típicamente
se utiliza un Servicio de Nombres (Naming Service).
Un
servicio de nombres permite asociar un nombre a uno o más objetos o recursos,
de modo que después será posible acceder a dichos objetos utilizando dicho
nombre, incluso aunque hayan sido trasladados a otra máquina. Hay servicios de
nombres que proporcionan información que describe el objeto asociado a un
nombre. Esto puede ser muy útil: por ejemplo, podría permitir encontrar una o
más impresoras color sin necesidad de saber su nombre. A estos servicios se les
llama Servicios de Directorio.
Para
trabajar con servicios de nombres y de directorio de forma portable existe una
API estándar, JNDI (Java Naming and Directory Interface), que nos
permite acceder usando el mismo interface a distintos servicios, como LDAP o
DSML.
Como se
ha podido ver hasta ahora, un servidor de aplicaciones proporciona una gran
cantidad de funcionalidad, mucha de ella de forma transparente, sin que se haya
de escribir código fuente alguno. Pero, ¿cómo es posible que esto sea así?
¿Cómo es posible, por ejemplo, que el servidor pueda chequear los permisos de
seguridad para cada método de nuestros objetos, sin que tengamos que escribir
ningún código adicional a la hora de implementar los métodos?
La
respuesta es muy sencilla: dado que nuestros Enterprise JavaBeans se ejecutan
dentro de la máquina virtual proporcionada por el servidor de aplicación, este
puede hacer muchas cosas a escondidas, y de hecho su ocupación favorita es
interponerse entre las llamadas que se hacen a los métodos de los beans y las
implementaciones de los mismos, de modo que entre otras cosas puede hacer los
chequeos necesarios para verificar si el usuario que llama al método tiene los
permisos adecuados, antes de llamarlo. El hecho de que el servidor de
aplicaciones controle totalmente la máquina virtual donde se ejecuta el
Enterprise Bean hace que le sea posible conseguir un control absoluto del mismo
y su entorno.
Por
supuesto, los detalles reales de cómo se lleva a cabo esta interposición
entre el EJB y el servidor de aplicaciones son un poco más complejos de lo que
podría dar a entender esta explicación, pero esta ilustra muy adecuadamente la
idea básica.
El uso
de HTML para implementar el interface de usuario es una alternativa muy
interesante, e incluso en algunos casos la única posible.
Las
ventajas de utilizar HTML son varias. En primer lugar, no se requiere ningún
tipo de instalación en el ordenador del cliente, excepto un navegador como
Netscape Navigator o Internet Explorer. Esta es una ventaja enorme, no solo
desde el punto de vista de la disminución de los costes de mantenimiento, algo
muy importante, sino también porque hay escenarios en los que es imposible
instalar una aplicación en el ordenador del cliente: por ejemplo, ¿sería Amazon
una alternativa tan popular si todos sus usuarios tuviesen que instalar una
aplicación en nuestro ordenador?. Seguramente no, de hecho ni siquiera sería
viable.
Otra
ventaja importante es que es posible modificar la funcionalidad ofrecida al
usuario sin que sea necesario hacer ningún tipo de mantenimiento en su máquina:
basta con modificar el HTML que recibirá su navegador, con lo que los costes de
actualización serán mucho más bajos.
Una
tercera ventaja importante del HTML es que una página puede ocupar un espacio
relativamente pequeño, por comparación a otras posibles soluciones, como el uso
de applets, por lo que su descarga será relativamente rápida. Por último, hay
que tener en cuenta que las páginas HTML se transmiten mediante el protocolo
HTTP, el cuál puede pasar a través de un cortafuegos (firewall)
sin problemas, algo que no sucede con IIOP, el protocolo utilizado por los
objetos distribuidos, sobre todo si el cliente es anónimo. Los applets, por
otra parte, cuentan con unas restricciones de seguridad importantes, por lo que
pueden no ser adecuados como alternativa.
Todas
estas ventajas convierten el uso de HTML en una solución prácticamente
universal, por lo que la plataforma J2EE proporciona soporte para el mismo a
través de dos APIs: la API para Servlets, que proporciona una serie de clases que encapsulan la generación
de páginas, y Java Server Pages (JSP), que permiten incluir código Java
dentro de páginas HTML.
Por
supuesto, el uso de HTML también tiene sus desventajas: los interfaces de
usuario tienden a ser mucho menos atractivos que los de una aplicación gráfica
desarrollada a medida, por ejemplo, y la programación de HTML puede ser más
complicada en algunos casos. Sin embargo, J2EE permite el uso de Enterprise
Beans desde páginas HTML, lo que resulta fundamental para encapsular
adecuadamente las reglas del negocio.
Como
complemento, J2EE proporciona una API para el envío de correo electrónico, JavaMail,
lo que permite el uso del mismo interface para acceder a distintos servidores
de correo.
Uno de
los objetivos de J2EE es conseguir que todo servidor de aplicaciones
proporcione un número mínimo de servicios y protocolos, independientemente de
que un servidor proporcione otros servicios adicionales. Estos servicios
mínimos son:
·
Servlets
2.2, Java Server Pages (JSP) 1.1, y posibilidad de servir peticiones HTTP 1.0 y
HTTPS (HTTP seguro) sobre SSL 3.0.
·
Enterprise
JavaBeans 1.1.
·
Java
Transaction API 1.0 (JTA).
·
Java
Messaging Services 1.0 (JMS), que proporciona soporte para diversos sistemas de
mensajería, como MQSeries. El servidor debe proporcionar el interface, pero no
es obligatorio proporcionar una implementación.
·
Java
Database Connectivity 2.0 (JDBC). No es obligatorio sin embargo soportar el Optional Package.
·
Java
Naming and Directory Interface 1.2 (JNDI).
·
JavaMail
1.1: está garantizado que se podrá enviar correo, pero no que se podrá
recibirlo.
·
Java
Activation Framework 1.0.
·
JavaIDL.
Tanto los Enterprise Beans y como los componentes Web deben ser capaces
de acceder a servicios CORBA externos a través de JavaIDL, lo que permite
garantizar que las aplicaciones cliente del servidor de aplicaciones tienen
conectividad con servicios CORBA externos al servidor.
·
RMI-IIOP
1.0: se debe proporcionar protocolo de invocación remota con la semántica de
RMI-IIOP.
J2EE
proporciona algunos servicios adicionales que aún no hemos estudiado,
incluyendo el servicio de mensajería (Java Messaging Services, JMS) o Java
Activation Framework (JAF); abordaremos estos servicios en artículos
posteriores.
Como se puede ver, J2EE proporciona una solución global,
estandarizada y relativamente sencilla al desarrollo de aplicaciones
corporativas. La funcionalidad del negocio se implementará típicamente mediante
Enterprise Java Beans, clases Java que implementan unos interfaces estándar,
cuyos métodos pueden ser utilizados directamente por el cliente sin que este
sepa que entre él y los beans se encuentra el servidor de aplicaciones. El
servidor de aplicaciones llevará a cabo tareas tan fundamentales como el
control de seguridad, concurrencia, etc., proporcionando además una serie de
interfaces estándar para el acceso a bases de datos (JDBC), acceso a servicios
de directorio, uso de correo electrónico, etc., lo que permite acceder de forma
transparente a productos de diferentes fabricantes y todo tipo de funcionalidad
sin que quedemos atrapados por la necesidad de utilizar una API propietaria, lo
que hace a la solución global mucho más abierta y adaptable.
Por lo que respecta al uso de los Enterprise Beans por las
aplicaciones de usuario final (clientes, en verde en la Figura 3), J2EE
proporciona numerosas alternativas, incluyendo la posibilidad de usar applets,
aplicaciones a medida o incluso HTML o XML. Nótese que el protocolo utilizado
por los beans hace posible que las aplicaciones cliente estén escritas en
cualquier lenguaje, al ser compatible con CORBA.
Para soportar el desarrollo de HTML/XML la especificación
J2EE proporciona un par de APIs adicionales, Servlets y Java Server Pages, que
hacen al código utilizado para generarlos portable entre diversas plataformas.
Por tanto, junto con los Enterprise Beans también será posible desarrollar
servlets y JSPs que se cargarán en el servidor de aplicaciones: todos ellos
aparecen en rojo en la Figura 3.
Figura 3.
Interacción entre aplicaciones cliente/applets/navegador, Enterprise Beans y
Servlets/JSPs, servidor de aplicaciones y servicios externos
Desde nuestro punto de vista, la plataforma J2EE es sin duda
la mejor plataforma que nunca se haya diseñado para la creación de aplicaciones
corporativas de gran escala, por los siguientes motivos:
·
La funcionalidad del negocio está escrita en Java, un
lenguaje portable, potente y orientado a objeto.
·
La funcionalidad del negocio es accesible a cualquier
producto CORBA, lo que permite que aplicaciones escritas en C++ o cualquier
otro lenguaje que no sea Java puedan acceder a la misma. Esto equivale a decir
que la funcionalidad del negocio es universalmente accesible.
·
Gran parte de los servicios requeridos para implementar
la funcionalidad del negocio son accesible a través de APIs estándar, en lugar
de APIs propietarias, lo que permite evitar caer completamente en las manos de
un fabricante.
·
Gran parte de la funcionalidad es proporcionada por el
Servidor de Aplicaciones, lo que hace el desarrollo mucho más fácil: piénsese
en la sencillez con que está implementado el soporte de seguridad o el soporte
para transacciones.