Durante la conferencia Google I/O que ha comenzado hoy en San Francisco, Google ha presentado su nuevo entorno de desarrollo para Android: Android Studio. Éste sustituye a Eclipse y está basado en IntelliJ IDEA, lo cual es (al menos para mí) tan inesperado como excitante.
Para los más despistadillos, o aquellos que no hayáis trabajado con IntelliJ, éste es el mejor entorno desarrollo para Java que existe en la actualidad, a años luz en estabilidad y comportamiento con respecto al resto de alternativas (Eclipse / Netbeans) y muy cerca del excelente Visual Studio de Microsoft (aunque éste juegue en la liga .NET...).
Aunque las últimas versiones de IntelliJ ya disponían de un diseñador integrado de Android, Android Studio es una versión de éste creada por y para esta plataforma. La guinda al pastel sería que Android Studio estuviera basado en la versión Community Edition de IntelliJ, con lo que estaríamos hablando de un producto gratuito y (probablemente) de código abierto. Sea como sea, se trata sin duda de una excelente noticia para la comunidad de desarrolladores Android.
EDICIÓN: Ya es...
Durante la conferencia Google I/O que ha comenzado hoy en San Francisco, Google ha presentado su nuevo entorno de desarrollo para Android: Android Studio. Éste sustituye a Eclipse y está basado en IntelliJ IDEA, lo cual es (al menos para mí) tan inesperado como excitante.
Para los más despistadillos, o aquellos que no hayáis trabajado con IntelliJ, éste es el mejor entorno desarrollo para Java que existe en la actualidad, a años luz en estabilidad y comportamiento con respecto al resto de alternativas (Eclipse / Netbeans) y muy cerca del excelente Visual Studio de Microsoft (aunque éste juegue en la liga .NET...).
Aunque las últimas versiones de IntelliJ ya disponían de un diseñador integrado de Android, Android Studio es una versión de éste creada por y para esta plataforma. La guinda al pastel sería que Android Studio estuviera basado en la versión Community Edition de IntelliJ, con lo que estaríamos hablando de un producto gratuito y (probablemente) de código abierto. Sea como sea, se trata sin duda de una excelente noticia para la comunidad de desarrolladores Android.
EDICIÓN: Ya es posible descargar desde aquí una early access preview para sistemas Windows, Mac y Linux, la cual puede contener características en desarrollo, sin implementar o con bugs.
Hace ya más de un año que activé una cuenta en Google+, aunque hasta ahora ha estado offline ya que realmente no sabía muy bien que uso debía darle, ni que ventajas podía ofrecerme.
Aprovechando que estoy preparando multitud de contenidos que publicaré en las próximas semanas he decido reactivar la cuenta y enfocarla a un uso netamente profesional, de manera que pueda ampliar mi ecosistema de comunicación del que ya forman parte mi cuenta en Twitter y mi canal RSS.
De manera adicional, Google+ puede ayudarme a mantener una comunicación más cercana con todos aquellos que me seguís, además de poner de manifiesto el verdadero significado de red social haciendo que la comunicación entre vosotros sea también posible.
El tratamiento de excepciones en Java es un mecanismo del lenguaje que permite gestionar errores y situaciones excepcionales. Debido a que el tratamiento de excepciones es uno de los pilares fundamentales del lenguaje, todo programador sabe como lanzarlas y capturarlas, pero (como cualquier aspecto fundamental en programación) es necesario conocer con cierta profundidad el proposito por el cual tenemos disponible dicho mecanismo, además de hacer un uso correcto de esta funcionalidad. En este artículo vamos a ver ambos aspectos: el qué y el cómo.
1. CONCEPTO DE EXCEPCIÓN
Una excepción en Java (así como en otros muchos lenguajes de programación) es un error o situación excepcional que se produce durante la ejecución de un programa. Algunos ejemplos de errores y situaciones excepcionales son:
- Leer un fichero que no existe.
- Acceder al valor N de una colección que contiene menos de N elementos.
- Enviar/recibir información por red mientras se produce una perdida de conectividad.
Todas las excepciones en...
El tratamiento de excepciones en Java es un mecanismo del lenguaje que permite gestionar errores y situaciones excepcionales. Debido a que el tratamiento de excepciones es uno de los pilares fundamentales del lenguaje, todo programador sabe como lanzarlas y capturarlas, pero (como cualquier aspecto fundamental en programación) es necesario conocer con cierta profundidad el proposito por el cual tenemos disponible dicho mecanismo, además de hacer un uso correcto de esta funcionalidad. En este artículo vamos a ver ambos aspectos: el qué y el cómo.
1. CONCEPTO DE EXCEPCIÓN
Una excepción en Java (así como en otros muchos lenguajes de programación) es un error o situación excepcional que se produce durante la ejecución de un programa. Algunos ejemplos de errores y situaciones excepcionales son:
- Leer un fichero que no existe.
- Acceder al valor N de una colección que contiene menos de N elementos.
- Enviar/recibir información por red mientras se produce una perdida de conectividad.
Todas las excepciones en Java se representan, como vamos a ver en la siguiente sección, a través de objetos que heredan, en última instancia, de la clase java.lang.Throwable.
2. TIPOS DE EXCEPCIONES
El lenguaje Java diferencia claramente entre tres tipos de excepciones: errores, comprobadas (en adelante checked) y no comprobadas (en adelante unchecked). El gráfico que se se muestra a continuación muestra el árbol de herencia de las excepciones en Java (se omite el paquete de todas las que aparecen, que es java.lang):
La clase principal de la cual heredan todas las excepciones Java es Throwable. De ella nacen dos ramas: Error y Exception. La primera representa errores de una magnitud tal que una aplicación nunca debería intentar realizar nada con ellos (como errores de la JVM, desbordamientos de buffer, etc) y que por tanto no tienen cabida en este artículo. La segunda rama, encabezada por Exception, representa aquellos errores que normalmente si solemos gestionar, y a los que comunmente solemos llamar excepciones.
De Exception nacen múltiples ramas: ClassNotFoundException, IOException, ParseException, SQLException y otras muchas, todas ellas de tipo checked. La única excepción (valga la redundancia) es RuntimeException que es de tipo unchecked y encabeza todas las de este tipo.
A pesar de que la diferencia entre las excepciones de tipo checked y unchecked es muy imporante, es también a menudo uno de los aspectos menos entendidos dentro del tratamiento de excepciones. Veamos cada una de ellas con un poco más de detalle.
3. EXCEPCIONES CHECKED
Una excepción de tipo checked representa un error del cual técnicamente podemos recuperarnos. Por ejemplo, una operación de lectura/escritura en disco puede fallar porque el fichero no exista, porque este se encuentre bloqueado por otra aplicación, etc. Todos estas situaciones, además de ser inherentes al propósito del código que las lanza (lectura/escritura en disco) son totalmente ajenas al propio código, y deben ser (y de hecho son) declaradas y manejadas mediante excepciones de tipo checked y sus mecanismos de control.
En ciertos momentos, a pesar de la promesa de recuperabilidad, nuestro código no estará preparado para gestionar la situación de error, o simplemente no será su responsabilidad. En estos casos lo más razonable relanzar la excepción y confiar en que un método superior en la cadena de llamadas sepa gestionarla.
Por tanto, todas las excepciones de tipo checked deben ser capturadas o relanzadas. En el primer caso, utilizamos el más que conocido bloque try-catch:
public class Main {
public static void main(String[] args) {
FileWriter fichero;
try {
// Las siguientes dos líneas pueden lanzar una excepción de tipo IOException
fichero = new FileWriter("ruta");
fichero.write("Esto se escribirá en el fichero");
} catch (IOException ioex) {
// Aquí capturamos cualquier excepción IOException que se lance (incluidas sus subclases)
ioex.printStackTrace();
}
}
}
En caso de querer relanzar la excepción, debemos declarar dicha intención en la firma del método que contiene las sentencias que lanzan la excepción, y lo hacemos mediante la clausula throws:
public class Main {
// En lugar de capturar una posible excepción, la relanzamos
public static void main(String[] args) throws IOException {
FileWriter fichero = new FileWriter("ruta");
fichero.write("Esto se escribirá en el fichero");
}
}
Hay que tener presente que cuando se relanza una excepción estamos forzando al código cliente de nuestro método a capturarla o relanzarla. Una excepción que sea relanzada una y otra vez hacia arriba terminará llegando al método primigenio y, en caso de no ser capturada por éste, producirá la finalización de su hilo de ejecución (thread).
La dos preguntas que debemos hacernos en este momento es: ¿Cuándo capturar una excepción? ¿Cuándo relanzarla? La respuesta es muy simple. Capturamos una excepción cuando:
- Podemos recuperarnos del error y continuar con la ejecución.
- Queremos registrar el error.
- Queremos relanzar el error con un tipo de excepción distinto.
En definitiva, cuando tenemos que realizar algún tratamiento del propio error. Por contra, relanzamos una excepción cuando:
- No es competencia nuestra ningún tratamiento de ningún tipo sobre el error que se ha producido.
4. EXCEPCIONES UNCHECKED
Una excepción de tipo unchecked representa un error de programación. Uno de los ejemplos más tipicos es el de intentar leer en un array de N elementos un elemento que se encuentra en una posición mayor que N:
int[] numerosPrimos = {1, 3, 5, 7, 9, 11, 13, 17, 19, 23}; // Array de diez elementos
int undecimoPrimo = numerosPrimos[10]; // Accedemos al undécimo elemento mediante el literal numérico 10
El código anterior accede a una posición inexistente dentro del array, y su ejecución lanzará la excepción unchecked ArrayIndexOutOfBoundsException (excepción de índice de array fuera de límite). Esto es claramente un error de programación, ya que el código debería haber comprobado el tamaño del array antes de intentar acceder a una posición concreta:
if(numerosPrimos.length > indiceUndecimoPrimo) {
System.out.println("El índice proporcionado (" + indiceUndecimoPrimo + ") es mayor que el tamaño del array (" + numerosPrimos.length + ")");
} else {
int undecimoPrimo = numerosPrimos[indiceUndecimoPrimo];
// ...
}
El código anterior no sólo valida el tamaño de la colección antes de acceder a una posición concreta (el proposito fundamental del ejemplo), sino que evita el uso de literales numéricos asignando el indice del array a una variable bien nombrada (tal como se explicó en este artículo).
El aspecto más destacado de las excepciones de tipo unchecked es que no deben ser forzosamente declaradas ni capturadas (en otras palabras, no son comprobadas). Por ello no son necesarios bloques try-catch ni declarar formalmente en la firma del método el lanzamiento de excepciones de este tipo. Ésto, por supuesto, también afecta a métodos y/o clases más hacia arriba en la cadena invocante.
5. CREANDO NUESTRAS PROPIAS EXCEPCIONES
Aprovechando dos de las características más importantes de Java, la herencia y el polimorfismo, podemos crear nuestras propias excepciones de forma muy simple:
class CreditoInsuficienteException extends Exception {
// ...
}
La clase del código anterior extiende a Exception y por tanto representa una excepción de tipo checked. Tal como su nombre indica, sería lanzada cuando durante una operación comercial no exista suficiente crédito. Esta situación excepcional es inherente a nuestra transacción comercial y no se genera por un defecto de código (no es un error de programación), y por tanto debe gestionarse con anterioridad:
class CarritoDeLaCompra {
// ...
public void pagarCompraConTarjeta() {
try {
TarjetaDeCredito tarjetaPreferida = cliente.getTarjetaDeCreditoPreferida();
tarjetaPreferida.realizarPago(getImporteCompra());
cliente.enviarEmailConfirmación();
} catch(CreditoInsuficienteException ciex) {
// Informamos al usuario de crédito insuficiente
}
}
}
Si por el contrario deseamos crear una excepción de tipo unchecked, debemos hacer que nuestra clase extienda (como no) de RuntimeException. Volviendo al último ejemplo, podríamos pensar que CreditoInsuficienteException podría ser declarada como una excepción de tipo unchecked, ya que siempre es posible validar el saldo de la tarjeta de credito con anterioridad al pago (como haciamos con los índices del array). Sin embargo esto no siempre es posible ni razonable ya que:
- No disponemos del código fuente, sólo somos clientes de una librería escrita por terceros.
- Aunque dispusieramos del código fuente, no deberíamos estar autorizados a conocer el credito disponible de ningún cliente (esto es información muy sensible y por tanto, confidencial).
Antes de escribir tus propias excepciones, piensa detenidamente en que grupo encaja mejor su responsabilidad (checked vs unchecked). Programa siempre de forma inteligente.
6. MALAS PRÁCTICAS DE USO
Para convertirnos en maestros de las excepciones, debemos evitar el uso de aquellas malas practicas que se han generalizado a los largo de los años (y por supuesto no inventar las nuestras...). La primera que vamos a ver es la más peligrosa y, a pesar de ello, también la más común:
El código anterior ignorará cualquier excepción que se lance dentro del bloque try, o mejor dicho, capturará toda excepción lanzada dentro del bloque try pero la silenciará no haciendo nada (frustrando así el principal propósito de la gestión de excepciones checked: gestiónala o relánzala). Cualquier error de diseño, de programación o de funcionamiento en esas lineas de código pasará inadvertido tanto para el programador como para el usuario. Lo mínimamente aceptable dentro de un bloque catch es un mensaje de log informando del error:
try {
// Código que declara lanzar excepciónes
} catch(Exception ex) {
logging.log("Se ha producido el siguiente error: " + ex.getMessage());
logging.log("Se continua la ejecución");
}
Algo más razonable sería pintar una traza completa del error mediante uno de los métodos informativos de Throwable:
try {
// Código que declara lanzar excepciónes
} catch(Excepcion ex) {
ex.printStackTrace(); // Podemos añadir cualquier tratamiento adicional antes y/o después de esta línea
}
Otro abuso del mecanismo de tratamiento de excepciones es cuando se está intentando escribir código que mejore el rendimiento de la aplicación:
try {
int i = 0;
while(true) {
System.out.println(numerosPrimos[i++]);
}
} catch(ArrayIndexOutOfBoundsException aioobex) {
}
El ejemplo anterior itera nuestro fabuloso array de números primos sin preocuparse de los límites del array (tal como haría de manera formal un bucle for) hasta sobrepasar el índice máximo, momento en el cual se lanzará una excepción de tipo ArrayIndexOutOfBoundsException que será capturada y silenciada. Esto es un error porque:
- El tratamiento de excepciones está diseñado para gestionar excepciones y no para realizar optimizaciones.
- El código dentro de bloques try-catch no dispone de ciertas optimizaciones de las JVM más modernas (por ejemplo, y aplicable a nuestro caso, iteración de colecciones).
- El resultado es estéticamente horrible.
Otro error común se produce cuando estamos creando nuestra propia librería de excepciones y nos excedemos declarando excepciones checked. Las excepciones checked son fabulosas ya que, al contrario que los códigos return de lenguajes como C, fuerzan al programador a manejar condiciones excepcionales, mejorando así la legibilidad del código. Sin embargo, esta obligación puede llegar a cargar el código cliente:
El código anterior suele abrumar, y el cliente acabará tentado por la siguiente alternativa:
try {
// Código que declara lanzar muchas excepciónes
} catch(Exception ex) {
// Gestionar cualquier excepcion, pues todas heredan de Exception
// Perdemos la ventaja de gestionar situaciones excepcionales
}
Por ello, debes pensar detenidamente si tu excepción es de tipo checked o unchecked. Recuerda, cualquier situación excepcional que deje la aplicación en un estado irrecuperable y/o no sea inherente al proposito del código que la produce debe ser declarada como una excepción de tipo unchecked.
La siguiente mala práctica que vamos a ver está intimamente relacionada con la anterior, y es la de lanzar excepciones de forma generica:
public void miMetodo() throws Exception {
// Código que declara lanzar muchas excepciones.
// Sin embargo, en la firma del método declaramos lanzar una única super-clase de todas éllas
}
Nunca hagas lo anterior. Se que lo has hecho. Yo reconozco haberlo hecho también en el pasado. Y es un absoluto ERROR. Los clientes de tu método no sabrán jamás con que condiciones especiales se pueden encontrar, y por tanto no podrán gestionarlas; no tendrán más remedio que informar del error y detener la ejecución.
Por último (y se que con mis palabras voy a crear polémica) existe una técnica para convertir toda excepción checked en unchecked:
public void noLanzoExcepcionesChecked() {
try {
// Código que lanza una o más excepciones de tipo checked
} catch(Exception ex) {
throw new RuntimeException("Se ha producido una excepción con el mensaje: " + ex.getMessage(), ex);
}
}
El método del código anterior convierte cualquier excepción de tipo checked en una excepción de tipo unchecked, de manera que ningún cliente suyo esté forzado a declarar/gestionar ninguna de ellas. Decía antes que con mis palabras iba a crear polémica porque en los últimos años ha crecido una comunidad de usuarios Java que abogan por eliminar el sistema de excepciones checked, que es justo lo que hace el código anterior. Si alguno de estos usuarios me lee, probablemente discrepe con mi idea de incluir dicho código (o cualquiera de sus variantes con menos ámbito de alcance y por tanto menos agresiva) dentro de lo que yo considero malas prácticas. Donde, y me incluyo en este grupo, unos vemos ventajas sobre el tratamiento de excepciones de tipo checked (porque nos da control sobre los errores que se producen a través de estructuras basadas en código legible y con un proposito claro), ellos ven desventajas (sobrecarga del lenguaje y en última instancia del código con una funcionalidad que, y en esto estoy de acuerdo, no es absolutamente perfecta). Existe actualmente un debate abierto sobre la verdadera utilidad de las excepciones checked. Tal vez en próximas versiones de Java (o en el próximo gran lenguaje orientado a objetos) se elimine cualquier concepto actual de error comprobado, pero en el momento actual no es así.
Personalmente considero el ejemplo anterior no un abuso del lenguaje, si no un verdadero mal uso. La razón es todavía peor a la del penúltimo ejemplo (¡aquel que te dije no hacer nunca!): no sólo estamos aniquilando toda posibilidad de un tratamiendo de excepciones que sea razonablemente útil, sino que lo más probable es que nuestra nueva y flamante excepción uncheked vaya subiendo hacia arriba en la cadena de llamadas y termine deteniendo el hilo de ejecución actual (puesto que, como ya sabes, éstas no tienen porqué ser gestionadas de forma obligatoria). A efectos prácticos, la única verdadera posibilidad de que sea capturada es que alguien haya declarado un bloque catch que declare capturar Exception (lo cual ya hemos dicho que es otro mal uso):
class MiClaseCliente() {
public void miMetodoCliente() {
try {
obj.noLanzoExcepcionesChecked();
} catch(Exception ex) {
// Bloque catch extremadamente genérico (mal uso)
// Capturamos RuntimeException porque hereda de Exception (¿casualidad?)
// El bloque try debe lanzar al menos una excepción checked (o este bloque es ilegal)
// ¿Cuantos niveles estoy por encima del origen de la excepción convertida?
// ¿Y si no existiera ningún bloque como este? Detención del thread (podría haberse evitado)
}
}
}
Existen ciertas situaciones en las que la conversión de una o más excepciones de tipo checked en unchecked es útil o práctica, pero son situaciones tan concretas y a menudo tan complejas y potencialmente peligrosas que no voy entrar en ningún tipo de detalle sobre éllas. Mi consejo final es: ¡NO CONVIERTAS EXCEPCIONES CHECKED EN UNCHECKED! (salvo que realmente sepas lo que haces).
7. RECOMENDACIONES DE USO
Hay algunos principios de uso que debemos ver desde la perspectiva del haz esto, en lugar del no hagas esto. Esto, que además de ser un pensamiento más positivo, me permite a mi añadir una sección más al artículo (:P) y a ti presumir de conocer tanto las malas como las buenas prácticas del uso de excepciones. Ahí es nada.
Un buen uso del tratamiento de excepciones es usar excepciones que ya existen, en lugar de crear las tuyas propias, siempre que ambas fueran a cumplir el mismo cometido (que es básicamente informar y, en caso de las checked, obligar a gestionar). Se suelen usar excepciones que ya existen cuando se dispone de un profundo conociento del API que se está usando (en otras palabras, experiencia). Si un argumento pasado a uno de tus métodos no es del tipo esperado, o no tiene el formato correcto, lanza una excepción IllegarArgumentException en lugar de crear tu propia excepción. Esto es bueno porque:
- Uno de los pilares de Java es la reutilización de código (no reinventes la rueda).
- Tu código es más universal (FormatoInvalidoException puede no significar nada para un germanoparlante).
Otra recomendación que no suele llevarse a cabo nunca o casi nunca es la de lanzar excepciónes acordes al nivel de abtracción en el que nos encontramos. Imaginemos una seríe de clases que actuan como capas, una encima de otra (cuanto más arriba más abstracta, cuanto más abajo más concreta). Cuando se produce un error en las capas más bajas y éste se propaga hacia arriba, llega un momento en que dicho error representando un error muy concreto se encuentra en un contexto muy abstracto. Esto tiene básicamente tres problemas: el primero, que puede ser importante, es que estamos contaminando el API de las capas superiores con suciedad de las inferiores. El segundo, que es importante, es que estamos desvelando detalles de nuestra implementación muchos niveles por encima de lo deseable. El último problema, que es importante y puede ser critico<, es que si en el futuro deseamos intercambiar una de las capas más concretas y esta ha cambiado su implementación, todas las capas por encima se romperán. Por tanto, debemos lanzar excepciones apropiadas a la abstracción en la que nos encontramos:
try {
// Código que declara lanzar excepciones de bajo nivel
} catch(BajoNivelException bnex) {
throw new AltoNivelException("Mensaje");
}
Por último, y para terminar con esta sección y (casi) con este artículo, debes documentar adecuadamente las excepciones que lanza tu código. Para ello, detalla en tus Javadoc todas las excepciones que lanzan tus métodos, informando que condiciones van a provocar el lanzamiento de cada una de ellas:
/**
* @author David Marco
* @throws MiExcepcion se lanza en caso de producirse [...] condición especial.
*/
public class MiClase {
// ...
}
8. RESUMEN
Como con otros temas tratados con anterioridad, el tratamiento de excepciones en Java podría merecer perfectamente un volumen entero de una enciclopedia sobre programación. En este artículo he intentado exponer los que considero más importantes, y algunos que considero tan útiles como (por desgracia) desconocidos.
Como siempre os invito a poneros en contacto conmigo con cualquier tema relacionado con mis artículos, como sugerencias, correcciones y por supuesto críticas. También como siempre os doy las gracias por el apoyo y agradecimiento que me brindáis constantemente a través del correo electrónico.
Desde este momento, los tutoriales en versión PDF de Spring MVC e Hibernate, así como el proyecto para eclipse del primero, se mueven desde el servicio de almacenamiento externo 4Shared a Dropbox. Este último, además de ofrecer descargas simultaneas, mantener la misma url al subir nuevas versiones y carecer de tiempo de espera, requiere menos mantenimiento por mi parte. Las nuevas direcciones son las siguientes:
Como consecuencia de este cambio, dentro de 60 días dejaré de dar soporte a mi cuenta de 4Shared, por lo que los citados archivos sólo estarán accesibles desde las nuevas direcciones.
Estoy seguro que este cambio supondrá una me...
Desde este momento, los tutoriales en versión PDF de Spring MVC e Hibernate, así como el proyecto para eclipse del primero, se mueven desde el servicio de almacenamiento externo 4Shared a Dropbox. Este último, además de ofrecer descargas simultaneas, mantener la misma url al subir nuevas versiones y carecer de tiempo de espera, requiere menos mantenimiento por mi parte. Las nuevas direcciones son las siguientes:
Como consecuencia de este cambio, dentro de 60 días dejaré de dar soporte a mi cuenta de 4Shared, por lo que los citados archivos sólo estarán accesibles desde las nuevas direcciones.
Estoy seguro que este cambio supondrá una mejora para todos, y como siempre os invito a que me enviéis vuestro feedback. Por último, recordaros que las versiones html siguen siendo accesibles desde la habitual página de tutoriales.
Una de las características que ofrece JPA (entre otros frameworks de mapeo relacional de objetos) que más puede beneficiarnos cuando la entendemos y aplicamos de forma correcta, y que más quebraderos de cabeza provoca a quienes están empezando, es Lazy Fetch. Aunque ya hemos hablado con anterioridad de forma superficial sobre Lazy Fetch durante el tutorial de JPA publicado en esta misma web, debido a la cantidad de personas que se ponen en contacto conmigo al respecto y por la satisfacción inherente de entender un poco mejor una importante caracteristica del lenguaje, he considerado interesante escribir un pequeño artículo donde se analice con cierta profundidad qué es, cómo funciona, y cómo podemos beneficiarnos (además de cómo evitar errores típicos) de Lazy Fetch . Comencemos.
LAZY FETCH, A.K.A. LECTURA DEMORADA
El termino Lazy Fetch, que traducido literalmente significa obtención perezosa (aunque yo prefiero entenderlo como lectura demorada), viene a reflejar la característica de JPA que permite que, cuando existen relaciones entre distintas entidades, las que so...
Una de las características que ofrece JPA (entre otros frameworks de mapeo relacional de objetos) que más puede beneficiarnos cuando la entendemos y aplicamos de forma correcta, y que más quebraderos de cabeza provoca a quienes están empezando, es Lazy Fetch. Aunque ya hemos hablado con anterioridad de forma superficial sobre Lazy Fetch durante el tutorial de JPA publicado en esta misma web, debido a la cantidad de personas que se ponen en contacto conmigo al respecto y por la satisfacción inherente de entender un poco mejor una importante caracteristica del lenguaje, he considerado interesante escribir un pequeño artículo donde se analice con cierta profundidad qué es, cómo funciona, y cómo podemos beneficiarnos (además de cómo evitar errores típicos) de Lazy Fetch . Comencemos.
LAZY FETCH, A.K.A. LECTURA DEMORADA
El termino Lazy Fetch, que traducido literalmente significa obtención perezosa (aunque yo prefiero entenderlo como lectura demorada), viene a reflejar la característica de JPA que permite que, cuando existen relaciones entre distintas entidades, las que son dependientes de otras no sean inicializadas con sus valores almacenados en base de datos hasta que no sean explícitamente accedidas (leidas). Toda esta verborrea se entiende mejor con un sencillo ejemplo:
@Entity
public class Factura {
// ...
}
@Entity
public class Proveedor {
// ...
@OneToMany
private List facturas;
}
En el ejemplo anterior, tenemos una entidad contenedora (Proveedor) que contiene una relación uno-a-muchos (@OneToMany) con otra entidad, Facturas a través de un objeto List. Hasta aquí todo es bastante simple, por lo que... ¡compliquémoslo!
Imaginemos que cada proveedor concreto suele tener multitud de facturas asociadas (algo bastante habitual), aunque gran parte de las consultas a la entidad Proveedor no están accediendo en ningún momento a dichas facturas; cada obtención de un proveedor desde base de datos supondría realizar diferentes subconsultas para inicializar todas sus facturas, de manera que estaríamos incurriendo en una sobrecarga innecesaria de los recursos del sistema (esto en un ejemplo de juguete como el nuestro no es apreciable ni notorio, pero en un sistema real que esté recibiendo miles o decenas de miles de peticiones por minuto (por poner una cifra) puede sobrecargar y hasta agotar los recursos de la red, procesadores, ram, base de datos, etc...).
Por este motivo, JPA permite marcar una relación entre entidades como Lazy, de manera que la inicialización desde base de datos de los objetos afectados es retrasada hasta que éstos son explicitamente requeridos por código cliente.
¿QUIÉN ES LAZY?
Por defecto, de los cuatro tipos de relación entre entidades permitidos en JPA (@OneToOne, @OneToMany, @ManyToOne y @ManyToMany) sólo dos son de tipo Lazy: @OneToMany y <@ManyToMany. Esto tiene mucho sentido, pues son estos dos tipos los que a más objetos conectan en el otro lado de la relación. Al ser el comportamiento por defecto, no debemos hacer nada para declarar estas relaciones como Lazy (es su comportamiento implícito).
El resto de relaciones no-Lazy (@ManyToOne y @OneToOne) deben ser marcadas explícitamente como Lazy si queremos que se comporten de dicha manera:
EL GRAN PROBLEMA
Una vez una entidad contenedora ha sido desconectada (dettached) del gestor de persistencia (por ejemplo al enviarla de vuelta al código cliente que la solicitó), esta se enviará tal como esté en ese momento sin importar en que estado estén sus relaciones que hayan sido marcadas como Lazy. Si una relación ha sido inicializada antes de desconectar la entidad del gestor de persistencia, podremos acceder a sus valores de forma normal; en caso contrario, la relación no apuntará a ningún objeto, y por tanto obtendremos un error al intentar manejarla:
@Stateless
public class ClaseDelLadoServidor {
// ...
public Proveedor obtenerProveedor(Long proveedorId) {
// Conectamos con BBDD y obtenemos la entidad a devolver
return proveedor; // Supongamos que en este momento la colección facturas // no ha sido inicializada aún
}
}
public class ClaseDelLadoCliente {
public void procesarFacturas(long proveedorId) {
Proveedor proveedor = claseLadoServidor.obtenerProveedor(proveedorId);
for(Factura factura : proveedor.getFacturas()) {
// Procesar cada factura
}
}
}
En el ejemplo anterior, la simple llamada a getFacturas() en la clase del lado del cliente provocaría una excepción que, dependiendo de tu proveedor de persistencia (en mi caso es EclipseLink) se parecerá más o menos a esto:
Exception in thread "main" Local Exception Stack:
Exception [EclipseLink-7242] (Eclipse Persistence Services - 2.0.1.v20100213-r6600): org.eclipse.persistence.exceptions.ValidationException
Exception Description: An attempt was made to traverse a relationship using indirection that had a null Session. This often occurs when an entity with an uninstantiated LAZY relationship is serialized and that lazy relationship is traversed after serialization. To avoid this issue, instantiate the LAZY relationship prior to serialization.
at org.eclipse.persistence.exceptions.ValidationException.instantiatingValueholderWithNullSession(ValidationException.java:979)
at org.eclipse.persistence.internal.indirection.UnitOfWorkValueHolder.instantiate(UnitOfWorkValueHolder.java:219)
at org.eclipse.persistence.internal.indirection.DatabaseValueHolder.getValue(DatabaseValueHolder.java:83)
at org.eclipse.persistence.indirection.IndirectList.buildDelegate(IndirectList.java:237)
at org.eclipse.persistence.indirection.IndirectList.getDelegate(IndirectList.java:397)
at org.eclipse.persistence.indirection.IndirectList.size(IndirectList.java:726)
at es.davidmarco.ejb.lazyloading.standaloneclient.LazyLoadingClient.main(LazyLoadingClient.java:43)
El mensaje principal que se informa en el error es el siguiente:
Ha sido realizado un intento de recorrer una relación mediante indirección que tenía una sesión null. Esto ocurre a menudo cuando una entidad que contiene una relación Lazy sin inicializar es serializada y dicha relación es recorrida después de la serialización. Para evitar este problema, inicializa la relación Lazy de forma previa a la serialización.
Los primeros de la clase se estarán preguntando: ¿cómo es posible que nuestro cliente, que posiblemente se esté ejecutando fuera de un contenedor de aplicaciones, tal vez incluso en otra máquina funcionando bajo una JVM distinta, esté lanzando una excepción JPA en lugar de algo como la archimalvada NullPointerException? El proveedor de persistencia puede hacer esto de algunas maneras, como devolviendo objetos proxy que envuelven al objeto original, o manipulando el código de bytes, entre otras. No voy a profundizar en este asunto pues no nos afecta de forma directa, pero tenlo simple presente de manera que, cuando suceda, sepas lo que está ocurriendo.
LA GRAN SOLUCIÓN
Es un poco atrevido pensar en que existe una gran solución a nuestro gran problema, pues primero deberíamos preguntarnos si éste último es realmente tal; hasta ahora lo hemos llamado así por el simple hecho de que a todos (al menos a todos los que hemos trabajado con JPA) nos ha ocurrido en alguna ocasión, sobre todo al principio, y los sentimientos que produce van desde "chocante" hasta "desconcertante" (sobre todo si no sabemos que está ocurriendo y, todavía peor, tampoco porqué). Lo cierto es que no es ningún problema, y tal como hemos dicho anteriormente, Lazy Fetch trata simple y llanamente de eso: enviar la información necesaria, y omitir el resto hasta que sea explicitamente accedida.
Una solución par evitar enviar a un cliente relaciones no inicializadas es definirlas explícitamente como no-lazy (o dicho de forma más correcta, Eager):
@Entity
public class Empleado {
// ...
@ManyToMany(fetch=FetchType.EAGER)
private List proyectos;
}
También podemos inicializar implícitamente las entidades asociadas a través de algún mecanismo del lenguaje:
@Stateless
public class ClaseDelLadoServidor {
// ...
public Proveedor obtenerProveedor(Long proveedorId) {
// Conectamos con BBDD y obtenemos la entidad a devolver
facturas.size(); // Inicializa la relación al completo. No usar para inicializar de
// forma explícita, en su lugar usar fetch=FetchType.EAGER.
return proveedor;
}
}
Para no violar el cometido principal de Lazy, sólo debemos inicializar una relación (ya sea de forma explícita o implícita) en escenarios como los siguientes:
- Cuando conocemos de antemano que los clientes de nuestra entidad van a acceder a el/los miembros de la relación después de que esta sea desconectada del contexto de persistencia.
- Cuando, a pesar de mantenernos aún dentro de una transacción, sabemos que se van a producir multitud de llamadas a los elementos referenciados en la relación (cada una de ellas realizando sus peticiones a base de datos de forma exclusiva e independiente de las demas), de manera que el coste total va a terminar siendo mayor que si hubiéramos inicializado la relación en el momento de la inicializar la entidad contenedora.
Evidentemente, estas situaciones no son faciles de preveer en la mayoría de los casos, más aún cuando no tenemos control sobre el código cliente. Éste último, si está bajo nuestro control, no debe hacer llamadas a relaciones no inicializadas (evidentemente). En caso de no conocer de antemano si una relación ha sido inicializada, debemos manejar un posible error mediante tratamiento de excepciones (esto es, capturándo la excepción e inicializando la relación dentro del bloque catch). Al margen del tratamiento de excepciones, si deseamos acceder a una relación que sabemos que no está inicializada, deberemos gestionarlo a través de lógica de negocio adicional (podemos enviar la entidad contenedora de vuelta al servidor de aplicaciones para que sea refrescada con la información ausente y devuelta de nuevo al cliente, solicitar sólo las entidades contenidas en la relación a traves de algún valor de la entidad contenedora que permita identificarlas, etc...). Esta lógica adicional estará integrada en el lado del servidor, pues es allí donde tenemos acceso al contexto de persistencia.
MOJÁNDONOS LOS PIES
Es importante entender que ocurre entre bambalinas cuando tratamos con relaciones de tipo Lazy, por lo que vamos a sumergirnos un poco (aunque muy poco) en la forma en que esto ocurre dentro de JPA. Todas (repito, todas) las operaciones de persistencia JPA se producen dentro de una transacción. Tarde o temprano, toda transacción finaliza, y en ese momento todas las entidades involucradas son desconectadas del contexto de persistencia, dejando de estar gestionadas por EntityManager.
En el ejemplo de la sección anterior, la clase ClaseDelLadoServidor ha sido marcada como @Stateless, de manera que ya sea mediante persistencia manejada por el contenedor (JTA) o controlada manualmente (RESOURCE-LOCAL), en el momento de terminar la ejecución del método se debe dar por terminada la transacción. Es en este momento cuando la entidad puede ser devuelta al cliente que la solicitó.
En el caso de estar trabajando con componentes de tipo @Statefull, podemos mantener una transacción por un periodo de tiempo mayor al de una única invocación a cualquiera de los métodos del componente, por lo que la entidad y sus relaciones pueden permanecer gestionadas por un tiempo mayor al de una única invocación a cualquiera de los métodos del componente (esto implica que cualquier acceso a una relación no inicializada se trataría de forma transparente para nosotros).
Al trabajar con relaciones Lazy, debemos usar siempre interfaces (como List o Map) en lugar de implementaciones concretas (como ArrayList o HashMap): esto, además de ser una las grandes-reglas/buenas-prácticas cuando utilizamos un lenguaje orientado a objetos como Java, va a permitir al proveedor de persistencia usar sus propias implementaciones lo que a su vez nos permitirá a nosotros beneficiarnos de la magia de Lazy Fetch.
LA OTRA CARA DE LA MONEDA
El comportamiento opuesto a Lazy Fetch, como vimos en un ejemplo anterior, es Eager Fetch (el término puede traducirse literalmente como obtención impaciente, pero yo, de nuevo, prefiero entenderlo como inicialización temprana). Cuando una relación es de tipo Eager, o es marcada con este comportamiento de forma explícita, ésta es inicializada en el mismo instante en que lo es su entidad contenedora. Esto conlleva cero problemas para el código cliente, pues a efectos prácticos estamos en la misma situación que cuando trabajamos con POJOs en un entorno de no-persistencia (se aplican las reglas de construcción típicas del class loader). Tan simple como eso.
USO Y ABUSO
Lazy Fetch tiene un cometido concreto, y esto por si sólo ya es motivo para su existencia. El coste de crear objetos puede ser inmenso si todas las relaciones dentro de una entidad (relaciones que a su vez pueden contener sus propias relaciones, y así indefinidamente) fueran iniciadas en cascada desde el momento de creación de la entidad contenedora original. Otra situación en la cual Lazy Fetch es sumamente útil es cuando definimos una relación con objetos de gran tamaño (como BLOBs y CLOBs). Esta clase de relaciones suelen definirse como Lazy y, en caso de ser necesario el/los objeto/s referenciados, accedidos mediante lógica adicional. Todo esto representa el buen uso de Lazy Fetch.
Por supuesto podemos inicializar explícitamente todas las relaciones como Eager y olvidarnos de todo lo demás, y esto representa el mal uso de la arquitectura. Tarde o temprano terminaremos incurriendo en costes extra, ya que entre todos los objetos que estaremos inicializando, multitud de ellos serán accedidos en muy raras ocasiones (o incluso ninguna).
En este punto, y a pesar del gran problema que tuvimos unas secciones más atrás, podríamos pensar que "Lazy Fetch es un mecanismo mejor que Eager Fetch"; nada más lejos de la realidad, pues podemos terminar cometiendo un pecado distinto pero igual de grave, el de marcar toda relación viviente como Lazy: puesto que ninguna relación estará inicializada dentro de la entidad contenedora, todos y cada una de los accesos que realicemos sobre ellas conllevará el coste extra de una nueva petición a base de datos, situándonos de nuevo frente a posibles futuros problemas de agotamiento de recursos.
La solución, como casi siempre, se encuentra en encontrar el equilibrio (en nuestro caso determinar que componentes deben ser Lazy y cuales Eager). Si bien esto es algo que no puede ser definido con exactitud en el momento de iniciar el desarrollo de una aplicación, lo será en mayor o menor medida durante su periodo de vida (en parte gracias a herramientas externas y en parte gracias al propio comportamiento y/o respuesta de la aplicación ante situaciones de estress).
RESUMEN
Esto es todo lo que necesitas saber para, si no ser el gran Gurú de Lazy Fetch, si al menos saber en cierto detalle qué es y cómo funciona. Puedes encontrar información adicional en esta misma web sobre persistencia en general y JPA en particular aquí y aquí. Saludos.
Para los que seguís mi blog, os será evidente que hace mucho tiempo que no publico nada; compromisos laborales y personales me tienen atado 24 horas al día, 7 días a la semana, dejándome apenas tiempo para sentarme delante del PC y escribir cualquiera de los muchos artículos que tengo en mente (o simplemente pendiente, como las dos últimas entregas del tutorial de EJB 3.1...).
Puesto que sois muchos los que me animáis a través de correo electrónico a continuar publicando contenido (y por ello os doy las gracias), en los próximos días volveré a la carga con nuevos artículos. Espero poder encontrar desde este mismo instante el tiempo suficiente para escribir tres artículos de cierta dimensión cada dos meses, como mínimo (y creedme que haré todo lo posible por llevarlo a cabo).
Por descontado, toda sugerencia sobre nuevos artículos será bienvenida, por lo que os animo a poneros en contacto conmigo al respecto como habéis hecho hasta ahora. Un saludo y hasta muy pronto.
Para todos aquellos que estáis siguiendo el tutorial de introducción a EJB 3.1, pero habéis decidido usar Glassfish v3 como servidor de aplicaciones (en lugar de JBoss 6), aquí tenéis el código necesario para configurar el cliente Java que conecta con el contenedor EJB:
// Opcional. Por defecto es localhost. Solo es necesario si el servidor se está ejecutando en una máquina distinta
props.setProperty("org.omg.CORBA.ORBInitialHost", "localhost");
// Opcional. Por defecto es 3700. Solo es necesario si el puerto ORB es diferente de 3700
props.setProperty("org.omg.CORBA.ORBInitialPort", "3700");
Context ic = new InitialContext(props);
De manera adicional, la aplicación cliente puede necesitar que el módulo gf-client.jar se encuent...
Para todos aquellos que estáis siguiendo el tutorial de introducción a EJB 3.1, pero habéis decidido usar Glassfish v3 como servidor de aplicaciones (en lugar de JBoss 6), aquí tenéis el código necesario para configurar el cliente Java que conecta con el contenedor EJB:
// Opcional. Por defecto es localhost. Solo es necesario si el servidor se está ejecutando en una máquina distinta
props.setProperty("org.omg.CORBA.ORBInitialHost", "localhost");
// Opcional. Por defecto es 3700. Solo es necesario si el puerto ORB es diferente de 3700
props.setProperty("org.omg.CORBA.ORBInitialPort", "3700");
Context ic = new InitialContext(props);
De manera adicional, la aplicación cliente puede necesitar que el módulo gf-client.jar se encuentre en el classpath (puedes encontrar dicho módulo en el directorio glassfish\modules de tu instalación de Glassfish v3).
Muchísimas gracias a Antonio Querol por ponerse en contacto conmigo y compartir esta información.
En los artículos anteriores del tutorial de introducción a EJB 3.1, hemos visto como declarar y trabajar con componentes de lado del servidor (Session Beans y Message-Driven Beans). El último componente que nos queda por ver es Entity Beans (EB - Beans de Entidad; a partir de ahora nos referiremos a ellos como entidades).
4.1 ENTIDADES: CONCEPTOS BÁSICOS
Las entidades, a diferencia del resto de componentes EJB, son objetos Java reales que son manejados entre componentes (o entre un cliente y un componente) en su forma original, nunca a través de proxys/vistas. Podemos crearlos con sentencias new, pasarlos como parámetros a un Session Bean, etc. Pero el verdadero valor de las entidades reside en que su estado puede ser almacenado en una base de datos, y más tarde recuperado en un nuevo objeto del tipo correspondiente. De manera adicional, los cambios que realicemos en el estado de una entidad serán sincronizados con la información que tenemos almacenada en la base de datos.
Aunque las entidades se consideran componentes EJB, hasta la version JavaEE 1.4 pertenecían a una especi...
En los artículos anteriores del tutorial de introducción a EJB 3.1, hemos visto como declarar y trabajar con componentes de lado del servidor (Session Beans y Message-Driven Beans). El último componente que nos queda por ver es Entity Beans (EB - Beans de Entidad; a partir de ahora nos referiremos a ellos como entidades).
4.1 ENTIDADES: CONCEPTOS BÁSICOS
Las entidades, a diferencia del resto de componentes EJB, son objetos Java reales que son manejados entre componentes (o entre un cliente y un componente) en su forma original, nunca a través de proxys/vistas. Podemos crearlos con sentencias new, pasarlos como parámetros a un Session Bean, etc. Pero el verdadero valor de las entidades reside en que su estado puede ser almacenado en una base de datos, y más tarde recuperado en un nuevo objeto del tipo correspondiente. De manera adicional, los cambios que realicemos en el estado de una entidad serán sincronizados con la información que tenemos almacenada en la base de datos.
Aunque las entidades se consideran componentes EJB, hasta la version JavaEE 1.4 pertenecían a una especificación independiente llamada Java Persistence API (JPA - API de Persistencia en Java). Fue a partir de la versión 5 de JavaEE que la especificación EJB absorvió a la especificación JPA. Sin embargo, podemos trabajar con entidades en una aplicación no-EJB, aunque, tras bambalinas, se seguirán ejecutando dentro de un contenedor EJB. Desde ahora usaremos el término aplicación JPA para referirnos a aplicaciones que realizan persistencia, y el término aplicación EJB cuando exista integración de ambas tecnologías.
En este artículo no vamos a tratar en profundidad la especificación JPA (ni la declaración ni el uso de entidades), si no como integrarla con una aplicación EJB 3.1. Si no conoces JPA, es prácticamente obligatorio que visites el tutorial de JPA publicado en este mismo blog, de manera que puedas comprender todo el material que sigue a continuación.
4.2 ENTIDADES: EL CICLO DE VIDA
Como ya se ha mencionado, las entidades no son objetos del lado del servidor. Sin embargo, cuando son usadas dentro del contexto de un contenedor EJB, se convierten en objetos gestionados (gracias al servicio de persistencia, el cual es controlado mediante la interface EntityManager). Esto nos lleva a los dos únicos estados de una entidad cuando se encuentra en el contexto de un contenedor EJB:
- Gestionada (Attached)
- No gestionada (Detached)
En el primer estado, la entidad se encuentra gestionada por el servicio de persistencia: cualquier cambio que realicemos en su estado se verá reflejado en la base de datos subyacente. En el segundo estado, la entidad es un objeto Java regular, y cualquier cambio que realicemos en su estado no será sincronizado con la base de datos subyacente. En este último estado, la entidad puede ser, por ejemplo, enviada a traves de una red (mediante serialización).
4.3 ENTIDADES: UNIDAD DE PERSISTENCIA
Una unidad de persistencia (persistence unit) representa un conjunto de entidades que pueden ser mapeadas a una base de datos, así como la información necesaria para que la aplicación JPA pueda acceder a dicha base de datos. Se define mediante un archivo llamado persistence.xml, el cual debe acompañar a la aplicación donde se realizan las tareas de persistencia (recuerda que puede ser una aplicación EJB o una aplicación Java normal):
<!-- configuración de acceso a la base de datos -->
<!-- lista de entidades que pueden ser mapeadas -->
</persistence-unit>
</persistence>
Podemos definir más de una unidad de persistencia por aplicación, declarando cada una de ellas mediante el elemento XML <persistence-unit>. Cada unidad de persistencia debe seguir estas dos reglas:
- Debe proporcionar un nombre (identidad) a través del cual pueda ser llamado
- Debe conectar a una sola fuente de datos (data source)
Dependiendo del tipo de paquete que estemos construyendo (EAR, JAR, etc), el archivo persistence.xml deberá encontrarse en una localización u otra. En la sección 4.6 veremos un ejemplo completo de este archivo, así como la información necesaria para su correcto despliegue dentro de una aplicación EJB.
4.4 ENTIDADES: CONTEXTO DE PERSISTENCIA
Otro concepto que tenemos que tener claro es el de contexto de persistencia. Un contexto de persistencia representa un conjunto de instancias de entidades que se encuentran gestionadas en un momento dado. Existen dos tipos de contextos de persistencia:
- Limitados a una transacción (Transaction-scoped)
- Extendidos (Extended)
Cuando trabajamos dentro de un contexto de persistencia limitado a una transancción, todas las entidades gestionadas pasarán a estar no gestionadas cuando dicha transacción finalice. Dicho con otras palabras, los cambios realizados tras finalizar la transacción no serán sincronizados con la base de datos.
Cuando trabajamos dentro de un contexto de persistencia extendido, las cosas funcionan de manera diferente: el contexto de persistencia sobrevivirá a la transacción donde se ejecuta, de manera que los cambios que realicemos en el estado de las entidades gestionadas por el contexto de persistencia se sincronizarán con la base de datos en el momento en que entremos en una nueva transacción. Este comportamiento es útil cuando trabajamos con SFSB, pues permite mantener un estado conversacional y mantener nuestras entidades sincronizadas.
4.5 ENTIDADES: ENTITY MANAGER
Mediante la interface EntityManager (Gestor de entidades) tenemos acceso al servicio de persistencia de nuestro contenedor. Podemos obtener una instancia de EntityManager en nuestros componentes EJB mediante inyección de dependencias:
El ejemplo anterior inyecta una instancia de EntityManager en el SLSB mediante la anotación @PersistenceContext. A esta anotación hay que proporcionarle como atributo el nombre de la unidad de persistencia que EntityManager usará para realizar la persistencia (y que hemos definido en el archivo persistence.xml). Con los metadatos proporcionados obtendremos por defecto un contexto de persistencia limitado a una transacción (ver sección anterior); si deseamos obtener un contexto de persistencia extendido (solo válido en SFSB por su naturaleza conversacional), debemos añadir el atributo type con el valor correspondiente para este comportamiento:
En lo que se refiere a este tutorial, la integración de EJB con JPA termina aquí. Por tanto, podemos pasar a ver un ejemplo donde conectaremos todas las piezas para realizar persistencia mediante EJB 3.1.
4.6 ENTIDADES: UN SENCILLO EJEMPLO
Veamos un sencillo ejemplo de un componente EJB realizando persistencia sobre una base de datos. Al igual que algunos ejemplos anteriores, este consta de varias partes:
- Una fuente de datos (data source) donde realizar la persistencia
- Una aplicación EJB donde se realiza las acciones de persistencia
- Un cliente EJB desde el que intercambiar entidades con la aplicación EJB
Nuestro primer paso va a ser definir una fuente de datos que conectará nuestro contenedor EJB con nuestra base de datos. Siguiendo el entorno de desarrollo configurado en el anexo que acompaña este tutorial, escribimos un archivo llamado derby-ds.xml y lo guardamos en el directorio server\default\deploy de nuestra instalación de JBoss:
En el archivo XML anterior definimos una fuente de datos (data source) limitado a transacciones locales, esto es, dentro del propio contenedor (existe otro ambito de ejecución de una transacción llamado extendido, capaz de realizar su trabajo a través de múltiples contenedores). A esta fuente de datos le hemos dado un nombre JNDI desde la que poder invocarla, así como los parámetros de conexión con nuestra base de datos subyacente (url, usuario, y password). Si JBoss 6.0.0 Final está arrancado, nada más guardar el archivo anterior la fuente de datos será activada y se producirá la conexión con Derby, así que deberás tener la base de datos iniciada o se producirá un error; nosotros vamos a considerar que en este preciso momento tanto JBoss como Derby están parados.
El siguiente paso es crear un proyecto EJB 3.1 en Eclipse, y continuación levantar la base de datos Derby desde el propio IDE. Para ello, haz click con el botón derecho sobre el nombre del proyecto EJB en la pestaña Proyect Explorer y selecciona:
Apache Derby > Add Apache Derby nature
La operación anterior añade a nuestro proyecto las librerias del servidor embebido Derby, de manera que podamos conectar con él. A continuación levantamos la base de datos haciendo click con el botón derecho sobre el nombre del proyecto EJB en la pestaña Proyect Explorer y seleccionando:
Apache Derby > Start Derby Network Server
Nos aparecerá una ventana donde se nos informa que la base de datos está siendo levantada, haz click en el botón OK para finalizar este proceso. En un entorno en producción, todas estas operaciones serían innecesarias, pues teóricamente tendríamos una base de datos externa funcionando de manera continua.
Ahora vamos a crear un componente SLSB remoto, de manera que podamos llamarlo desde un cliente Java normal. Como recordarás, un componente remoto requiere de manera obligatoria implementar una interface:
package es.davidmarco.ejb.slsb;
import es.davidmarco.ejb.entidad.Cuenta;
public interface OperacionesConCuentas {
public void crearCuenta(Cuenta cuenta);
public Cuenta obtenerCuenta(Long id);
public void borrarCuenta(Cuenta cuenta);
}
Ahora ya podemos implementar el componente remoto:
@Override
public void crearCuenta(Cuenta nuevaCuenta) {
em.persist(nuevaCuenta);
}
@Override
public Cuenta obtenerCuenta(Long id) {
return em.find(Cuenta.class, id);
}
@Override
public void borrarCuenta(Cuenta cuenta) {
em.remove(cuenta);
}
}
El componente anterior es extremadamente sencillo: en él se inyecta una instancia de EntityManager, la cual es usada en los métodos del Session Bean para realizar las tareas de persistencia. Estos métodos usan como parámetros o tipos de retorno instancias de la clase Cuenta (la cual define cuentas bancarias) y que será nuestra entidad:
public String getNumeroDeCuenta() {
return numeroDeCuenta;
}
public void setNumeroDeCuenta(String numeroDeCuenta) {
this.numeroDeCuenta = numeroDeCuenta;
}
public String getNombreDelTitular() {
return nombreDelTitular;
}
public void setNombreDelTitular(String nombreDelTitular) {
this.nombreDelTitular = nombreDelTitular;
}
public Double getSaldo() {
return saldo;
}
public void setSaldo(Double saldo) {
this.saldo = saldo;
}
public void aumentarSaldo(Double cantidad) {
saldo += cantidad;
}
public void reducirSaldo(Double cantidad) {
saldo -= cantidad;
}
}
Algunos detalles de la entidad definida en el ejemplo anterior merecen una pequeña explicación: para empezar, nuestra entidad implementa la interface Serializable, necesaría cuando nuestra entidad va a viajar a través de una red (en nuestro caso entre el cliente Java y el contenedor EJB). Dentro de la entidad definimos 4 propiedades: una para la identidad de la entidad, y tres para representar su estado (numeroDeCuenta, nombreDelTitular, y saldo), más sus correspondientes métodos getter/setter. De manera adicional, hemos añadido algunas operaciones de lógica de negocio (aumentarSaldo() y reducirSaldo) dentro de la entidad; es una buena práctica que las operaciones relacionadas con cuentas estén dentro de la clase que representa dichas cuentas.
Ahora que tenemos un componente EJB y una entidad, vamos a crear la unidad de persistencía asociada a la fuente de datos y nuestra entidad. Crea un archivo llamado persistence.xml en el directorio META-INF del proyecto EJB:
En el archivo XML anterior hemos declarado una unidad de persistencia con nombre introduccionEJB (que fue el que usamos como parámetro de la anotación @PersistenceContext en el componente SLSB que definimos previamente), así como transacciones de tipo JTA (gestionadas por el contenedor; los tipos de transacción se explicaron en las secciones 3.2 y 3.3 del tercer artículo del tutorial de JPA).
Ya dentro de la declaración de la unidad de persistencia, hemos declarado el proveedor de persistencia que usaremos (HibernatePersistence), la dirección JNDI de la fuente de datos que declaramos en el archivo derby-ds.xml, y las entidades que gestionaremos en esta unidad de persistencia (en nuestro caso solamente Cuenta). Por último, hemos configurado algunos detalles relativos a la fuente de datos dentro del elemento <properties>: el dialecto que usará el contenedor para construir sentencias SQL adecuadas a nuestra base de datos, y la creación automática de los esquemas necesarios en base a los metadatos de nuestras entidades (de esta manera nos evitamos crear manualmente tanto las tablas como sus columnas, incluyendo la definición del tipo de dato de cada columna, restricciones de cada columna, etc). Ahora ya podemos desplegar la aplicación EJB en el contenedor.
El último paso necesario para probar nuestro ejemplo es crear un cliente Java que pasará una instancia ya inicializada de la entidad Cliente al componente EJB. Para ello, y para mantener las cosas sencillas, creamos un proyecto Java en Eclipse y le añadimos las librerias del proyecto EJB haciendo click con el botón derecho del ratón en el nombre del proyecto Java y seleccionando:
Build Path > Configure Build Path
En la ventana que nos aparece, vamos a la pestaña Proyects, hacemos click en el botón Add, seleccionamos el proyecto EJB donde esta nuestro componente SLSB y nuestra entidad, hacemos click en el botón OK, y de nuevo hacemos click en el botón OK. Ahora ya podemos escribir el cliente:
En el ejemplo anterior, creamos e inicializamos una cuenta, obtenemos un proxy/vista al componente EJB, e invocamos su método crearCuenta() pasándole como parámetro la cuenta. Esta entidad será serializada, viajará a traves de la red hasta el contenedor, será deserializada, y dentro del componente EJB será persistida en la base de datos. Podemos comprobarlo ejecutando una sentencia SQL contra la base de datos: para ello, haz click con el botón derecho sobre el nombre del proyecto EJB en la pestaña Proyect Explorer y selecciona:
Apache Derby > ij (Interactive SQL)
La consola de ij se abrirá en la pestaña Console de Eclipse, y desde el prompt de ij nos conectamos a la base de datos mediante el comando:
Cuando la conexión se realice, volveremos a ver el prompt de ij. Ahora ya podemos realizar la consulta SQL mediante el comando:
select * from cuenta;
La consola de ij nos mostrará un registro en la tabla Cuenta:
1 |Nuevo cliente |0000-0001 |2500.0
Esto demuestra que nuestra entidad (un POJO Java) ha sido almacenado en una base de datos relacional (tablas y columnas) de manera transparente para nosotros. Misión cumplida. Si deseáramos realizar el proceso inverso (obtener un objeto Java desde la información almacenada en la base de datos):
// ...
public class Cliente {
private static final String JNDI_BEAN = "OperacionesConCuentasRemote/remote";
public static void main(String[] args) throws NamingException {
Properties properties = new Properties();
properties.put("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory");
properties.put("java.naming.factory.url.pkgs", "org.jboss.naming:org.jnp.interfaces");
properties.put("java.naming.provider.url", "jnp://localhost:1099");
Context context = new InitialContext(properties);
// Cuenta cuenta = new Cuenta();
// cuenta.setNumeroDeCuenta("0000-0001");
// cuenta.setNombreDelTitular("Nuevo cliente");
// cuenta.setSaldo(2500D); */
OperacionesConCuentas occ = (OperacionesConCuentas)context.lookup(JNDI_BEAN);
// occ.crearCuenta(cuenta);
Cuenta cuenta = occ.obtenerCuenta(1L);
System.out.println("Titular de la cuenta "
+ cuenta.getNumeroDeCuenta()
+ " con saldo "
+ cuenta.getSaldo()
+ ": "
+ cuenta.getNombreDelTitular());
}
}
En el ejemplo anterior, hemos comentado las lineas que crean y persisten un cliente (de otra manera se insertará un nuevo registro con los misma información en la base de datos pero con ID con valor 2), y en su lugar hemos llamado al método obtenerCuenta() del SLSB, para así obtener una cuenta desde la base de datos en base a su ID. Si una cuenta con ese ID no existe, obtendremos como respuesta un valor null. Te invito a que, a modo de práctica, elimines de la base de datos la cuenta que hemos creado llamando al método borrarCuenta() del SLSB; para verificarlo, una vez hayas ejecutado esta operación vuelve a realizar la consulta SQL contra la base de datos a través de la consola de ij:
select * from cuenta;
Si has realizado correctamente el ejercicio, la tabla Cuenta no deberá mostrar ninguna entidad (salvo que hayas insertado entidades adicionales).
4.7 RESUMEN
Como hemos visto en este artículo, podemos realizar persistencia de manera extremadamente sencilla en nuestras aplicaciones EJB. Las entidades son simples POJO's, la configuración con la base de datos se configura mediante archivos XML, y en nuestros componentes EJB solo tenemos que inyectar una unidad de persistencia y ejecutar sus métodos.
En este punto ya hemos visto todos los componentes EJB (Session Beans, Message-Driven Beans, y Entities). Los dos próximos artículos estarán dedicados a los diversos servicios que ofrece el contenedor, servicios que nuestros componentes pueden usar para realizar taréas más complejas (y que son necesarias en aplicaciones reales).
Hace ya algún tiempo que publiqué en el blog el tutorial de JPA (14 meses para ser más exactos), y por ser aquellos los primeros artículos técnicos que escribí y publiqué, llevaban consigo la impronta del escritor novato. Hoy, después de haber escrito algunos artículos más, mi estilo es en cierto modo diferente, para mi gusto un poquito mejor, y tal vez ahora me veo capaz de expresar los mismos conceptos de forma más simple y comprensible, lo que yo llamo más fácil de leer.
El proposito de todo este rollo (que no creo que os importe demasiado) es que, aprovechando que el tutorial de EJB 3.1 se encuentra en plena efervescencia, y sobre todo que el último de sus artículos publicado hasta ahora termina haciendo un guiño (por no decir un grito) al tutorial de JPA, he revisado, reescrito, y corregido este último, acciones que, una vez concluidas, he comprobado que eran realmente necesarias. Como siempre espero que disfrutéis del material revisado, tanto aquellos que ya lo hicieron en su momento, como los que lo tengan ante si por primera vez.
En el artículo anterior vimos algunos conceptos comunes a los componentes de una aplicación EJB, como el contexto de sesión, o la diferencia entre remoto y local. Además, vimos en cierta profundidad los dos tipos de Session Bean más comunes en una aplicación EJB: Stateless y Stateful. En este artículo vamos a ver el último tipo de Session Bean (Singleton Session Bean), dos conceptos aplicables a todos los Session Bean y que son nuevos en la especificación EJB 3.1 (vista sin interface y llamadas asíncronas), y finalmente un nuevo tipo de componente: Message-Driven Bean. Comencemos.
3.1 SINGLETON SESSION BEANS: CONCEPTOS BÁSICOS
El Singleton Session Bean (no existe una traducción literal de la palabra Singleton, pero viene a expresar el concepto de instancia única; desde ahora nos referiremos a este componente como Singleton Session Bean, o Singleton a secas) es un nuevo tipo de Session Bean introducido en la especificación EJB 3.1. Este componente se basa en el patrón de diseño del mismo nombre definido por Erich Gamma, Richard Helm, Ralph Johnson y John ...
En el artículo anterior vimos algunos conceptos comunes a los componentes de una aplicación EJB, como el contexto de sesión, o la diferencia entre remoto y local. Además, vimos en cierta profundidad los dos tipos de Session Bean más comunes en una aplicación EJB: Stateless y Stateful. En este artículo vamos a ver el último tipo de Session Bean (Singleton Session Bean), dos conceptos aplicables a todos los Session Bean y que son nuevos en la especificación EJB 3.1 (vista sin interface y llamadas asíncronas), y finalmente un nuevo tipo de componente: Message-Driven Bean. Comencemos.
3.1 SINGLETON SESSION BEANS: CONCEPTOS BÁSICOS
El Singleton Session Bean (no existe una traducción literal de la palabra Singleton, pero viene a expresar el concepto de instancia única; desde ahora nos referiremos a este componente como Singleton Session Bean, o Singleton a secas) es un nuevo tipo de Session Bean introducido en la especificación EJB 3.1. Este componente se basa en el patrón de diseño del mismo nombre definido por Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides en su libro Design Patterns: Elements of Reusable Object Oriented Software (Patrones de Diseño: Elementos Reusables de Software Orientado a Objetos; una lectura imprescindible). Este patrón de diseño garantiza que de una clase dada solamente pueda crearse una instancia, con un punto de acceso global para acceder a dicha instancia. La naturaleza única de un componente Singleton conlleva un alto rendimiento dentro del contenedor para este tipo de Session Bean.
Es importante tener en cuenta la sutil diferencia entre un componente de tipo Singleton y cualquiera de los otros dos tipos de Session Bean (SLSB y SFSB): cuando un cliente hace una llamada a un método de un SLSB o SFSB, puesto que la instancia del Session Bean está asociada a ese cliente en concreto (durante la invocación del metodo para un SLSB, y durante toda la duración de la sesión para un SFSB), un único thread (hilo de ejecución) es capaz de acceder a dicho método, y por tanto el componente es seguro en terminos de multi-threading (ejecución de múltiples hilos). Sin embargo, cuando trabajamos con un Singleton, multiples llamadas en paralelo pueden estar produciendose en un momento dado a su única instancia, y por tanto el componente debe garantizar que un hilo de ejecución no está interfiriendo con otro hilo de ejecución, produciendo resultados incorrectos. Dicho de otra manera, un componente Singleton debe ser concurrente;
3.2 SINGLETON SESSION BEANS: CONCURRENCIA BÁSICA
Debido a la naturaleza de los Session Bean de tipo Singleton, tenemos que tener muy claros ciertos aspectos al diseñar este tipo de componente. Veamos un ejemplo de una clase donde no tenemos en cuenta la cuestión de concurrencia:
class ClaseNoConcurrente {
private int numeroDeInvocacionesDeEstaInstancia = 0;
public int metodoUno() {
// lógica de negocio
return numeroDeEjecuciones++;
}
public int metodoDos() {
// lógica de negocio
return numeroDeEjecuciones++;
}
}
En la clase del ejemplo anterior, no se ha tenido en cuenta el aspecto de concurrencia (lo cual, dependiendo de las especificaciones del problema de negocio que queremos resolver, puede ser perfectamente legal). Una consecuencia de esto es que, cuando intervienen múltiples hilos de ejecución en paralelo, una llamada al cualquiera de los métodos metodoXxx puede devolver un resultado incorrecto. Veamos una posibilidad (entre muchas permitidas por la JVM) de la ejecución de esta clase por varios clientes:
1. Un cliente C-1 llama a metodoUno() 2. El cliente C-1 obtiene el valor 1 para el número de ejecuciones
3. Un cliente C-2 llama a metodoUno() 4. Un cliente C-3 llama a metodoDos() 6. El cliente C-3 obtiene el valor 2 para el número de ejecuciones
7. El cliente C-2 obtiene el valor 2 para el número de ejecuciones
¿Que ha ocurrido en el ejemplo anterior? El cliente C-2 cree que los métodos metodoXxx han sido invocados dos veces (punto 7), cuando en realidad se han invocado tres veces (puntos 1, 3, y 4). Esto es debido a que la operación numeroDeEjecuciones++ no es atómica: cuando el código Java es convertido en código de bytes (.class), dicha operación se compone de muchas otras, de manera que la JVM puede para el hilo actual en mitad del incremento y dar tiempo de ejecución a otro hilo. Incluso aunque la operación fuera atómica, la JVM podría seguir deteniendo un hilo durante la lógica de negocio (antes de realizar el incremento), dando tiempo de ejecución a otro hilo y produciendo de nuevo resultados incorrectos. Puesto que un Singleton es compartido por muchos clientes (accedido por muchos hilos de ejecución) este comportamiento no es aceptable: el Singleton debe ser concurrente.
La especificación EJB 3.1 nos permite controlar la concurrencia de un Singleton de dos maneras:
- Concurrencia Gestionada por el Contenedor (CMC - Contained-Managed Concurrency)
- Concurrencia Gestionada por el Bean (BMC - Bean-Managed Concurrency)
Veamos cada una de ellas con cierto detalle.
3.3 SINGLETON SESSION BEANS: CONTAINED-MANAGED CONCURRENCY
Mediante CMC, el contenedor es responsable de gestionar toda la concurrencia en el Singleton mediante metadatos, liberando así al programador de la tarea de escribir código concurrente. Por defecto, todos los componentes Singleton son gestionados por el contenedor, aunque podemos especificarlo de manera explicita mediante la anotación @ConcurrencyManagement:
@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
class MiSingleton {
// ...
}
En el ejemplo anterior hemos definido un componente Singleton mediante la anotación @Singleton, y hemos indicado al contenedor de forma explícita que gestione por nosotros la concurrencia del componente mediante CMC (aunque, repito, este comportamiento es el ofrecido por defecto para todos los Singleton). Cuando usamos CMC, todos los métodos de la clase tienen por defecto un bloqueo de tipo write (escritura). Este bloqueo es de utilidad cuando uno o varios métodos deben ser accedidos de manera secuencial (hasta que uno no termine, otro no puede comenzar) para evitar que se obtengan resultados incorrectos. Este acceso secuencial es necesario en casos como el del ejemplo de la sección 3.2, en el que varios métodos escribían sobre el valor de una variable que después era devuelta al cliente. Como se puede deducir del comportamiento del bloqueo write, este afecta a todo el objeto, y no a métodos concretos (todos los métodos write del mismo Singleton deben compartir un único bloqueo).
Por otro lado, cuando un método realiza solo operaciones de tipo read (lectura), de manera que no se altera el estado del componente, podemos indicarle al contenedor que no bloquee ninguna invocación a dicho método. De esta manera, el método pueda ser accedido por múltiples hilos de ejecución simultaneamente (puesto que no modificamos el estado del componente, el acceso en paralelo es seguro). Tenemos que indicar que un método es de tipo read mediante la anotación @Lock:
import javax.ejb.Lock;
import javax.ejb.LockType;
// ...
@Lock(LockType.READ)
public String metodo() {
// ...
}
Aunque, como ya se ha dicho, el tipo de bloqueo write se aplica por defecto, podemos expresarlo de forma explícita mediante @Lock(LockType.WRITE). Otra opción que nos brinda CMC es la liberación de un bloqueo automáticamente si este no ocurre tras un tiempo preestablecido (timeout):
@AccessTimeout(value=5, unit=TimeUnit.SECONDS)
public String metodo() {
// ...
}
3.4 SINGLETON SESSION BEANS: BEAN-MANAGED CONCURRENCY
En ocasiones, la concurrencia gestionada por el contenedor no es suficiente (tal vez no nos permite definir con suficiente detalle la manera en la que necesitamos que funcione la concurrencia de nuestra aplicación, por ejemplo). Bean-Managed Concurrency (BMC - Concurrencia Gestionada por el Bean) deja en manos del programador toda la gestión de la concurrencia. Esto puede realizarse utilizando bloques synchronized, variables atómicas como java.util.concurrent.atomic.AtomicInteger, etc. Podemos indicar que la concurrencia de un componente Singleton será gestionada íntegramente por el programador mediante la anotación @ConcurrencyManagement (la cual usamos en la sección anterior para declarar explícitamente el comportamiento contrario, CMC):
@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
class OtraClaseConcurrente {
// Gestión explícita de la concurrencia
}
La concurrencia en Java es un tema largo y complejo que no puede ser tratado en este tutorial. Un buen lugar para empezar (en inglés) es The Java Tutorials. Estoy seguro que existen otras buenas fuentes de información en español, y si estás interesado tu buscador favorito te llevará hasta ellas.
3.5 SINGLETON SESSION BEANS: EL CICLO DE VIDA
El ciclo de vida de un Singleton muy parecido al de un SLSB, pues como en este último se compone de los dos mismos estados:
- No Existe (Does not exists)
- Preparado (Method-ready)
La diferencia estriba en cuando se crea la única instancia del componente Singleton (tras el despliegue de la aplicación o tras la primera invocación al componente), y en su duración (a lo largo de toda la vida de la aplicación, esto es, hasta que la aplicación sea replegada o el servidor sea parado). Evidentemente, solo el primer comportamiento (el momento de creación) puede ser customizado, y para ello la especificación EJB nos ofrece, como siempre, metadatos. Aunque el contenedor puede decidir inicializar la única instancia de un Singleton en el momento que considere más oportuno, podemos forzar que dicho proceso ocurra durante el despliegue mediante la anotación @Startup:
@Singleton
@Startup
public class MiSingleton {
// ...
}
El código anterior fuerza al contenedor a crear la única instancia de MiSingleton en el momento del despliegue de la aplicación EJB de la que forma parte. Esto comportamiento es llamado eager initialization (inicialización temprana). Ahora supongamos que un Singleton debe ser inicializado antes que otro Singleton (tal vez este último necesite poner la aplicación en cierto estado necesario para el correcto funcionamiento del primero): podemos indicar esta dependencia en el orden de inicialización mediante la anotación @DependsOn:
@Singleton
public class MiSingletonA {
// ...
}
@Singleton
@DependsOn("MiSingletonA")
public class MiSingletonB {
// ...
}
En el ejemplo anterior, indicamos al contenedor que en el momento de inicializar la única instancia de MiSingletonB, MiSingletonA debe haber sido inicializado. De esta manera, el contenedor forzará la inicialización del Singleton dependiente si aún no se ha producido. Este comportamiento es totalmente ajeno al uso u omisión de @Startup: si un Singleton depende de otro, este último se creará antes que el primero.
Para terminar con el ciclo de vida de los componentes Singleton, indicar que, al igual que con los componentes SLSB y SFSB, disponemos de las anotaciones @PostConstruct y @PreDestroy para responder a los eventos de creación y destrucción (respectivamente) del componente:
@Singleton
public class MiSingletonA {
@PostConstruct
public void inicializar() {
// Cualquier operación/es necesaria/s para la creación del componente,
// como obtención de recursos
}
@PreDestroy
public void detener() {
// Cualquier operación/es necesaria/s antes de la destrucción del componente,
// como liberación de recursos
}
// lógica de negocio del Singleton
}
3.6 SINGLETON SESSION BEANS: UN EJEMPLO SENCILLO
La utilidad del componente Singleton está limitada a ciertos problemas de negocio que podemos (o debemos) resolver mediante una única instancia de un objeto. Ejemplos válidos de componentes Singleton serían gestores de ventanas, sistemas de ficheros, colas de impresión, caches, etc. Para nuestro ejemplo de Singleton vamos a desarrollar un sistema de logging, el cual debe ser accedido por toda la aplicación a través de una única instancia:
El ejemplo anterior, aunque más desarrollado que los vistos hasta ahora (es completamente funcional), sigue siendo un ejemplo de juguete; sin embargo, es bastante interesante para explicar el componente Singleton. Existen verdaderos frameworks de logging que puedes (¡y debes!) usar en tus aplicaciones.
Las primero que nos interesa del ejemplo son los métodos callback@PostConstruct y @PreDestroy. En ellos se obtiene y libera (respectivamente) un recurso dado, en nuestro caso el acceso a un fichero en disco. Esto nos muestra la utilidad del componente Singleton: únicamente una instancia debe crear, editar, y finalmente cerrar el mismo fichero en disco. Y esta única instancia es la que obtendrán todos los clientes del Singleton. Ambos métodos se han marcado con visibilidad protected para facilitar la tarea de testing (y por un motivo adicional que se explicará en la próxima sección).
Lo segundo que nos interesa del ejemplo son los tres métodos que se exponen al cliente: debug, info, y error. A traves de ellos el cliente puede realizar las operaciones de logging. Los tres han sido marcados como métodos write mediante @Lock(LockType.WRITE), y aunque este comportamiento es definido por defecto para todos los métodos de un Singleton (y por tanto la anotación es redundante), se han incluido para diferenciarlos del resto (siempre es aconsejable escribir código expresivo).
Por último, los métodos de utilidad escribirMensajeEnArchivo y generarCabecera son métodos de utilidad usados por la lógica de negocio de nuestro componente.
3.7 SESSION BEANS EN GENERAL: NO-INTERFACE VIEW
Como se indicó en la sección anterior, el ejemplo del sistema de logging es completamente funcional. Sin embargo, el componente no ha sido declarado local ni remoto (ejemplos anteriores omitían este aspecto intencionadamente para mantener el código simple). En este momento es preciso introducir el concepto de vista sin interface.
Una vista sin interface (no-interface view) es una variación del concepto de componente local, e introducida en la especificación EJB 3.1. Como su nombre indica, se trata de un componente que no está anotado con @Local ni @Remote, ni implementa ninguna interface de negocio donde se hayan aplicado cualquiera de las anotaciones anteriores. Si no implementamos ninguna interface de negocio, ¿como sabe el contenedor que métodos del componente debe exponer a sus clientes? La respuesta es sencilla: todos aquellos declarados public, incluyendo los de sus superclases y los de tipo callback (en el ejemplo de la sección anterior hemos declarado los métodos callback con un nivel de visibilidad menor a public precisamente para evitar que sean expuestos). Aunque a efectos prácticos un componente de este tipo es tratado como uno local, puedes encontrar los detalles concretos sobre la vista sin interface en la especificación EJB 3.1.
3.8 SESSION BEANS EN GENERAL: LLAMADAS ASÍNCRONAS
Otra de las novedades de la especificación EJB 3.1 es la posibilidad de llamar a los métodos de nuestros Session Bean de forma asíncrona. Hasta ahora, todas las llamadas que hemos realizado han sido sincronas, de manera que los clientes deben esperar hasta que el método invocado termine de ejecutarse para seguir ejecutando el resto de sus sentencias. Este es el comportamiento normal cuando invocamos un método en Java. Sin embargo, cuando realizamos una llamada asíncrona, el flujo de ejecución vuelve automáticamente a la aplicación que realiza la llamada, sin esperar el resultado de la llamada. Más tarde, podemos comprobar dicho resultado si existe y lo necesitamos.
Las llamadas asíncronas son de gran utilidad en situaciones en las que un método realiza una operación que necesita un tiempo considerable para completarse, y no deseamos bloquear al cliente mientras dicha operación se realiza. Veamos primero el ejemplo más sencillo posible de llamada asíncrona:
@Asynchronous
public void metodoLento() {
try {
Thread.sleep(15000);
} catch(InterruptedException ie) {
throw new RuntimeException(ie);
}
}
}
En el ejemplo anterior, hemos declarado el método metodoLento() como un método asíncrono mediante la anotación @Asynchronous. Dicha anotación puede ser aplicada también a nivel de clase, en cuya caso todos los métodos de negocio (los declarados en una interface local, en una interface remota, o los de visibilidad public en una vista sin interface) serán considerados como asíncronos. Dentro del método hemos simulado un proceso relativamente largo deteniendo durante quince segundos el hilo de ejecución que está procesando la instancia del SLSB. Cualquier cliente que llame a este método no tendrá que esperar esos quince segundos, ya que nada más realizar la llamada le será devuelto en control de ejecución, sin esperar a que el método asíncrono termine. Este comportamiento se conoce como fire-and-forget (disparar y olvidar).
Otra posibilidad es que necesitemos el resultado de una invocación asíncrona. Supongamos que necesitamos un método que realiza un cálculo intensivo y, cuando finalmente obtiene el resultado, lo devuelve en una variable de tipo Double. La manera de declarar dicho método sería similar a esta:
@Asynchronous
public Future metodoLentoConResultado() {
try {
Thread.sleep(15000);
return new AsyncResult(Math.PI);
} catch(InterruptedException ie) {
throw new RuntimeException(ie);
}
}
Como puedes ver, el método metodoLentoConResultado() devuelve un objeto de tipo Future (más concretamente de su implementación AsyncResult) parametizado a Double. Es sobre este objeto Future sobre el que el cliente deberá comprobar si el método asíncrono ha terminado su ejecución, para obtener a continuación su resultado:
Future resultado = bean.metodoLentoConResultado();
System.out.println("Método asíncrono invocado");
System.out.println("Realizando otras tareas mientras se ejecuta el método asíncrono...");
// ...
El ejemplo anterior es parte de un cliente del SLSB asíncrono. En el, en un momento dado, se llama al método asíncrono, asignando el objeto Future devuelto por dicho método en una variable. En este momento el resultado no está listo (recuerda que tardará quince segundos en producirse), pero el control de la ejecución del cliente continua sin esperar. Más adelante el cliente comprueba si el resultado está finalmente disponible mediante el método isDone() de la clase Future. Cuando esto ocurra, podemos obtener nuestro ansiado resultado mediante el método get() (también de la clase Future), el cual devuelve un objeto del mismo tipo al que usamos para parametizar la respuesta del método asíncrono (en nuestro caso, un objeto Double). Es importante tener presente que el método get() bloquea el hilo de ejecución del cliente hasta que el resultado esté listo, de manera que podemos omitir por completo el bucle while:
Future resultado = bean.metodoLentoConResultado();
System.out.println("Método asíncrono invocado");
System.out.println("Realizando otras tareas mientras se ejecuta el método asíncrono...");
// ...
System.out.println("Método asíncrono devuelve resultado " + resultado.get());
La interface Future incluye otros métodos con los que podemos cancelar la ejecución del método asíncrono, o comprobar si dicha ejecución ha sido cancelada por el contenedor (por ejemplo en caso de excepción). Es recomendable que visites la API de Future si vas a trabajar con ella.
Antes de terminar con los métodos asíncronos, quiero hacer constar que en la implementación del contenedor EJB que estamos usando en este tutorial (JBoss 6.0.0 Final) las llamadas asíncronas aún no están completamente implementadas (el hilo de ejecución del cliente se bloquea al llamar al método asíncrono hasta que este ha terminado de ejecutarse; en otras palabras, las llamadas asíncronas se comportan como llamadas síncronas). En otros contenedores compatibles con EJB 3.1, como Glassfish v3, el comportamiento de las llamadas asíncronas si es el correcto (no lo he probado personalmente, pero así aparece indicado por usuarios de ambos servidores).
3.9 SESSION BEANS EN GENERAL: RESUMEN
Con esta sección terminamos de ver los componentes de tipo Session Bean, que son aquellos que contienen la lógica de negocio de una aplicación EJB que puede ser invocada por los clientes de dicha aplicación (ya sea en cualquiera de sus tres variaciones: Stateless, Stateful, o Singleton). Ahora es el momento de pasar a un nuevo tipo de componente con una misión totalmente diferente: Message-Driven Beans.
3.10 MESSAGE-DRIVEN BEANS: CONCEPTOS BÁSICOS
Los componentes de tipo Message-Driven Bean (MDB - Bean Dirigido por Mensajes) son componentes asíncronos de tipo listener (oyente). Un MDB no es más que un componente que espera a que se le envie un mensaje, y realiza cierta acción cuando finalmente recibe dicho mensaje (el MDB escucha por si alguien le llama, de ahí su nombre). Algunas propiedades de los componentes MDB son:
- No mantienen estado
- Son gestionados por el contenedor (transacciones, seguridad, concurrencia, etc)
- Son clases puras que no implementan interfaces de negocio (puesto que son invocados por un cliente)
Los componentes MDB forman parte de un sistema de mensajería, el cual se compone de los siguientes subsistemas:
En lo que refiere a este tutorial, el cliente consumidor será el propio contenedor EJB, el cual distribuirá los mensajes que consuma a los MDB correspondientes.
Los tres primeros subsistemas (clientes y broker) forman parte del servicio de mensajería, que en la especificación EJB es gestionado por defecto mediante JMS. En este momento es preciso desviarnos del camino para explicar con un mínimo de detalle que es y cómo funciona JMS.
3.11 JAVA MESSAGE SERVICE: CONCEPTOS BÁSICOS
Java Message Service (JMS - Servicio de Mensajería en Java) es una API neutral que puede ser usada para acceder a sistemas de mensajería. Todos los contenedores EJB 3.x deben proporcionar un proveedor de JMS (el cual define su propia implementación de la API), de manera que podamos trabajar con mensajes sin necesidad de añadir librerías externas. Puesto que esta API es neutral, podemos cambiar en cualquier momento el proveedor JMS por uno que se adecue más a nuestras necesidades (o incluso usar un sistema de mensajería diferente a JMS).
Una aplicación JMS se compone, generalmente, de múltiples clientes JMS y un único proveedor JMS. Un cliente JMS puede ser de dos tipos:
- Productor: su misión es enviar mensajes
- Consumidor: su misión es recibir mensajes
Por otro lado, la misión del proveedor JMS es dirigir y enviar los mensajes que le llegan a traves de un broker (esto es algo bastante más complejo, pero por simplicidad vamos a pensar que tenemos un subsistema llamado broker donde se almacenan los mensajes enviados hasta que son servidos a todos sus consumidores). JMS proporciona un tipo de mensajería asíncrona: los clientes JMS envían mensajes a traves del broker sin esperar una respuesta. Es responsabilidad del broker hacer llegar el mesaje a los clientes JMS que deban consumir el mensaje. De esta manera JMS proporciona un sistema de comunicación muy poco acoplado, pues el productor y el consumidor no saben el uno del otro en ningún momento.
3.12 JAVA MESSAGE SERVICE: MODELOS DE MENSAJERÍA
JMS proporciona dos modelos de mensajería, los cuales nos permiten definir el comportamiento de nuestro sistema de mensajería:
- Publicar y Suscribir (pub/sub - Publish and Subscribe)
- Punto a Punto (p2p - Point to Point)
En el modelo pub/sub, un cliente JMS de tipo productor publicasus mensajes en un canal virtual llamado topic (tema). A su vez, uno o varios clientes JMS de tipo consumidor se suscriben a dicho topic si desean recibir los mensajes que en él se publiquen. Si un suscriptor decide desconectarse del topic y más tarde reconectarse, recibirá todos los mensajes publicados durante su ausencia (aunque este comportamiento es configurable). Por todo esto, el modelo pub/sub es de tipo uno-a-muchos (un productor, muchos consumidores).
En el modelo p2p, un cliente JMS de tipo productor publica sus mensajes en un canal virtual llamado queue (cola). De manera similar al modelo pub/sub, pueden existir multiples consumidores conectados al queue. Sin embargo, el queue no enviará automáticamente los mensajes que le lleguen a todos los consumidores, si no que son estos últimos los que deben solicitarlos al queue. De manera adicional, solo un consumidor consumirá cada mensaje publicado: el primero en solicitarlo. Cuando esto ocurra, el mensaje se borrará del queue, y el resto de consumidores no será siquiera consciente de la anterior existencia del mensaje. Por todo esto, el modelo p2p es de tipo uno-a-uno (un productor, un consumidor).
En versiones anteriores a JMS 1.1 cada uno de estos modelos usaba su propio conjunto de interfaces y clases para para el envio y recepción de mensajes. Desde la citada versión de JMS, sin embargo, está disponible una API unificada que es válida para ambos modelos de mensajería.
Ahora es el momento de volver al camino que dejamos dos secciones atrás, y seguir con nuestros amados componentes MDB.
3.13 MESSAGE-DRIVEN BEANS: EL CICLO DE VIDA
Tal como vimos en la sección 3.10, los componentes MDB no mantienen estado entre invocaciones (son stateless, pero no confundir con los componentes SLSB). Por tanto, su ciclo de vida es similar a los SLSB, constando de dos estados:
- No existe (Does not exists)
- Preparado en pool (Method-ready pool)
Un MDB en el primer estado es aquel que no ha sido creada aún, y por tanto no existe en memoria. Un MDB en el segundo estado representa una instancia que ha sido instanciada e inicializada por el contenedor. Se llega a este estado cuando se inicia el servidor (que puede decidir crear cierto número de instancias del MDB para procesar mensajes), o cuando el número de instancias en el pool sea insuficiente para atender todos los mensajes que se estén recibiendo. La especificación EJB no fuerza a que exista un pool de MDB's, de manera que una implementación concreta de contenedor EJB ppdría decidir crear una instancia cada vez que se reciba un mensaje (y eliminar esta instancia al terminar de procesar el mensaje) en lugar de mantener un pool de instancias ya preparadas. Este último aspecto, sea como sea, no nos afecta como programadores (al diseñar un MDB) ni como clientes; la forma en que sea gestionada nuestra aplicación es, a priori, responsabilidad exclusiva del contenedor.
Durante la transición entre el primer estado y el segundo, el contenedor realizará tres operaciones (en este orden):
- Instanciación del MDB
- Inyección de cualquier recurso necesario y de dependencias
- Ejecución de un método dentro del MDB marcado con la anotación @PostConstruct, si existe
La instanciación del MDB se lleva a cabo mediante reflexión, a traves de Class.newInstance(). Por tanto, el MDB debe tener un constructor por defecto (sin argumentos), ya sea de forma implícita o explícita.
Durante la inyección de dependencias, el contenedor inyectará automáticamente cualquier recurso necesario para el MDB en base a los metadatos que hayamos proporcionado (como una anotación @MessageDrivenContext, o una entrada en el descriptor XML de EJB). De manera adicional, cada vez que un MDB procese un nuevo mensaje, todas las dependencias se inyectarán de nuevo.
Por último, el contenedor ejecutará, si existe, un método dentro del MDB anotado con @PostConstruct (Post construcción). En este método podemos obtener recursos adicionales necesarios para el MDB (como conexiones de red, etc), recursos que permanecerán abiertos hasta la destrucción del MDB. Las reglas de declaración de los métodos callback como @PostConstruct se vieron una y otra vez en el artículo anterior.
Cuando el contenedor no necesita una instancia del MDB (ya sea porque decide reducir el número de instancias en el pool, o porque se esta produciendo un shutdown del servidor), se realiza una transición en sentido inverso: del estado preparado en pool al estado no existe. Durante esta transición se ejecutará, si existe, un método anotado con @PreDestroy (Pre destrucción), donde podemos liberar los recursos adquiridos en @PostConstruct. Es importante tener presente que dentro del método @PreDestroy todavía tenemos acceso al contexto del MDB (lo mismo es válido para todos los Session Bean).
3.14 MESSAGE-DRIVEN BEANS: DEFINICIÓN
Como ya hemos visto, los componentes de tipo MDB tienen como misión procesar mensajes enviados de forma asíncrona. Aunque todo contenedor compatible EJB 3.x debe incluir una implementación de JMS (de manera que tengamos un servicio de mensajería on the box), MDB puede trabajar con otros servicios de mensajería diferentes. En este tutorial solo se usará JMS como servicio de mensajería (afectando este hecho, por ejemplo, a la interface que debe implementar el MDB, como veremos en el próximo párrafo).
Todo MDB debe implementar la interface javax.jms.MessageListener, la cual define el método onMessage(). Es dentro de este método donde se desarrolla toda la acción cuando el MDB procesa un mensaje, como veremos en el ejemplo de la próxima sección. De manera adicional, debemos indicar al contenedor que nuestra clase es un componente MDB. Para ello, utilizamos la anotación @MessageDriven:
@MessageDriven()
public class PrimerMDB implements MessageListener {
public void onMessage(Message message) {
// procesar el mensaje
}
}
El ejemplo anterior aún no es funcional (producirá un error de despliegue), ya que el MDB necesita saber el tipo de canal virtual (topic/queue) al que debe conectarse, así como el nombre de dicho canal virtual. Esta información forma parte de la configuración del MDB, configuración que proporcionamos al componente a través del atributo activationConfig (configuración de activación) de la anteriormente vista anotación @MessageDriven:
@MessageDriven(activationConfig={
@ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Topic"),
@ActivationConfigProperty(propertyName="destination", propertyValue="topic/MiTopic")})
public class PrimerMDB implements MessageListener {
// ...
}
En el ejemplo anterior, hemos proporcionado al MDB la configuración necesaria mediante un array de anotaciones @ActivationConfigProperty. Cada una de estas anotaciones contiene dos atributos: propertyName y propertyValue, en los cuales indicamos parejas nombre-de-la-propiedad/valor-de-la-propiedad, respectivamente. Volviendo a nuestro último ejemplo, hemos configurado el MDB con las siguientes propiedades:
- El tipo de canal virtual al que el MDB se conectará (javax.jms.Topic)
- El nombre JNDI del canal virtual al que el MDB se conectará (topic/MiTopic).
Otra opción de configuración bastante interesante (aunque opcional) es la duración de la subscripción:
Añadiendo la anotación anterior al array de propiedades de configuración del MDB, nuestro componente recibirá todos los mensajes que se envien a su canal virtual mientras el contenedor EJB (y por tanto el propio componente MDB) esté offline. Esta situación puede ocurrir durante un shutdown del servidor, un problema de conectividad por red, etc. En otras palabras, el mensaje estará disponible hasta que todos sus suscriptores de tipo durable lo hayan recibido y procesado. La opción contraria se aplica cambiando el valor del atributo propertyValue a NonDurable (no durable). La durabilidad de los mensajes solo afecta a los componentes MDB que trabajan con topics (modelo pub/sub), pues en un canal de tipo queue no tiene ningún sentido almacenar un mensaje para componentes MDB offline (en cuanto un MDB este online y lo consuma, el mensaje se eliminará).
Al igual que los componentes Session Bean, los componentes MDB funcionan dentro de un contexto de ejecución. Y por tanto, al igual que los componentes Session Bean, nuestros MDB pueden acceder a su contexto de ejecución si necesitan comunicarse con el contenedor:
@MessageDriven(/*...*/)
public class PrimerMDB implements MessageListener {
@Resource
private MessageDrivenContext context;
// ...
}
En el ejemplo anterior indicamos al contenedor que inyecte una instancia de MessageDrivenContext mediante la anotación @Resource. MessageDrivenContext extiende la interface EJBContext, sin añadir ningún método. Solamente los métodos transaccionales son útiles al MDB, como se verá cuando trabajemos con transacciones en artículos posteriores. El resto de métodos de la interface lanzará una excepción si son invocados, ya que no tiene sentido que un MDB pueda, por ejemplo, acceder al servicio de seguridad.
Por otro lado, un MDB también puede referenciar un Session Bean para realizar el procesamiento del mensaje (mediante la anotación @EJB, sección 2.5 del artículo anterior), así como enviar él mismo sus propios mensajes mediante JMS (al final de la próxima sección veremos un ejemplo de un MDB procesando mensajes, y a su vez enviando nuevos mensajes que serán procesador por otro MDB).
3.15 MESSAGE-DRIVEN BEANS: UN SENCILLO EJEMPLO
Ya hemos visto como los servicios de mensajería asíncronos desacoplan de forma completa al productor de un mensaje de su/s receptor/es: ninguno de ellos es consciente de la parte contraria. Esto debido a que, entre ellos, existe un broker donde se realiza todo el proceso de almacenaje (ya sea en canales virtuales de tipo topic o queue) y reparto de los mensajes que se reciben.
El ejemplo que vamos a ver consta de tres partes:
- Creación de un canal virtual
- Creación de un componente MDB para consumir mensajes
- Creación de un cliente para enviar mensajes mediante JMS
El primer paso lógico es crear un canal virtual donde poder enviar mensajes (un topic para modelos pub/sub o un queue para modelos p2p). En JBoss 6.0.0 Final (el servidor de aplicaciones que instalamos en el anexo que acompaña este tutorial), el proveedor JMS incorporado es HornetQ 2.1.2 Final. Para declarar un canal virtual de tipo topic, edita el archivo llamado hornetq-jms.xml del directorio server/default/deploy/hornetq/ de tu instalación de JBoss y añade lo siguiente (default es la configuración por defecto al crear el servidor en Eclipse; si seleccionaste otra configuración, modifica la ruta anterior a la que corresponda):
En el archivo XML anterior hemos definido un canal virtual de tipo topic (mediante el elemento <topic>) con nombre PrimerTopic, y lo hemos asociado a la dirección JNDI /topic/PrimerTopic (es necesario reiniciar JBoss si se encuentra levantado). La forma de configurar un canal virtual puede ser diferente entre distintos contenedores (incluso entre distintas implementaciones de un mismo contenedor), por lo que si estás usando un servidor de aplicaciones diferente a JBoss 6.0.0 Final o un proveedor JMS diferente a HornetQ 2.1.2 Final, quizá necesites revisar la documentación correspondiente para declarar el canal virtual.
Aunque este ejemplo se basará en un canal virtual de tipo topic, puedes declarar un queue (para montar un modelo de mensajería p2p) de la siguiente manera:
El siguiente paso lógico sería escribir un componente MDB que procese los mensajes del topic. Para ello, necesitamos crear un proyecto EJB en Eclipse y declarar una clase como la siguiente:
@MessageDriven(activationConfig={
@ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Topic"),
@ActivationConfigProperty(propertyName="destination", propertyValue="topic/PrimerTopic")})
public class PrimerMDB implements MessageListener {
@Override
public void onMessage(Message message) {
if(message instanceof TextMessage) {
try {
String contenidoDelMensaje = ((TextMessage)message).getText();
System.out.println("PrimerMDB ha procesado el mensaje: " + contenidoDelMensaje);
} catch (JMSException jmse) {
throw new RuntimeException("Error al procesar un mensaje");
}
}
}
}
En el ejemplo anterior declaramos un componente MDB (con la anotación @MessageDriven), lo configuramos para actuar como consumidor de los mensajes del topic registrado con dirección JNDI topic/PrimerTopic (con las anotaciones @ActivationConfigProperty), y finalmente añadimos dentro del método onMessage() la lógica que procesará los mensajes. Recuerda que este método es el único declarado en la interface MessageListener, la cual deben implementar todos los MDB basados en JMS.
Es interesante explicar con un mínimo detalle las operaciones que se realizan dentro de nuestro método onMessage. Este método requiere un parámetro de tipo javax.jms.Message, que es una interface base de la cual extienden otras interfaces más específicas. Una de esas subinterfaces es TextMessage, la cual provee de métodos para trabajar con mensajes cuyo contenido es texto. Por tanto, este MDB está diseñado para recibir mensajes desde el topic asociado, y procesarlos si son de tipo TextMessage (en caso afirmativo, se imprime en el log del servidor el contenido del mensaje). Si durante dicho procesamiento se produce un error, el MDB lanzará una excepción. Lo ideal es que el topic asociado solo reciba este tipo de mensajes, y que mensajes con otro tipo de contenido sean almacenados en otros canales virtuales (de esta manera no se crearán instancias que finalmente no procesarán el mensaje).
Tras desplegar el proyecto EJB con nuestro primer componente MDB, el último paso lógico sería crear un cliente JMS que produjera mensajes y los enviara al topic. Este cliente podría ser, por ejemplo, un proyecto Java normal y corriente. Puesto que necesitamos las librerias de JMS (las cuales son parte de EJB 3.x), debemos incluirlas al crear nuestro proyecto. La forma más sencilla sería pulsando Next en la pantalla de creación del proyecto Java en Eclipse, seleccionando la pestaña Libraries y añadiendo las librerias del servidor JBoss:
Add Library > Server Runtime > Botón Next > JBoss 6.0 Runtime > Botón Finish > Botón Finish
Ahora ya estamos listos para escribir el cliente productor JMS:
TextMessage mensajeDeTexto = sesion.createTextMessage("Mensaje enviado desde Java");
productor.send(mensajeDeTexto);
conexion.close();
}
}
En nuestro cliente JMS externo al contenedor lo primero que hacemos es crear un objeto de propiedades con los valores necesarios para conectar con el contenedor EJB (como hicimos en el cliente del primer artículo). A continuación creamos un contexto de ejecución desde el que poder acceder al contenedor, y mediante JNDI obtenemos un objeto factoría ConnectionFactory, a través del cual podremos realizar conexiones para el envío de mensajes.
Ahora viene la parte interesante: creamos un objeto Topic que está asociado al canal virtual que hemos declarado en el primer paso lógico de esta sección (mediante su dirección JNDI), creamos una conexión con el broker mediante el objeto factoría, iniciamos una nueva sesión dentro de la conexión recién creada, creamos un objeto MessageProducer asociado al objeto Topic, iniciamos la conexión para poder enviar un mensaje, creamos un mensaje de tipo texto, lo enviamos, y finalmente cerramos la conexión.
Al ejecutar el cliente, el mensaje se enviará al topic, y puesto que existen clientes consumidores asociados a ese topic, el contenedor EJB consumirá el mensaje, extraerá del pool MDB una instancia del componente MDB (o creará la instancia en el aire; a nosotros nos es indiferente), y le pasará el mensaje al método onMessage() de dicha instancia para que procese el mensaje. Si, tras ejecutar el cliente JMS, miras la pestaña Console de Eclipse, verás el mensaje imprimido en el log de JBoss (tal como se definió al escribir el método onMessage).
¿Recuerdas el sencillo esquema de la sección 3.10?:
- El cliente Java es el cliente productor
- JMS (más concretamente su implementación HornetQ) es el broker
- El contenedor EJB es el cliente consumidor
- Una instancia MDB es quien procesa el mensaje
Lo interesante de un servicio de mensajería es que el cliente productor no sabe quien consumirá su mensaje, ni como lo hará. Podría ser un MDB, o podría ser otro componente en nada relacionado con la especificación EJB (tal vez ejecutándose en una máquina remota con un sistema operativo distinto).
Por último, veamos como hacer que un MDB sea también productor:
En el ejemplo anterior, dentro del método onMessage() se llama a un método de utilidad que envia un nuevo mensaje a un segundo topic (que debemos haber declarado con anterioridad, por supuesto). La única diferencia entre el código del método de utilidad y el método main() del cliente Java es que, puesto que el primero se está ejecutando dentro del contenedor EJB, podemos obtener el contexto de ejecución mediante inyección de dependencia, evitando así la necesidad de crear el objeto de propiedades y conectar por red al contenedor (lo cual sería bastante absurdo).
Ahora podemos declarar un segundo MDB que procese los mensajes del segundo topic:
@MessageDriven(activationConfig={
@ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Topic"),
@ActivationConfigProperty(propertyName="destination", propertyValue="topic/SegundoTopic")})
public class SegundoMDB implements MessageListener {
@Override
public void onMessage(Message message) {
if(message instanceof TextMessage) {
try {
String contenidoDelMensaje = ((TextMessage)message).getText();
System.out.println("SegundoMDB ha procesado el mensaje: " + contenidoDelMensaje);
} catch (JMSException jmse) {
throw new RuntimeException("Error al procesar un mensaje");
}
}
}
}
Cuando PrimerMDB reciba un mensaje, la consola de JBoss (pestaña Console de Eclipse) mostrará como ambos MDB han procesado los mensajes de sus topics correspondientes:
19:00:47,453 INFO [STDOUT] PrimerMDB ha procesado el mensaje: Mensaje enviado desde Java
19:00:47,475 INFO [STDOUT] SegundoMDB ha procesado el mensaje: Mensaje enviado desde MDB
3.16 RESUMEN
En este tercer artículo del tutorial de EJB hemos terminado de ver los componentes de lado del servidor, además de algunas características interesantes que son nuevas en la especificación EJB 3.1: llamadas asíncronas y vista sin interface (ambas son aplicables a componentes Session Bean, pero no a Message-Driven Beans).
En el próximo artículo veremos el último tipo de componente EJB que nos queda por ver (Entity Beans), así como la forma de realizar persistencia con JPA en una aplicación EJB 3.1. En ningún caso se explicará a fondo JPA (ni que es, ni como funciona), así que si no conoces este framework te aconsejo que visites el tutorial de cuatro artículos sobre JPA que se encuentra publicado en esta misma web.
En el artículo anterior del tutorial de EJB 3.1 vimos de manera superficial que representa la especificación EJB, que es un contenedor, los tipos de componentes dentro de una aplicación EJB, y por último un sencillo ejemplo de desarrollo de una aplicación EJB 3.1 mediante Test-Driven Development. En este segundo artículo vamos a empezar a profundizar en algunos de esos temas, viendo dos de los tres tipos de Session Beans: Stateless y Stateful. Sin embargo, antes es necesario comprender algunos conceptos relacionados con la tecnología EJB, como el funcionamiento del pool, el uso de metadatos, componentes locales vs componentes remotos, inyección de dependencias, y contexto de sesión dentro de una aplicación EJB. Comencemos.
2.1 METADATOS
Como vimos en la primer artículo del tutorial, para declararar nuestros POJO's como verdaderos componentes EJB necesitamos usar metadatos. Estos metadatos pueden ser de dos tipos:
- Anotaciones
- XML
...
En el artículo anterior del tutorial de EJB 3.1 vimos de manera superficial que representa la especificación EJB, que es un contenedor, los tipos de componentes dentro de una aplicación EJB, y por último un sencillo ejemplo de desarrollo de una aplicación EJB 3.1 mediante Test-Driven Development. En este segundo artículo vamos a empezar a profundizar en algunos de esos temas, viendo dos de los tres tipos de Session Beans: Stateless y Stateful. Sin embargo, antes es necesario comprender algunos conceptos relacionados con la tecnología EJB, como el funcionamiento del pool, el uso de metadatos, componentes locales vs componentes remotos, inyección de dependencias, y contexto de sesión dentro de una aplicación EJB. Comencemos.
2.1 METADATOS
Como vimos en la primer artículo del tutorial, para declararar nuestros POJO's como verdaderos componentes EJB necesitamos usar metadatos. Estos metadatos pueden ser de dos tipos:
- Anotaciones
- XML
El uso de anotaciones es, a priori, el más sencillo y expresivo. Esta forma de aplicar información a nuestros componentes está disponible en la plataforma JavaEE desde su versión 5, y por tanto disponible también en JavaEE 6 (plataforma de la cual forma parte la especificación EJB 3.1). El único punto en contra del uso de anotaciones es que, si deseamos cambiar el comportamiento de un componente, debemos recompilar nuestro código (ya que la anotación acompaña al código Java). Por motivos de simplicidad, esta será la forma de metadatos que se utilizará durante todo el tutorial. Puedes consultar la referencia completa (en inglés) de las anotaciones soportadas en la especificación EJB 3.0 en la siguiente dirección.
El uso de XML para añadir metadatos a nuestro código es la forma heredada de versiones anteriores de la especificación JavaEE. Para ello, debemos incluir un archivo llamado ejb-jar.xml dentro del directorio META-INF del archivo jar/ear a desplegar. La ventaja del uso de XML como fuente de metadatos reside en que no es necesario recompilar nuestro código para cambiar el comportamiento de nuestros componentes, con todas las ventajas que esto puede suponer una vez que un proyecto se encuentra en producción. De manera adicional, al encontrarse los metadatos en un archivo externo, nuestro código Java no contiene ningún tipo de información relativa a la especificación EJB, y por tanto es más portable. Por contra, además de tener que mantener dos fuentes de información a la vez (el código Java y el archivo XML), el propio archivo XML puede ser dificil de mantener y entender cuando alcanza cierta longitud y complejidad. En este tutorial no se verán ejemplos de metadados en XML por motivos de simplicidad, aunque si deseas ampliar información puedes visitar el capítulo 19 de la especificación EJB 3.1, la cual puedes descargar desde la siguiente dirección.
Por último, debes tener muy presente que los metadatos en formato XML sobreescriben cualquier comportamiento ya expresado mediante anotaciones, siempre que ambos hagan referencia a un mismo componente. Por ello, una combinación de ambos tipos de metadatos sería perfectamente legal, aunque salvo honrosas excepciones, poco recomendable (puede inducir a la confusión, y por tanto a cometer errores).
2.2 EL POOL
Un pool es, expresado de forma básica, un almacén de objetos. El contenedor EJB mantiene uno o varios pools donde almacena componentes que están listos para ser servidos a un cliente que los solicite. De esta manera, el contenedor gestiona la creación, mantenimiento, y destrucción de componentes en segundo plano y de manera transparente a la aplicación EJB (mejorando así el rendimiento de esta última y de sus clientes).
Cuando un cliente obtiene una referencia, ya sea local (mediante @EJB, como veremos en la sección 2.5), o remota (mediante JNDI, como vimos en el ejemplo al final del artículo anterior), esta no apunta a ninguna instancia del componente en cuestión. Solamente cuando se invoca un método en dicha referencia, el contenedor extrae una instancia del pool y la asigna a dicha referencia, de manera que la invocación pueda tener efecto.
El comportamiento de multitud de aspectos del pool es configurable, aunque en este tutorial no necesitamos hacerlo en ningún momento. Los detalles concretos del funcionamiento del pool es algo que tampoco necesitamos saber de antemano para seguir el tutorial; cuando se necesite comprender un aspecto concreto de dicho funcionamiento, se explicará en la sección correspondiente.
2.3 LOCAL VS REMOTO
Como se explicó muy brevemente en el primer artículo del tutorial, un Session Bean puede ser declarado de tipo local o remoto. Un Session Bean declarado como local estará destinado a servir solicitudes de otros componentes dentro de la misma Java Virtual Machine (JVM - Máquina Virtual Java) donde está desplegado (dicho con otras palabras, dentro del contenedor donde se ejecuta la aplicación). El contenedor pasará la referencia que apunta al objeto en cuestión al cliente, pues dentro de la misma JVM está referencia es válida. Por contra, un Session Bean declarado como remoto está destinado a servir peticiones de clientes externos al propio contenedor (como vimos en el ejemplo al final del artículo anterior). En este caso, lo que se pasa es una copia del objeto en cuestión, pues para una JVM externa no tendrá ningun sentido una referencia a un objeto que no se encuentre en ella misma. Recuerda que esa copia no es una copia del componente EJB (una instancia del POJO desplegado en el contenedor), si no una vista/proxy que sabe como acceder a través de una red al objeto real.
La especificación EJB no permite que la interface (en el sentido de contrato) de un Session Bean sea declarada local y remota al mismo tiempo. Por tanto, lo siguiente no es válido y producirá un error al ser desplegado:
@Local
@Remote
@Stateless
public class MiBean implements MiInterfaceRemote {
// esta clase produce un error al desplegar
}
En el ejemplo anterior, la clase MiBean actua como interface local y remota al mismo tiempo, lo cual no está permitido. Sin embargo, lo siguiente si que es válido:
@Local
public interface MiBeanLocalInterface {
// declaración de métodos locales
}
@Remote
public interface MiBeanRemoteInterface {
// declaración de métodos remotos
}
@Stateless
public class MiBean implements MiBeanLocalInterface, MiBeanRemoteInterface {
// implementación de métodos locales y remotos
}
En el ejemplo anterior, declaramos una interface local y otra remota, y las implementamos en un Session Bean de tipo Stateless. De esta manera, el Session Bean podrá servir solicitudes de ambos tipos (cada una a través de la interface correspondiente). Otra forma de expresar lo mismo sería de la siguiente manera:
public interface MiBeanLocalInterface {}
public interface MiBeanRemoteInterface {}
@Local(PrimerBeanLocalInterface.class)
@Remote(PrimerBeanRemoteInterface.class)
@Stateless
public class MiBean implements MiBeanRemoteInterface {}
En el ejemplo anterior, hemos eliminado las anotaciones en las interfaces y las hemos aplicado a la clase MiBean. Añadiendo los argumentos correspondientes en @Local y @Remote le indicamos al contenedor que interface actuará como local y cual como remota. Seguimos teniendo que implementar la interface remota en la declaración de la clase (implements MiBeanRemoteInterface), pues como se vio en el artículo anterior, todo Session Bean declarado como remoto necesita implementar una interface de manera obligatoria (dicha interface es necesaria para la creación del proxy/vista).
Por supuesto también podemos aprovechar las características de polimorfismo y herencia en Java para crear nuestros componentes:
public interface MiInterfaceBase {
// contrato general para todas las interfaces que hereden de esta
}
public interface MiInterfaceLocal extends MiInterfaceBase {
// contrato concreto para acceso local
}
public interface InterfaceRemote extends MiInterfaceBase {
// contrato concreto para acceso remoto
}
public abstract class MiBeanBase implements MiInterfaceBase {
// implementación general para todas las clases que hereden de esta
}
@Stateless
@Local(MiInterfaceLocal.class)
public class MiBeanLocal extends MiBeanBase implements MiInterfaceLocal {
// implementación concreta para acceso local
}
@Stateless
@Remote(MiInterfaceRemote.class)
public class MiBeanRemote extends MiBeanBase implements MiInterfaceRemote {
// implementación concreta para acceso remoto
}
En este tutorial los ejemplos se mantendrán siempre lo más simple posibles, pues a efectos lectivos toda la parafernalia del ejemplo anterior no es necesaria (de hecho es contraproducente). El aspecto clave a recordar estriba en no declarar una misma interface (repito, en el sentido de contrato) de tipo local y remota al mismo tiempo.
2.4 EJB CONTEXT Y SESSION CONTEXT
A veces, necesitamos acceder al contexto de ejecución del componente que se está usando. EJBContext (Contexto de EJB) es una interface que provee acceso al contexto de ejecución asociado a cada instancia de un componente EJB. A través de él podemos acceder, por ejemplo, al servicio de seguridad: imaginemos que invocamos un método en un Session Bean dentro de una aplicación donde hay restricciones de seguridad, de manera que el Session Bean necesita comprobar si el cliente que lo está invocándo está autorizado o no a hacerlo. A través del contexto de ejecución de dicho Session Bean podemos acceder al servicio de seguridad y realizar dicha comprobación. Otros ejemplos del uso del contexto de ejecución son el acceso a transacciones, u obtener el Timer Service (Servicio de Reloj, necesario para programar eventos mediante unidades de tiempo, como veremos en un artículo posterior).
SessionContext (Contexto de Sesión) es una interface que implementa EJBContext, añadiendo métodos que permiten el uso de servicios adicionales. A través de él podemos, por ejemplo, obtener una referencia al Session Bean actual para poder pasarla a otro Session Bean como argumento de un método; esto es necesario, pues el contenedor no permite pasar el Session Bean actual usando una referencia de tipo this (los detalles de porqué esto es así no son necesarios para entender el material, y por tanto los omito). Podemos acceder al contexto de sesión asociado a la instancia del componente actual mediante inyección de dependencia (asunto que se explicará en la sección 2.5):
@Local
@Stateless
public class MiBean {
@Resource
private SessionContext contexto;
// ...
}
Mediante la anotación @Resource (Recurso) indicamos al contenedor que debe inyectar el contexto de sesión asociado a dicha instancia en la variable contexto (cuando veamos el ciclo de vida de los componentes EJB se verá cómo y cuándo se realiza esta operación). Otra manera de acceder al contexto de sesión es usando la misma anotación, pero sobre un método setter que siga el estandar JavaBean (no confundir con Enterprise JavaBean):
@Local
@Stateless
public class MiBean {
private SessionContext contexto;
@Resource
public void setContexto(SessionContext contexto) {
this.contexto = contexto;
}
// ...
}
Puesto que SessionContext extiende EJBContext, podemos acceder a ambas interfaces desde la primera. Es muy importante que tengas presente que ciertos métodos en EJBContext están marcados como deprecated, y además lanzarán una excepción de tipo RuntimeException si son invocados. Te recomiendo que, por este motivo, consultes la API de EJBContext en la siguiente dirección.
2.5 INYECCIÓN DE DEPENDENCIAS
La inyección de dependencias (Dependency Injection) es un proceso por el cual el contenedor puede inyectar en un componente recursos que son necesarios. Un ejemplo de inyección de dependencia lo vimos en la sección anterior, donde usamos la anotación @Resource para inyectar una instancia de SessionContext en un SLSB. Otro ejemplo de inyección de dependencia surje cuando uno de nuestros componentes necesita de otro componente:
@Stateless
public class UnComponente {
private OtroComponente dependencia;
public String metodo() {
return dependencia.otroMetodo();
}
}
En el ejemplo anterior, el Session Bean UnComponente requiere otro Session Bean de tipo OtroComponente. Puesto que, como ya se ha explicado, el contenedor EJB gestiona el ciclo de vida de todos los componentes (toda la aplicación EJB funciona en un entorno gestionado), una instanciación del tipo new OtroComponente() sería incorrecta (esta instancia no tendría, por ejemplo, un contexto de sesión asociado, pues ha sido inicializada manualmente). La solución consiste en informar al contenedor de dicha dependencia mediante la anotación @EJB, dejando así en sus manos esta tarea:
@Local
@Stateless(name="otroComponente")
public class OtroComponente {
public String otroMetodo() {
// ...
}
}
@Stateless
public class UnComponente {
@EJB(beanName="otroComponente")
private OtroComponente dependencia;
public String metodo() {
return dependencia.otroMetodo();
}
}
Gracias a la anotación @EJB el contenedor sabe que componente instanciar, inicializar, y finalmente inyectar en la variable dependencia. En el ejemplo anterior, se registra un Session Bean con nombre otroComponente (gracias a @Stateless(name="otroComponente")), el cual puede ser accedido mediante inyección de dependencia gracias a @EJB(beanName="otroComponente").
La inyección de dependencias es una poderosa carecterística de EJB que libera al programador de muchas responsabilidades, ademas de resultar en código poco acoplado y apto para unit testing (entre otras buenas cualidades). Puedes encontrar más información sobre la anotación @EJB en la aquí y sobre la anotación @Resourceaquí (ambas páginas en inglés).
2.6 COMPONENTES DEL LADO DEL SERVIDOR
La primera pregunta que nos debemos hacer es: ¿Que es un componente del lado del servidor?. No es ni más ni menos que un componente que es gestionado de manera integra por el contenedor. Esto es, el cliente del componente no interacciona con el componente en si, si no con una representación (referencia/proxy/vista) del componente real. Los componentes del lado del servidor son dos:
- Todos los Session Bean (Stateless, Stateful, y Singleton)
- Message-Driven Beans
En este artículo veremos los dos primeros tipos de Session Beans, y dejaremos el componente Singleton y los Message-Driven Beans para el próximo artículo. Ahora otra buena pregunta sería: ¿Que NO es un componente del lado del servidor? La respuesta es: Entity Beans (EB - entidades). Los EB viajan entre el cliente y el servidor siendo siempre la representación del mismo objeto Java en ambos lados. Todo lo relacionado con EB se tratará en el capítulo relacionado con persistencia (si estás interesado en este tema puedes visitar un tutorial sobre Java Persistent API (JPA - API de Persistencia en Java) publicado en esta misma web en la siguiente dirección).
2.7 STATELESS SESSION BEANS: CONCEPTOS BÁSICOS
Como vimos brevemente en el primer artículo del tutorial, los Stateless Session Bean (SLSB - Session Bean Sin Estado) son aquellos que no mantienen estado entre diferentes invocaciones de sus métodos. Podriamos considerar cada método dentro de un SLSB como un servicio en si mismo, que realiza una tarea y devuelve un resultado (puede no hacerlo) sin depender de invocaciones anteriores o posteriores sobre él mismo o sobre otro método. Debido a este comportamiento, el contenedor puede utilizar un número relativamente bajo de SLSB's para servir llamadas de muchos clientes (ya que ninguno de estos clientes estará asociado a un SLSB en concreto), y por ello son muy eficientes. El hecho de no mantener un estado entre invocaciones los hace aún más eficientes.
Es importante destacar que un SLSB si puede mantener un estado interno, aunque este no debe escapar nunca hacia el cliente. Un ejemplo de este estado interno sería una variable que almacenara el número de veces que una instancia en concreto ha sido invocada: un cliente que dependa del valor de esta variable podría obtener un resultado distinto en cada llamada al SLSB, ya que el contenedor no garantiza devolver la misma instancia al mismo cliente. La regla es: si un SLSB mantiene estado, este debe ser interno (invisible para el cliente).
2.8 STATELESS SESSION BEANS: EL CICLO DE VIDA
El ciclo de vida de un componente describe los distintos estados (gestionados íntegramente por el contenedor) por los que puede pasar cada instancia del componente desde que es creada hasta que es destruida. Para los SLSB, este ciclo de vida es muy simple y consta únicamente de dos estados (incluyo el término original en inglés):
- No existe (Does not exists)
- Preparado en pool (Method-ready pool)
El primer estado, no existe, es autoexplicativo: la instancia del SLSB no ha sido creada aún. El segundo estado, Preparado en pool, representa una instancia del SLSB que ha sido instanciada y construida por el contenedor, y se encuentra en el pool lista para recibir invocaciones por parte de un cliente (cuando esta invocación sucede, la instancia es extraida el pool y asociada a una referencia que es pasada al cliente). A este estado se llega durante el arranque del contenedor (el pool es poblado con cierto número de instancias), y cuando no existan suficientes instancias en el pool para servir llamadas de clientes (y por tanto se deban crear más). En resumen, siempre que una nueva instancia del SLSB sea creada.
Durante la transición entre el primer estado y el segundo, el contenedor realizará tres operaciones (en este orden):
- Instanciación del SLSB
- Inyección de cualquier recurso necesario y de dependencias
- Ejecución de un método dentro del SLSB marcado con la anotación @PostConstruct, si existe
La instanciación del SLSB se lleva a cabo mediante reflexión, a traves de Class.newInstance(). Por tanto, el SLSB debe tener un constructor por defecto (sin argumentos), ya sea de forma implícita o explícita.
La inyección de recursos y de dependencias se vio en las secciones 2.4 y 2.5, donde se utilizaron las anotaciones @Resource y @EJB para tales efectos. Estos y otros recursos son inyectados en el SLSB de forma automática por el contenedor durante esta operación. De manera adicional, cada vez que un SLSB sea invocado por un nuevo cliente, todos los recursos son inyectados de nuevo.
Por último, el contenedor ejecutará un método dentro del SLSB que esté anotado con @PostConstruct (Post construcción), si existe. Esta anotación declara dicho método (que puede ser uno y solo uno) como un método callback, esto es, un método que reacciona ante cierto evento (en este caso, a la construcción de la instancia, como su propio nombre indica). Dentro de este método tenemos la oportunidad de adquirir recursos adicionales necesarios para el SLSB, como una conexión de red o de base de datos. Este método debe ser void, no aceptar parámetros, y no lanzar ninguna excepción de tipo checked. Al contrario que la inyección de recursos, la ejecución del método @PostConstruct se ejecuta una y solo una vez (al final de la instanciación del componente).
Cuando el contenedor no necesita una instancia de SLSB, ya sea porque decide reducir el número de instancias en el pool, o porque se está produciendo un shutdown del servidor, se realiza una transición en sentido inverso: del estado preparado en pool al estado no existe. Durante esta transición se ejecutará, si existe, un método anotado con @PreDestroy (Pre destrucción), el cual es también un método callback, y el cual nos da la oportunidad de liberar cualquier recurso adquirido durante la construcción de la instancia (adquisición que hicimos en @PostConstruct). Al igual que ocurría con @PostConstruct, un método anotado con @PreDestroy es opcional, debe ser void, no aceptar parámetros, no lanzar ninguna excepción de tipo checked, solo puede ser usado en un único método, y es ejecutado una y solo una vez (durante la destrucción de la instancia).
2.9 STATELESS SESSION BEANS: UN SENCILLO EJEMPLO
Supongamos que estamos desarrollando una aplicación de banca, y entre otras cosas necesitamos escribir código que represente los conceptos de ingreso, retirada, y transferencia de efectivo. Estos tres conceptos son operaciones que no requieren mantener un estado dentro de la aplicación (esto es, una vez invocadas y terminadas no dependen de otras operaciones) y por tanto son excelentes candidatas para ser representadas mediante un Session Bean de tipo Stateless. Como ya hemos visto en multitud de ejemplos, para declarar un Session Bean de tipo Stateless utilizamos la anotación @Stateless (se omiten otras anotaciones como @Remote o @Local, así como todo código no estrictamente necesario para explicar el tema tratado; desde este momento se dará por hecho esto en la mayoría de los ejemplos que veamos):
@Stateless
public class OperacionesConEfectivo {
public void ingresarEfectivo(Cuenta cuenta, double cantidad) {
// ingresar cantidad en cuenta
}
public void retirarEfectivo(Cuenta cuenta, double cantidad) {
//retirar cantidad de cuenta
}
public void transferirEfectivo(Cuenta cuentaOrigen, Cuenta cuentaDestino, double cantidad) {
// transferir cantidad de cuenta origen a cuenta destino
}
}
La clase Cuenta representa una cuenta bancaria, y teóricamente lo representaríamos mediante un componente de tipo Entity Bean (Bean de Entidad), los cuales veremos en el cuarto artículo de este tutorial. Lo realmente importante del ejemplo anterior es el concepto de operaciones que no requieren estado, y por tanto pueden ser representadas mediante Session Beans de tipo Stateless.
2.10 STATEFUL SESSION BEANS: CONCEPTOS BÁSICOS
Al contrario que los Session Bean de tipo Stateless, los Stateful Session Bean (SFSB - Session Bean con Estado) mantienen estado entre distintas invocaciones de un mismo cliente. Podríamos considerar un SFSB como una extensión del cliente en el contenedor, ya que cada SFSB está dedicado de manera exclusiva a un único cliente durante todo su ciclo de vida. Otra diferencia entre SLSB y SFSB es que estos últimos no son almacenados en un pool, pues no son reusados, como veremos en la sección 2.11 cuando expliquemos su ciclo de vida.
¿A que nos referimos cuando decimos que un SFSB mantiene un estado? La cuestión es simple: un SFSB almacena información a consecuencia de las operaciones que realiza en él su cliente asociado, y dicha información (estado) debe estar disponible en invocaciones posteriores. De esta manera, un SFSB puede realizar una acción compleja mediante multiples invocaciones. Un ejemplo muy común es el carrito de la compra de una tienda online; añadimos y eliminamos artículos del carrito en diferentes invocaciones, lo cual sería imposible de realizar con un SLSB.
A pesar de mantener estado, un SFSB no es persistente, de manera que dicho estado se pierde cuando la sesión del cliente asociado termina. Su misión es servir como lógica de negocio (misión de todos los Session Bean) y nada más. Para persistir dicha información deberemos usar otro tipo de componente EJB (Entity Bean) el cual veremos en el artículo sobre persistencia.
2.11 STATEFUL SESSION BEANS: EL CICLO DE VIDA
El ciclo de vida de un SFSB es ligeramente más complejo que el de su hermano pequeño SLSB, y consta de tres estados:
- No existe (Does not exists)
- Preparado (Method-ready)
- Pasivo (Passive)
El primer estado, no existe, es similar al de un SLSB: la instancia del SFSB no ha sido creada aún. El segundo estado, preparado, representa una instancia del SFSB que ha sido construida e inicializada, y está lista para servir llamadas de su cliente asociado (recuerda que esta asociación perdura durante toda la vida del SFSB). El tercer estado, pasivo, representa un SFSB que, después de un periodo de inactividad, es persistido temporalmente para liberar recursos del servidor.
Durante la transición entre el primer estado y el segundo, el contenedor realizará las siguientes tres operaciones:
- Instanciación del SFSB
- Inyección de cualquier recurso necesario y de dependencias, y asociación con el cliente
- Ejecución de un método dentro del SFSB marcado con la anotación @PostConstruct, si existe
La instanciación del SFSB se lleva a cabo mediante reflexión, a traves de Class.newInstance(). Por tanto, el SFSB debe tener un constructor por defecto (sin argumentos), ya sea de forma implícita o explícita.
La inyección de recursos y de dependencias se vio en las secciones 2.4 y 2.5, donde se utilizaron las anotaciones @Resource y @EJB para tales efectos. Estos y otros recursos son inyectados en el SFSB de forma automática por el contenedor durante esta operación. Una vez finaliza la inyección de recursos y dependencias, el SFSB es asociado al cliente que ha generado su creación.
Por último, el contenedor ejecutará un método dentro del SFSB que esté anotado con @PostConstruct (Post construcción), si existe. Esta anotación declara dicho método (que puede ser uno y solo uno) como un método callback, esto es, un método que reacciona ante cierto evento (en este caso, a la construcción de la instancia, como su propio nombre indica). Al igual que con un SLSB, la función de este método es adquirir recursos adicionales que sean necesarios para el SFSB, como una conexión de red o de base de datos. Y también al igual que su homónimo en un SLSB, este método debe ser void, no aceptar parámetros, no lanzar ninguna excepción de tipo checked, y no ser static ni final. La ejecución del método @PostConstruct se ejecuta una y solo una vez, al final de la instanciación del componente. En este momento, el SFSB ya se encuentra en el estado preparado, y procede a servir la llamada del cliente que ha generado la creación de la instancia.
Por último, un SFSB en estado preparado puede realizar una transición a cualquiera de los otros dos estados de su ciclo de vida: no existe o pasivo. La transición al estado no existe ocurre cuando:
- El cliente ejecuta un método dentro del SFSB anotado con @Remove - El SFSB sobrepasa un periodo de inactividad establecido (timeout)
En el primer caso, la ejecución de un método anotado con @Remove informa al contenedor que el cliente ha terminado de usar su SFSB asociado y, por tanto, dicha instancia ya no es necesaria. En este momento la instancia es desasociada de su contexto de sesión y eliminada. Este método no es de tipo callback (como @PostConstruct o @PreDestroy), pues no reacciona a un evento, si no que es ejecutado explicitamente por el cliente. En el segundo caso (timeout), el SFSB ha sobrepasado un periodo de tiempo establecido y el contenedor decide eliminarlo, previa ejecución de un método (uno y solo uno) anotado con @PreDestroy, y con iguales reglas que en su homónimo en SLSB: opcional, void, sin parámetros, sin excepciones de tipo checked, no static, y no final.
Por otro lado, la transición al estado pasivo se produce cuando el contenedor decide liberar recursos, persistiendo de manera temporal el estado del SFSB. La pasivación y posterior activación de un SFSB requiere una sección exclusiva que veremos enseguida.
Por finalizar con el ciclo de vida de un componente SFSB, ten presente que si este lanza una excepción de sistema (cualquiera de tipo unchecked cuya definición no esté anotada con @ApplicationException), se producirá una transición al estado no existe sin llamar a su método @PreDestroy. Este comportamiento no está muy bien comprendido entre cierta parte de la comunidad, pues a todas luces no parece muy correcto, pero es así y debes tenerlo en cuenta.
2.12 STATEFUL SESSION BEANS: PASIVACIÓN Y ACTIVACIÓN
Durante la vida de un SFSB, el contenedor puede decidir liberar recursos persistiendo de manera temporal el estado de dicho SFSB, liberando de esta manera memoria del sistema (u otros recursos). A esto lo llamamos pasivación. Si durante la pasivación el cliente realiza una llamada al SFSB, el contenedor recreará en memoria la instancia persistida y procedera con dicha llamada; a esto lo llamamos activación. Todo el ciclo de activación-pasivación puede ocurrir múltiples veces en la vida de un SFSB, o ninguna en absoluto. Sea como sea, es un proceso gestionado por el contenedor y totalmente transparence al cliente, el cual no sabe en ningún momento si se ha producido una pasivación o activación del SFSB asociado a su sesión.
No toda la información (estado) almacenada en un SFSB puede ser persistida. Los siguientes tipos son pasivados por defecto:
- Todos los tipos primitivos
- Objetos que implementan la interface Serializable - Referencias a factorias de recursos gestionados por el contenedor (como javax.sql.DataSource)
- Referencias a otros componentes EJB
- javax.ejb.SessionContext - javax.jta.UserTransaction - javax.naming.Context - javax.persistence.EntityManager - javax.persistence.EntityManagerFactory
Para todos los demás tipos, la especificación EJB nos proporciona dos métodos callback para controlar la pasivación-activación de un SFSB cuando existen datos que no deben/pueden ser persistidos: @PrePassivate (Pre-pasivación) y @PostActivate (Post-activación). Ambas anotaciones son aplicadas en métodos dedicados a controlar cada uno de los procesos, según corresponda (en la sección 2.13 veremos un ejemplo de un SFSB con métodos de pasivación y activación).
@PrePassivate nos permite anotar un método (opcional) que deje el SFSB es un estado adecuado para su pasivación. Puesto que la pasivación se lleva a cabo mediante serialización, todas las referencias a objetos no serializables deben ser puestas a null. Otra operacion llevada a cabo mediante @PrePassivate es la liberación de recursos que no pertenezcan a la lista anterior. Una vez ejecutado el método @PrePassivate, el SFSB es serializado y persistido temporalmente (tal vez en disco, en una cache, etc; esto es decisión de cada implementación de EJB). Es importante tener en cuenta que si un SFSB en estado pasivo sobrepasa un periodo de inactividad establecido (timeout), el contenedor eliminará la instancia persistida sin ejecutar su método @PreDestroy.
Si durante la pasivación el cliente asociado realiza una llamada al SFSB, el contenedor realizará la activación del componente, deserializando la instancia desde el lugar donde se encuentra persistida, restaurándola al estado anterior a producirse la pasivación (inyección del contexto de sesión, inyección de referencias a otros componentes EJB, etc), y ejecutando un método anotado con @PostActivate (también opcional). En este método podemos inicializar todos los objetos no serializables a unos valores adecuados (no debes confiar en los valores por defecto que el contenedor da a objetos no serializables, pues entre implementaciones pueden ser diferentes), así como restaurar cualquier recurso necesario para el correcto funcionamiento del SFSB. Una vez que la activación ha concluido, el SFSB puede gestionar la llamada del cliente que generó dicha activación.
Tanto @PrePassivate como @PostActivate deben seguir las siguientes reglas:
- El tipo de retorno debe ser void - No puede aceptar ningún parámetro
- No puede lanzar ninguna excepción de tipo checked - No puede ser static ni final
Estas reglas son las que se han aplicado a todos los métodos callback vistos hasta ahora, por lo que una vez aprendidas podrás aplicarlas fácilmente cuando escribas cualquier método callback.
2.13 STATEFUL SESSION BEANS: UN EJEMPLO SENCILLO
Supongamos que estamos diseñando una aplicación de tienda online, con un carrito de la compra donde los clientes pueden añadir artículos, eliminarlos, vaciar el carrito, o realizar un pedido. Puesto que cada una de estas operaciones se lleva a cabo de manera independiente a las demas, pero los cambios de cada una de ellas pueden afectar al resto, necesitamos mantener un estado entre distintas invocaciones, y por tanto un componente Session Bean de tipo Stateful. Para declarar un SFSB usamos la anotación @Stateful y hacemos que la clase en cuestión implemente la interface Serializable (puesto que es este el mecanismo que utilizará el contenedor para la pasivación-activación). El ejemplo, como siempre, es una estructura básica sin apenas implementación: el objetivo del ejemplo es únicamente mostrar donde encaja cada pieza:
@Stateful
public class Carrito implements Serializable {
private Map articulosEnCarrito = new HashMap();
private BaseDeDatos bbdd;
public void añadirArticulo(Articulo articulo, int cantidad) {
// añadir la cantidad de cierto artículo al carrito
}
public void eliminarArticulo(Articulo articulo, int cantidad) {
// eliminar la cantidad de cierto artículo del carrito
}
public void vaciarCarrito() {
// vaciar el carrito
}
@Remove
public void finalizarCompra() {
// procesar el pedido
}
@PostConstruct
@PostActivate
private void inicializar() {
// obtener conexión con la base de datos
}
@PrePassivate
@PreDestroy
private void detener() {
// liberar conexión con la base de datos
}
}
Como puedes ver, la clase Carrito está anotada con @Stateful y marcada como Serializable. Dentro de ella tenemos dos campos: artículosEnCarrito, donde se almacena el estado requerido para el correcto funcionamiento del proceso de compra (HashMap es de tipo Serializable y por tanto es persistida de forma automática en caso de pasivación), y BaseDeDatos, que es una clase ficticia que supuestamente nos permite realizar operaciones sobre una base de datos, pero no puede ser pasivada (supongamos que mantiene una conexión con una base de datos real que no puede permanecer abierta durante la pasivación).
Dentro de la clase, los cuatro primeros métodos nos permiten realizar operaciones de lógica de negocio, como añadir cierta cantidad de cierto artículo al carrito, vaciar el carrito completamente, o procesar el pedido una vez añadidos los artículos deseados. El cuarto método (finalizarCompra), de manera adicional, indica al contenedor que hemos terminado de trabajar con el componente SFSB y que por tanto puede eliminarlo (gracias a la anotación @Remove). Estos cuatro métodos son la interface que mostramos al cliente, y por tanto todos ellos son declarados public.
Los dos últimos métodos (inicializar y detener) se encargar de obtener y liberar recursos en momentos clave del ciclo de vida del SFSB. Fíjate como @PostConstruct y @PostActivate anotan el mismo método, pues todas las operaciones que hay en él son comunes a los procesos de construcción del SFSB y una posible activación posterior. De igual manera, @PrePassivate y @PreDestroy acompañan al mismo método, por el mismo motivo. Ambos métodos, de tipo callback, son gestionados por el contenedor en base a eventos, y por tanto el cliente del SFSB no necesita saber de su existencia (nosotros los hemos declarado private, aunque cualquier nivel de visibilidad está permitido).
2.14 RESUMEN
En este segundo artículo del tutorial de EJB hemos visto conceptos relacionados con la tecnología EJB, como la definición del pool, local vs remoto, metadatos, inyección de dependencias, y contexto de sesión. Ademas, hemos visto en cierta profundidad los dos tipos de Session Bean más comunes: Stateless y Stateful.
En el próximo artículo veremos el tercer y último tipo de Session Bean (Singleton) así como un nuevo tipo de componente: Message-Driven Beans. Hasta entonces, ¡feliz inyección de dependencias!
Con este artículo comienza un tutorial que describe el estandar EJB 3.1 de manera introductoria. El tutorial contiene tanto material teórico como práctico, de manera que según se vayan introduciendo nuevos conceptos se irán reflejando en código. Es muy recomendable que antes de seguir leyendo visites el siguiente anexo donde se explica como poner en marcha un entorno de desarrollo compatible con EJB 3.1, de manera que puedas seguir todos los ejemplos que se van a presentar en el tutorial, así como desarrollar los tuyos propios.
La siguiente lista muestra el contenido de los primeros 6 artículos de los que constará el tutorial. Esta lista se actualizará si se publica contenido adicional:
- 1. Introducción a EJB y primer ejemplo
- 2. Stateless Session Beans y Stateful Session Beans
- 3. Singletons y Message-Driven Beans
- 4. Persistencia
- 5. Servicios que ofrece el contenedor (1ª parte)
- 6. Servicios que o...
Con este artículo comienza un tutorial que describe el estandar EJB 3.1 de manera introductoria. El tutorial contiene tanto material teórico como práctico, de manera que según se vayan introduciendo nuevos conceptos se irán reflejando en código. Es muy recomendable que antes de seguir leyendo visites el siguiente anexo donde se explica como poner en marcha un entorno de desarrollo compatible con EJB 3.1, de manera que puedas seguir todos los ejemplos que se van a presentar en el tutorial, así como desarrollar los tuyos propios.
La siguiente lista muestra el contenido de los primeros 6 artículos de los que constará el tutorial. Esta lista se actualizará si se publica contenido adicional:
- 1. Introducción a EJB y primer ejemplo
- 2. Stateless Session Beans y Stateful Session Beans
- 3. Singletons y Message-Driven Beans
- 4. Persistencia
- 5. Servicios que ofrece el contenedor (1ª parte)
- 6. Servicios que ofrece el contenedor (2ª parte)
Con todo esto dicho, ¡comencemos!
1.1 ¿QUE SON LOS ENTERPRISE JAVABEANS?
EJB (Enterprise JavaBeans) es un modelo de programación que nos permite construir aplicaciones Java mediante objetos ligeros (como POJO's). Cuando construimos una aplicación, son muchas las responsabilidades que se deben tener en cuenta, como la seguridad, transaccionalidad, concurrencia, etc. El estandar EJB nos permite centrarnos en el código de la lógica de negocio del problema que deseamos solucionar y deja el resto de responsabilidades al contenedor de aplicaciones donde se ejecutará la aplicación.
1.2 EL CONTENEDOR DE APLICACIONES
Un contenedor de aplicaciones es un entorno (en si mismo no es más que una aplicación) que provee los servicios comunes a la aplicacion que deseamos ejecutar, gestionándolos por nosotros. Dichos servicios incluyen la creación/mantenimiento/destrucción de nuestros objetos de negocio, así como los servicios mencionados en el punto anterior, entre otros. Aunque el contenedor es responsable de la gestión y uso de dichos recursos/servicios, podemos interacturar con él para que nuestra aplicación haga uso de los servicios que se ofrecen (normalmente mediante metadados, como se verá a lo largo del tutorial).
Una vez escrita nuestra aplicación EJB, podemos desplegarla en cualquier contenedor compatible con EJB, beneficiandonos de toda el trabajo tras bastidores que el contenedor gestiona por nosotros. De esta manera la lógica de negocio se mantiene independiente de otro código que pueda ser necesario, resultando en código que es más fácil de escribir y mantener (además de ahorrarnos mucho trabajo).
1.3 LA ESPECIFICACIÓN EJB 3.1
La especificación EJB 3.1 es parte de la plataforma JavaEE 6, desarrollada y mantenida por Sun Microsystems (ahora parte de Oracle Corporation). JavaEE 6 provee diversas API's para la construcción de aplicaciones empresariales, entre ellas EJB, JPA, JMS, y JAX-WS. Cada una de ellas se centra en un area específica, resolviendo así problemas concretos. Además, cada API/especificación está preparada para funcionar en compañia de las demás de forma nativa, y por tanto en su conjunto son una solución perfectamente válida para desarrollar una aplicación end-to-end (de principio a fin).
Desde la versión 3.0, EJB no impone ninguna restricción ni obligación a nuestros objetos de negocio de implementar una API en concreto. Dicho de otro modo, podemos escribir dichos objetos de negocio usando POJO's, facilitando entre otras cosas la reutilización de componentes y la tarea de testearlos.
Como se ha dicho, los POJO's son faciles de testear (siempre que estén bien diseñados). Al final de este primer artículo se verá un sencillo ejemplo de programación de un EJB mediante Test-Driven Development (Desarrollo Dirigido por Tests). TDD es una metodología de desarrollo en la cual cada bloque de código está respaldado por uno o más tests que han sido escritos con anterioridad. De manera muy resumida, TDD nos permite enfocar de manera efectiva el problema que deseamos resolver de la siguiente manera:
- Escribimos un test que define que queremos hacer.
- Ejecutamos el test y este falla (puesto que aún no hay lógica de negocio, o lo que es lo mismo, como queremos hacerlo).
- Escribimos la lógica de negocio que hace pasar el test (la solución más simple posible).
- Mejoramos la lógica de negocio gradualmente, ejecutando el test después de cada mejora para verificar que no hemos roto nada.
Escribir el test antes que la lógica de negocio y mantenerlo lo más simple posible nos obliga a escribir código independiente de otro código, con responsabilidades bien definidas (en resumen, buen código). Usado de forma correcta, TDD permite crear sistemas que son escalables, y con niveles de bugs muy bajos. TDD es un tema tan amplio en si mismo que no tiene cabida en este tutorial (a excepción del citado ejemplo que veremos al final del artículo y que servirá solamente para demostrar que el modelo EJB es un buen modelo de programación), y en el que te animo que profundices si no lo conoces; las ventajas que ofrece para escribir código de calidad son muchas, independientemente del uso de EJB o no.
Por otro lado, el uso de POJO's para encapsular nuestra lógica de negocio nos proporciona un modelo simple que es altamente reutilizable (recuerda que la reutilización de clases es un concepto básico y esencial en programación orientada a objetos). Debes tener en cuenta que un POJO no actuará como un componente EJB hasta que haya sido empaquetado, desplegado en un contenedor EJB y accedido por dicho contenedor (por ejemplo a petición de un usuario). Una vez que un POJO definido como EJB haya sido desplegado en el contenedor, se convertirá en uno de los tres siguientes componentes (dependiendo del como lo hayamos definido):
Veamos una breve descripción de cada tipo de componente (en capítulos posteriores se explicará cada tipo de componente con más detalle).
1.4 SESSION BEANS
Los Session Beans (Beans de Sesión) son los componentes que contienen la lógica de negocio que requieren los clientes de nuestra aplicación. Son accedidos a través de un proxy (también llamado vista, término que utilizaré en adelante) tras realizar una solicitud al contenedor. Tras dicha solicitud, el cliente obtiene una vista del Session Bean, pero no el Session Bean real. Esto permite al contenedor realizar ciertas operaciones sobre el Session Bean real de forma transparente para el cliente (como gestionar su ciclo de vida, solicitar una instancia a otro contenedor trabajando en paralelo, etc).
Los componentes Session Bean pueden ser de tres tipos:
Stateless Session Beans (SLSB - Beans de Sesión sin Estado) son componentes que no requieren mantener un estado entre diferentes invocaciones. Un cliente debe asumir que diferentes solicitudes al contenedor de un mismo SLSB pueden devolver vistas a objetos diferentes. Dicho de otra manera, un SLSB puede ser compartido (y probablemente lo será) entre varios clientes. Por todo esto, los SLSB son creados y destruidos a discrección del contenedor, y puesto que no mantienen estado son muy eficientes a nivel de uso de memoria y recursos en el servidor.
Stateful Session Beans (SFSB - Beans de Sesión con Estado), al contrario que SLSB, si que mantienen estado entre distintas invocaciones realizadas por el mismo cliente. Esto permite crear un estado conversacional (como el carrito de la compra en una tienda online, que mantiene los objetos que hemos añadido mientras navegamos por las diferentes páginas), de manera que acciones llevadas a cabo en invocaciones anteriores son tenidas en cuenta en acciones posteriores. Un SFSB es creado justo antes de la primera invocación de un cliente, mantenido ligado a ese cliente, y destruido cuando el cliente invoque un método en el SFSB que esté marcado como finalizador (también puede ser destruido por timeout de sesión). Son menos eficientes a nivel de uso de memoria y recursos en el servidor que los SLSB.
Los Singleton son un nuevo tipo de Session Bean introducido en EJB 3.1. Un Singleton es un componente que puede ser compartido por muchos clientes, de manera que una y solo una instancia es creada. A nivel de eficiencia en uso de memoria y recursos son indiscutíblemente los mejores, aunque su uso está restringido a resolver ciertos problemas muy específicos.
1.5 MESSAGE-DRIVEN BEANS
Message-Driven Beans (MDB - Beans Dirigidos por Mensajes) son componentes de tipo listener que actuan de forma asíncrona. Su misión es la de consumir mensajes (por ejemplo eventos que se producen en la aplicación), los cuales pueden gestionar directamente o enviar (derivar) a otro componente. Los MDB actuan sobre un proveedor de mensajería, por ejemplo Java Messaging System (JMS es además soportado de forma implícita por la especificacion EJB).
Al igual que los Stateless Session Beans, los Message-Driven Beans no mantienen estado entre invocaciones.
1.6 ENTITY BEANS
Los Entity Beans (EB - Beans de Entidad) son representaciones de datos almacenados en una base de datos, siempre en forma de POJO's. El encargado de gestionar los EB es EntityManager, un servicio que es suministrado por el contenedor y que está incluido en la especificación Java Persistence API (JPA - API de Persistencia en Java). JPA es parte de EJB desde la versión 3.0 de esta última. Para saber más sobre JPA, puedes visitar un tutorial anterior publicado en esta misma web en la siguiente dirección.
Al contrario que los Session Beans y los Message-Driven Beans, los Entity Beans no son componentes del lado del servidor. En otras palabras, no trabajamos con una vista del componente, si no con el componente real.
1.6 EJB Y TEST-DRIVEN DEVELOPMENT
Para terminar este primer artículo, dejemos de lado la teoría y escribamos una sencilla aplicación EJB para ir abriendo el apetito. Para poder seguir este y futuros ejemplos, debes tener en marcha un entorno compatible con EJB 3.1 (aunque la mayoría de los ejemplos, incluido este, funcionarán en un contenedor compatible con EJB 3.0). Por otro lado, todo el código se ajusta al estandard EJB 3.1 (no se usarán extensiones exclusivas de un contenedor concreto). Sin embargo, ten presente que las indicaciones relativas a la creación del proyecto, despliegue, y ejecución de los ejemplos estarán condicionadas por el entorno concreto que se ha puesto en marcha mediante el anexo que acompaña este tutorial; si tu entorno es diferente, ciertas acciones (como los opciones de menús a ejecutar en tu IDE) serán otras.
Como se ha indicado en el punto 1.3, este ejemplo de desarrollará de manera puntual mediante Test-Driven Development. En los próximos artículos solo se mostrará código EJB, que es al fin y al cabo el tema a tratar en este turorial. Así mismo, los pasos para crear un proyecto o como desplegarlo en el contenedor EJB se omitirán en artículos posteriores.
Para comenzar inicia Eclipse (si aún no lo has hecho) y crea un nuevo proyecto EJB:
File > New > EJB Project
Dale un nombre al nuevo proyecto y asegúrate tanto de seleccionar en el desplegable 'Target runtime' un contenedor compatible con EJB como de seleccionar la versión 3.1 en el desplegable 'EJB module version'. Haz click en el botón 'Finish' para crear el proyecto.
Ahora es el momento de crear un test que defina y respalde nuestro primer EJB. Lo primero es crear una carpeta dentro del proyecto donde almacenaremos todos los tests que escribamos. En la pestaña 'Project Explorer' haz click con el botón derecho sobre el proyecto EJB y selecciona:
New > Source Folder
Introduce el nombre del directorio en el campo 'Folder name' (utiliza un nombre descriptivo, como 'tests') y haz click en el botón 'Finish'. Si expandes el proyecto EJB (con la flecha negra que hay a la izquierda del nombre) verás que el nuevo directorio se ha creado correctamente. Ahora vamos a crear la clase donde escribiremos los tests para nuestro EJB. Haz click con el botón derecho sobre el directorio de tests y selecciona:
New > Other
En la ventana que te aparecerá selecciona:
Java > JUnit > JUnit Test Case
En la ventana de creación de un test de JUnit selecciona la opción 'New JUnit 4 test', introduce el nombre del paquete donde deseas alojar la clase en el campo 'Package' (muy recomendado) y escribe un nombre para la clase de tests. En mi caso, el nombre del paquete será 'es.davidmarco.ejb.slsb' y el nombre de la clase de tests
'PrimerEJBTest'. Haz click en el botón 'Finish'; si es el primer tests que escribes para el proyecto (como es nuestro caso) aparecerá una ventana donde Eclipse nos informa que la librería JUnit no está incluida en el classpath. Seleccionamos la opción 'Perform the following action: Add JUnit4 library to the build path' (Realizar la siguiente acción: añadir la libreria JUnit 4 al path de construcción) y haz click en el botón 'OK'. Hecho esto, ya podemos escribir nuestro primer (y de momento único) test:
public class PrimerEJBTest {
@Test
public void testSaluda() {
PrimerEJB ejb = new PrimerEJB();
assertEquals("Hola usuario", ejb.saluda("usuario"));
}
}
El test (un método que debe ser void, no aceptar parámetros, y estar anotado con la anotación de JUnit @Test) declara las intenciones (el contrato) del código que estamos diseñando: crear un objeto de la clase PrimerEJB con un método saluda() que acepte un argumento de tipo String y devuelva un mensaje con el formato 'Hola argumento'. Aquí empezamos a ver las ventajas de EJB: podemos testear nuestro código de negocio directamente, sin tener que desplegar el componente EJB en un contenedor y entonces hacer una llamada a este, con toda la parafernalia que esto requiere.
Y ahora si (por fin) vamos a escribir nuestro primer EJB. Nuestra clase de negocio ira en un paquete con el mismo nombre que el utilizado para almacenar las clase de tests, pero en una carpeta diferente. De esta manera mantenemos ambos tipos de clases separadas físicamente en disco (por motivos de organización y por claridad), pero accesibles gracias a que virtualmente pertenecen al mismo paquete, y por tanto entre ellos hay visibilidad de tipo package-default (esto puede resultarnos útil si necesitamos, por ejemplo, acceder desde la clase de tests a métodos en las respectivas clases de negocio que han sido declarados como 'protected'). En la pestaña 'Project Explorer' haz click con el botón derecho en la carpeta 'ejbModule' (la carpeta por defecto que crea Eclipse en un proyecto EJB para almacenar nuestras clases) y selecciona:
New > Class
Introduce en el campo 'Package' el nombre del paquete donde vamos a almacenar la clase de negocio (en mi caso 'es.davidmarco.ejb.slsb') y en el campo 'Name' el nombre de la clase de negocio; para este último caso es conveniente usar el nombre de la clase de tests sin el sufijo 'Test' (en mi caso 'PrimerEJB') de manera que podamos asociar visualmente en el explorador del IDE cada clase de negocio ('Xxx') con su clase de tests ('XxxTest'). Haz click en el botón 'Finish' para crear la clase de negocio y añade la lógica de negocio (comienza con la solución mas simple posible):
package es.davidmarco.ejb.slsb;
public class PrimerEJB {
public String saluda(String nombre) {
return null;
}
}
Si en este punto ejecutamos el test que hemos escrito (haciendo click con el botón derecho sobre el editor donde tenemos el código del test y seleccionando 'Run As > JUnit Test') este fallará (verás una barra de color rojo que indica que al menos un tests no ha pasado correctamente). Si observas la ventana 'Failure trace' (seguimiento de fallos) de la pestaña de resultados de JUnit, verás un mensaje que, traducido a español, indica que se esperaba como respuesta 'Hola Usuario' pero se recibió 'null'. Debajo de este mensaje puedes ver la pila de llamadas que ha generado el error, en nuestro caso ha sido la función estática AssertEquals de JUnit. Volvamos al editor donde tenemos la clase de negocio y arreglemos el código que está fallando:
package es.davidmarco.ejb.slsb;
public class PrimerEJB {
public String saluda(String nombre) {
return "Hola usuario";
}
}
Si ahora ejecutamos el test, veremos en la pestaña de JUnit que la barra es ahora de color verde, lo cual indica que todos los tests se han ejecutado sin fallos (la ventana 'Failure Trace' esta ahora vacía, evidentemente). Ahora decidimos que, cuando un cliente pase un argumento de tipo null a nuestra función, esta deberá devolver un saludo por defecto. Renombremos el nombre del primer test que hemos escrito y escribamos un segundo test que pruebe esta nueva condición (desde ahora y hasta el final de esta sección omitiré en ambas clases las sentencias package e import por claridad):
public class PrimerEJBTest {
@Test
public void testSaludaConNombre() {
PrimerEJB ejb = new PrimerEJB();
assertEquals("Hola usuario", ejb.saluda("usuario"));
}
@Test
public void testSaludaConNull() {
PrimerEJB ejb = new PrimerEJB();
assertEquals("Hola desconocido", ejb.saluda(null));
}
}
Como se dijo previamente, mediante TDD estamos dejando claras las intenciones de nuestro código antes incluso de escribirlo, como se puede ver en el segundo tests. En él, esperamos recibir como respuesta la cadena de texto 'Hola desconocido' cuando invoquemos el método saluda() con un argumento de tipo null. Y mientras tanto, el primer test (que hemos renombrado para darle más claridad y expresividad a nuestros tests) debe seguir pasando, por supuesto. Ejecutamos los tests ('Run As > JUnit Test') y el nuevo test que hemos escrito falla (podemos ver en la ventana a la izquierda de la pestaña de JUnit el/los test/s que ha/n fallado marcados con una cruz blanca sobre fondo azul). Volvamos al editor donde estamos escribiendo la lógica de negocio e implementemos la nueva funcionalidad:
public class PrimerEJB {
public String saluda(String nombre) {
if(nombre == null) {
return "Hola desconocido";
}
return "Hola usuario";
}
}
Ahora ambos tests pasan. Para terminar, ¿que ocurriría si en lugar de la cadena de texto 'usuario' pasamos al método saluda() una cadena de texto distinta?. Añadamos un test que pruebe esta condición:
public class PrimerEJBTest {
@Test
public void testSaludaConNombre() {
PrimerEJB ejb = new PrimerEJB();
assertEquals("Hola usuario", ejb.saluda("usuario"));
}
@Test
public void testSaludaConOtroNombre() {
PrimerEJB ejb = new PrimerEJB();
assertEquals("Hola Pedro", ejb.saluda("Pedro"));
}
@Test
public void testSaludaConNull() {
PrimerEJB ejb = new PrimerEJB();
assertEquals("Hola desconocido", ejb.saluda(null));
}
}
Este último test demuestra, al ejecutarse y fallar, que nuestra lógica de negocio contiene un bug: siempre que invocamos el método saluda() con un parámetro de tipo String (diferente de null) obtenemos la cadena de texto 'Hola usuario', ignorando así el parametro que le hemos pasado. He aquí otra ventaja más que surje del uso de TDD: descubrir bugs lo antes posible. Cuanto más tiempo tardemos en descubrir un bug, más dificil nos resultará encontrarlo y solucionarlo. Vamos a resolver este último error en nuestra lógica de negocio:
public class PrimerEJB {
public String saluda(String nombre) {
if(nombre == null) {
return "Hola desconocido";
}
return "Hola " + nombre;
}
}
Ahora todos los tests pasan. Aunque aún nos quedaría la tarea de refactorizar los tres métodos de tests (hay código redundante en todos ellos) y tal vez añadir algún test más (o eliminar...), vamos a dejar las cosas aquí. TDD es un tema demasiado amplio y complejo que está fuera del propósito de este tutorial. Aunque este ejemplo ha sido extremadamente simple/tonto/llamalo-como-quieras, nos ha servido para demostrar lo facil que es diseñar un componente EJB paso a paso y libre de errores. Nadie quiere software que falle, y por tanto debes tomarte la tarea de testear el código que escribes muy en serio. Test-Driven Development es una manera muy sencilla y divertida de diseñar software de calidad.
1.7 DESPLEGANDO NUESTRO PRIMER EJB
Hasta ahora hemos tratado la clase PrimerEJB como si fuera un componente EJB. Pero lo cierto es que no es así (en otras palabras, te he mentido, aunque espero que puedas perdonarme...). Para que nuestro POJO sea reconocido por nuestro contenedor como un componente EJB verdadero tenemos que decirle que lo es:
package es.davidmarco.ejb.slsb;
import javax.ejb.Stateless;
@Stateless
public class PrimerEJB {
// ...
}
La anotación @Stateless define nuestro POJO como un Session Bean de tipo Stateless y una vez desplegado en un contenedor EJB, este lo reconocerá como un componente EJB que podremos usar. ¡Así de simple!. Sin embargo, debemos añadir una segunda anotación a nuestro (ahora si) componente EJB:
@Remote
@Stateless
public class PrimerEJB {
public String saluda(String nombre) {
if(nombre == null) {
return "Hola desconocido";
}
return "Hola " + nombre;
}
}
La anotación @Remote permite a nuestro EJB ser invocado remotamente (esto es, desde fuera del contenedor). Si omitimos esta anotación, el EJB sería considerado como 'Local' (concepto que veremos en el próximo artículo) y solo podría ser invocado por otros componentes ejecutandose dentro del mismo contenedor. Nosotros la hemos incluido pues en la próxima sección vamos a escribir un cliente Java externo al contenedor que solicitará a este el componente que estamos diseñado. De manera adicional, todos los componentes que sean de tipo remoto (como este) deben extender una interface (o de lo contrario se producirá un error durante el despliegue):
package es.davidmarco.ejb.slsb;
public interface MiInterfaceEJB {
public String saluda(String nombre);
}
Por último modificamos nuestro EJB para que implemente la interface y el despliegue sea correcto:
@Remote
@Stateless
public class PrimerEJB implements MiInterfaceEJB {
// ...
}
La necesidad de una interface para componentes de tipo remoto, aunque que a priori pueda parecer una restricción (o una limitación), es necesaria para que el contenedor pueda construir la vista/proxy que será enviada a los clientes remotos (externos al contenedor) por motivos que no vienen al caso. Además, se considera una buena práctica que nuestras clases y métodos de negocio se construyan sobre interfaces: de esta manera los clientes que usan nuestro código trabajan con la interface, ignorando la implementación concreta. De esta manera podemos cambiar dicha implementación en el futuro sin romper el código de nuestros clientes.
Ahora ya podemos desplegar nuestra primera aplicación EJB en el contenedor. En la pestaña 'Project Explorer' haz click con el botón derecho sobre el nombre del proyecto, y selecciona:
Run As > Run on Server
Durante el primer despliegue nos aparecerá una ventana donde podemos seleccionar el servidor donde deseamos realizar el despliegue (aparecerá por defecto el que definimos al crear el proyecto). Seleccionamos la casilla 'Always use this server when running this project' (Usar siempre este servidor cuando se ejecute este proyecto) y hacemos click en el botón 'Finish'. La pestaña 'Console' se volverá activa y en ella veremos multitud de información relativa al proceso de arranque del servidor (puesto que no estaba arrancado). Tras unos momentos (30-40 segundos en mi equipo) el contenedor se habra levantado, y con él nuestra aplicación EJB. Entre los últimos mensajes de arranque del servidor puedes ver los siguientes:
PrimerEJB/remote - EJB3.x Default Remote Business Interface
PrimerEJB/remote-es.davidmarco.ejb.slsb.InterfaceEJB - EJB3.x Remote Business Interface
Esas dos lineas nos indican dos referencia JNDI válidas al componente EJB que hemos desplegado, y las necesitaremos cuando escribamos el cliente para que el contenedor nos devuelva el objeto correcto.
Antes de finalizar esta sección, veamos un último asunto relativo al despliegue. Una vez que ya tenemos desplegada nuestra aplicación en JBoss, si realizamos un cambio en nuestra lógica de negocio y deseamos volver a desplegar la aplicación en el contenedor, debemos hacerlo desde la pestaña 'Servers' de Eclipse (y no desde la pestaña 'Project Explorer' como hicimos la primera vez que desplegamos la aplicación). Para ello, primero abrimos la pestaña 'Servers' si no es visible en el Workbench de Eclipse:
Window > Show Views > Servers
Si miramos a la recién abierta pestaña 'Servers' veremos la instancia de JBoss asociada a nuestro proyecto, y junto a ella una flecha. Pinchamos en esta flecha para expandir el servidor y veremos nuestro proyecto EJB. Hacemos click con el botón derecho sobre el nombre del proyecto y seleccionamos 'Full Publish'. En unos segundos nuestro proyecto estará re-desplegado (puedes ver el proceso en la pestaña 'Console').
Por otro lado, cuando iniciemos el IDE y queramos acceder a una aplicación desplegada con anterioridad (por ejemplo desde un cliente como el que vamos a construir en la próxima sección) debemos iniciar primero el servidor, evidentemente. Para ello, haz click con el botón derecho sobre el nombre del servidor en la pestaña 'Servers' y selecciona 'Start'.
1.8 EL CLIENTE JAVA
Ahora es el momento de escribir el cliente Java, el cual va a hacer una solicitud al contenedor mediante JNDI para obtener el Stateless Session Bean que hemos creado en la sección 1.6 y desplegado en la sección 1.7. Con esto veremos como el contenedor gestiona todo el ciclo de vida de una aplicación EJB así como de sus componentes, mientras los clientes solo tienen que preocuparse de solicitar el componente que necesiten y usarlo. Lo primero es crear un nuevo proyecto Java:
File > New > Other
Seleccionamos 'Java Project', hacemos click en 'Next', le damos un nombre al proyecto y hacemos click en 'Finish'. En la pestaña 'Project Explorer' expandimos el proyecto pinchando en la flecha que aparece a la izquierda de su nombre y en la carpeta 'src' creamos un nuevo paquete (muy recomendado) haciendo click con el botón derecho y seleccionando:
New > Package
Le damos un nombre al paquete (en mi caso 'es.davidmarco.ejb.cliente') y hacemos click en 'Finish'. Volvemos a hacer click con el botón derecho sobre el paquete recién creado y seleccionamos:
New > Class
En la ventana que nos aparecerá le damos un nombre a la clase (en mi caso 'Cliente') y marcamos la casilla 'public static void main(String[] args)' para que nos cree un método main() automáticamente, y hacemos click en el botón 'Finish'.
Antes de mostrar el código del cliente es preciso mencionar que para ejecutarlo se necesitan ciertas librerias que, por motivos de simplicidad, vamos a obtener del primer proyecto (la aplicación EJB). Para ello, en la pestaña 'Project Explorer' haz click con el botón derecho sobre el nombre del proyecto que hace de cliente y selecciona:
Build Path > Configure Build Path
En la ventana que se abrirá seleccionamos la pestaña 'Projects', hacemos click en el botón 'Add', y marcamos la casilla correspondiente al proyecto donde está la aplicación EJB que hemos desplegado (y que, repito, contiene todas las librerias que necesita el cliente). Para finalizar, hacemos click en el botón 'OK', y de nuevo hacemos click en el botón 'OK'. El código del cliente es el siguiente:
El cliente contiene una constante llamada JNDI_PRIMER_EJB a la que le hemos dado el valor de la referencia JNDI a nuestro componente (recuerda que este valor nos los dio el contenedor cuando desplegó la aplicación, como vimos al final de la sección 1.7). Dentro del método main() creamos un objeto de propiedades, introducimos los valores necesarios para acceder al contexto del contenedor EJB, y creamos dicho contexto mediante un objeto InitialContext y el objeto de propiedades que acabamos de crear y configurar.
A continuación viene lo realmente interesante: no obtenemos un objeto de tipo InterfaceEJB mediante el constructor new (como haríamos en una aplicación Java normal), si no que se lo solicitamos al contenedor EJB a traves del método lookup() del contexto que acabamos de crear. A este método le hemos pasado el nombre de la referencia JNDI del componente que queremos obtener. Recuerda que cuando solicitamos al contenedor un componente de tipo Session Bean (ya sea Stateless, Stateful, o Singleton) lo que obtenemos no es una instancia del componente EJB (en nuestro caso PrimerEJB), si no una vista (un objeto proxy) que sabe como alcanzar el objeto real dentro del contenedor.
Una vez que tenemos la vista podemos ejecutar cualquiera de los métodos que definimos en su interface asociada (MiInterfaceEJB). Para ejecutar el cliente nada tan sencillo como hacer click con el botón derecho sobre el editor donde lo tenemos y seleccionar:
Run As > Java Application
En la pestaña 'Console' aparecera el resultado de la ejecución del cliente. A modo de experimento puedes cambiar la invocación al método saluda(), pasarle un valor null en lugar de una cadena de texto, volver a ejecutar el cliente, y ver la respuesta del componente EJB.
1.9 RESUMEN
Este primer artículo del tutorial de introducción a EJB 3.1 ha sido muy fructífero. En el hemos visto de manera superficial los conceptos básicos de la especificación EJB, una breve introducción al contenedor EJB, los tipos de componentes que podemos producir, un sencillo ejemplo de Test-Driven Development + EJB, la forma de desplegar dicho ejemplo en un contenedor compatible con EJB, y como acceder al componente (a través del contenedor) desde un cliente Java.
En el próximo artículo veremos en profundidad dos de los tres tipos de componentes de tipo Session Bean: Stateless Session Beans y Stateful Session Beans. Hasta entonces, ¡felices despliegues!.
Actualmente me encuentro leyendo una joya de la literatura informática de los últimos años, Clean Code: A Handbook of Agile Software Craftsmanship, de Robert C. Martin. Sin enrollarme demasiado, el libro muestra como mejorar nuestro código (y como no empeorarlo) siguiendo algunas técnicas que, en los dos últimas decadas, han demostrado ser efectivas para mantener un proyecto de software funcionando de la mejor manera posible durante el mayor tiempo posible.
Este artículo trata sobre una de esas técnicas, una tan sencilla como poderosa, y que podemos empezar a aplicar desde ya a todo el código que pase por nuestras manos: usar buenos nombres. Vamos al lio.
INTRODUCCIÓN: ¿POR QUE USAR BUENOS NOMBRES?
Usar buenos nombres es esencial para mantener el código legible. Como programadores, pasamos más tiempo leyendo código (propio y ajeno) que escribiéndolo (según algunos estudios ¡en un ratio de 10:1!); toda nueva funcionalidad se basa en funcionalidad anterior, y si esa funcionalidad anterior no es ...
Actualmente me encuentro leyendo una joya de la literatura informática de los últimos años, Clean Code: A Handbook of Agile Software Craftsmanship, de Robert C. Martin. Sin enrollarme demasiado, el libro muestra como mejorar nuestro código (y como no empeorarlo) siguiendo algunas técnicas que, en los dos últimas decadas, han demostrado ser efectivas para mantener un proyecto de software funcionando de la mejor manera posible durante el mayor tiempo posible.
Este artículo trata sobre una de esas técnicas, una tan sencilla como poderosa, y que podemos empezar a aplicar desde ya a todo el código que pase por nuestras manos: usar buenos nombres. Vamos al lio.
INTRODUCCIÓN: ¿POR QUE USAR BUENOS NOMBRES?
Usar buenos nombres es esencial para mantener el código legible. Como programadores, pasamos más tiempo leyendo código (propio y ajeno) que escribiéndolo (según algunos estudios ¡en un ratio de 10:1!); toda nueva funcionalidad se basa en funcionalidad anterior, y si esa funcionalidad anterior no es legible nuestra tarea se ralentizará y, en el peor de los casos, puede convertirse en una pesadilla. Más aún, prácticamente todo tiene un nombre (constantes, variables, métodos, clases, paquetes, ...) así que entidades bien definidas con un nombre apropiado y no ambiguo nos harán el trabajo mucho más sencillo. Usar buenos nombres nos va a ayudar a desarrollar más rápido, cometer/producir menos errores, y sobre todo a ser profesionales y sinceros con nuestro trabajo. Comencemos con las reglas.
TEN CUIDADO AL ELEGIR LOS NOMBRES Y CAMBIALOS CUANDO ENCUENTRES UNO MEJOR
Sin llegar a convertir esta regla en una obsesión, pues puede haber más de una manera igual de adecuada de nombrar una entidad, es la base de todo lo que se va a exponer. Debes aceptar esta regla de manera natural y acometerla sin poner excusas. La forma de llevarla a cabo está explicada en el resto de reglas.
NOMBRANDO CLASES
Clases y objetos deben ser nombrados usando sustantivos como Cliente, Usuario y Empleado.
NOMBRANDO MÉTODOS
Métodos deben ser nombrados usando verbos o frases con verbos como pagarSalario, borrarArchivo, etc. Métodos que actuan como accesores según el estandar Javabean deben seguir las reglas típicas (getXxx, setXxx, isXxx).
USA NOMBRES QUE REVELEN UNA INTENCIÓN
El nombre de una variable, un método, o una clase debe decir porqué existe, que hace, y que representa, respectivamente. Si una variable, clase o método necesita un comentario que responda a una de esas tres cuestiones es que su nombre no está revelando su intención:
int dias; // dias transcurridos desde la última comprobación
El código anterior es ambiguo, y esa variable usada 100 lineas más abajo no nos estará diciendo quien es, porqué está ahí y porqué debe ser usada de la forma en que lo está siendo. Lo más probable es que tengamos que retroceder buscando el lugar donde ha sido declarada (y tal vez viendo cómo ha sido usada anteriormente) para responder a las tres preguntas anteriores. Una manera mucho más correcta de declararla sería así:
int diasDesdeUltimaComprobacion;
Ahora, la misma variable leida las mismas 100 lineas más abajo expresa claramente su razón de ser y la del código que la usa.
EVITA LA DESINFORMACIÓN
La desinformación oscurece el código, llevandote a seguir falsas pistas y a perderte entre ellas. No uses nombres del tipo usuariosList para un objeto que no represente realmente una colección de tipo List (mejor usa conjuntoDeUsuarios o aún mejor usuarios).
Otra fuente de desinformación resulta de nombres muy largos y muy similares:
public class ControladorParaManejoEficienteDeEntradas { }
public class ControladorParaGuardadoEficienteDeEntradas { }
Para este último caso no hay solución sencilla (ambas clases están nombradas de manera adecuada) y lo mejor es tener presente la similaridad de sus nombres, prestando atención a si estamos usando la clase adecuada en cada momento.
Un caso extremo de desinformación se produce al usar los caracteres l (ele minúscula) y O (o mayúscula), que son fácilmente confundidos por 1 (uno) y 0 (cero).
REALIZA DISTINCIONES DE SIGNIFICADO
Que el compilador entienda y compile todo el código que le proporcionamos no nos convierte en buenos programadores. Una de mis citas favoritas sobre programación (y que tengo pinchada en la pared delante de mi) es de Martin Fowler y dice lo siguiente:
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand."
Que traducido viene a decir:
"Cualquier tonto puede escribir código que un ordenador puede entender. Los buenos programadores escriben código que los seres humanos pueden entender."
Que nuestro compilador entienda la diferencia inherente entre dos variables array1 y array2 no le va a importar lo más mínimo a quien, seis meses después de haber sido escritas, tenga que usarlas (y ese alguien podriamos ser nosotros) y no sepa por donde empezar porque sus nombres no significan nada. Ese dia seremos maldecidos:
void copiarCaracteres(char[] array1, char[] array2) {
for(int i = 0; i < array1.length; i++) {
array2[i] = array1[i];
}
}
Aunque el método anterior tiene un nombre adecuado, el algoritmo que contiene en su interior es confuso. Y si en lugar de contener solamente 2 lineas de código tuviera 20, sería un caos. La solución es hacer una distinción de significado entre los nombres de los argumentos del método, renombrandolos para que tengan un significado explícito y claro:
void copiarCaracteres(char[] origen, char[] destino) {
for(int i = 0; i < origen.length; i++) {
destino[i] = origen[i];
}
}
Otra fuente de indistinción de significado nace del uso de ciertas palabras en los nombres que damos a una entidad. ¿Acaso no resultan tremendamente similares en significado las variables cliente y clienteInfo? ¿Que representa cada una de ellas cuando ambas aparecen en el mismo contexto? Por este motivo, palabras como info y datos deben ser evitadas.
USA NOMBRE PRONUNCIABLES
Imagina el siguiente ejemplo:
Date genAmdhms() { }
El ejemplo anterior es un caso claro, tal vez un poco extremo (aunque recuerdo haber hecho cosas no mucho mejores en algunos proyectos...), de nombre impronunciable, y está relacionado con una de las primeras reglas que vimos: USA NOMBRES QUE REVELEN UNA INTENCIÓN. Probablemente el método vaya acompañado de un comentario que clarifique cual es su función, ya que de otra manera sería prácticamente imposible deducir cual es esta (función que, dicho sea de paso, es generar el año, mes, dia, hora, minuto, y segundo actual). ¿Acaso no habría sido más sencillo hacer lo siguiente?:
Date generarTimestamp() { }
Otro problema que surje al usar nombres impronunciables aparece cuando tenemos que comunicarnos verbalmente con un compañero o socio. Imagínate por un momento la siguiente conversación:
Durante la última batería de tests ha aparecido un bug que apunta al método gen a eme de ache eme ese, ¿puedes echarle un vistazo?
Creo que las palabras sobran.
USA NOMBRES QUE PUEDAN SER BUSCADOS
Nombres con un solo carácter (o extremadamente cortos) y el uso de literales numéricos sufren de un mal común: son difíciles de buscar. El el primer caso (nombres con un solo carácter) su uso debería ser reducido a métodos más bien cortos y algunos casos especiales (por ejemplo la famosa variable i dentro de un bucle for) y nunca en ningún otro lugar. Una variable llamada e para representar a un Empleado es una atrocidad que no debes cometer jamás. En el segundo caso (literales numéricos) deberían ser sustituidos por constantes con un nombre bien definido. Veamos un ejemplo:
En el método anterior, se está usando un literal numérico (40) donde debería utilizarse una constante con un nombre bien definido. Si en el futuro decidimos que las horas base por semana deben ser 35 en lugar de 40, nos veremos obligados a buscar cada linea donde aparece el valor 40 (problema 1: es dificil de buscar) y determinar en cada una de ellas si dicho valor 40 hace referencia a las horas trabajadas, puesto que si hace referencia a otro contexto (por ejemplo el número máximo de empleados por departamento) y lo cambiamos por error, estaremos introduciendo un bug que será dificil de encontrar cuando la aplicación comience a funcionar mal (problema 2: propensión a introducir errores). La solución ya se ha mencionado dos veces: declarar una constante con un nombre adecuado:
final static int HORAS_BASE_POR_SEMANA = 40;
...
if(empleado.getHorasTrabajadasUltimaSemana() > HORAS_BASE_POR_SEMANA) {
pagarHorasExtra(empleado);
}
Ojala algún dia a todos nos paguen las horas extra con la misma facilidad :)
ELIGE UNA ÚNICA PALABRA POR CADA CONCEPTO
Elige una palabra para cada concepto abstracto y úsala siempre. Imagina tres clases, cada una con un método que obtiene información de forma similar desde una base de datos. En la primera clase el método se llama leerDatosDesdeBBDD, en la segunda obtenerDatosDesdeBBDD y en la tercera recuperarDatosDesdeBBDD. Esto solo puede crear confusión, además de ser muy poco elegante.
Quiero reconocer que este error (o más exactamente violación de buenas prácticas) lo he cometido a menudo en el pasado, por ejemplo diseñando una interface DAO con métodos llamados leerXxx, leerYyy, ... y una interface de servicio con métodos llamados obtenerXxx, obtenerYyy, ... . Esta última interface se limitaba a llamar a los métodos del DAO, y a pesar de trabajar conectadas, cada una utilizaba su propia nomenclatura, resultando en código feo y muy poco estético.
Quienquiera que lea código de este tipo se empezará a hacer preguntas totalmente innecesarias como ¿que diferencia hay entre leer datos y recuperar datos? ¿Lee desde un archivo y recupera desde una base de datos? ¿O lee desde una base de datos y recupera desde un archivo? Entonces tendrá que revisar código fuente, documentación y/o especificaciones para descubrir que ambos métodos hacen lo mismo, aunque han sido nombrados según convenciones diferentes. El código debe ser (dentro de sus/nuestras posibilidades) como la prosa, como un libro que cuenta una historía que puedes comprender frase a frase. Nunca como una adivinanza ni como un juego de palabras. No dejes nada al azar.
EVITA USAR LA MISMA PALABRA PARA DOS CONCEPTOS
Esta regla es hermana de la regla anterior. Imagina otras tres clases, cada una con un método llamado añadir. En la primera clase, dicho método añade un registro a una base de datos, en la segunda añade un objeto a un array, y en la tercera añade una cadena de texto a otra previamente almacenada en una variable. Esos tres métodos, si son utilizados "cara a cara" en una misma aplicación (como la interface de servicio y la interface DAO de la regla anterior) producirán confusión al lector, ya que los tres se llaman igual pero hacen cosas completamente distintas (ten presente que intento hacer una distinción sutil del que hace y no del cómo lo hace, siguiendo el más que importante principio de encapsulación). La solución a este problema es ser consistente con los nombres que usamos, o en su defecto o imposibilidad, usar nombres apropiados:
class SimpleDao {
void añadirRegistroEnBBDD(Registro registro) { }
}
Facil, ¿verdad?.
USA NOMBRES QUE REFLEJEN LA SOLUCIÓN
Quien lee tu código es también programador, por lo debes usar terminos que reflejen la solución que estás usando. Un ejemplo visto anteriormente es usuariosList, que rápidamente se interpreta como una colección de tipo List que contiene usuarios. Quienquiera que tenga que usar dicha variable tendrá conciencia inmediata de que es y cómo debe usarla. Otro ejemplo de nombre que refleja la solución que está tratando sería UsuarioFacade: una clase que implementa el patrón de diseño Facade. Es más fácil nombrar bien una entidad que acompañarla con un comentario de 15 palabras.
USA NOMBRES QUE REFLEJEN EL DOMINIO
Esta regla complementa a la anterior. Cuando tratamos problemas de un dominio especifico, debemos usar nombres que reflejen ese dominio. Un método actualizarInventario está hablando sobre algo llamado inventario, que es parte del dominio de la aplicación que estamos escribiendo. Esto nos va a ayudar tanto a escribir el código necesario para implementar el método (puesto que el nombre deja claro el que) como a entender su función cuando sea usado o leido por nosotros mismos o por otro programador.
AÑADE UN CONTEXTO
La última regla, añade un contexto, nos puede ayudar en ciertos momentos a evitar ambigüedades y por tanto confusión. Imagina que tenemos un conjunto de variables llamadas calle, numero, ciudad y codigoPostal. Todas ellas, cuando se usan conjuntamente, reflejan claramente que estamos tratando con una dirección postal y que sus nombres son perfectamente válidos. Sin embargo, ¿que ocurre si una de ellas, por ejemplo numero, es usada de manera independiente dentro de un método? Para el lector de dicho método, numero puede significar un montón de cosas, o tal vez no significar nada, convirtiendo el método o ciertas partes de él en (como dije unas reglas más arriba) una adivinanza: sabemos el que, pero no sabemos el porqué. Esto no es código legible.
Existen dos soluciones a este problema. La primera es añadir un prefijo a las variables, lo que les proporciona un contexto y elimina de un plumazo cualquier ambigüedad presente y futura:
La segunda solución, y en cualquier lenguaje orientado a objetos la preferida, es crear una clase que sea en si misma el contexto:
public class Direccion {
String calle;
String numero;
String ciudad;
String codigoPostal;
// getters y setters
}
De esta manera dejamos de llamar a numero para llamar a direccion.getNumero(), provocando que el significado de la antes ambigua variable se descubra ahora por si mismo.
RESUMEN
Todo el código que no es legible y claro en sus intenciones es dicifil de entender y de mantener. Programar debe ser un placer, dentro de su dificultad y otras peculiaridades que no vienen a cuento, y nunca una pesadilla. El código debe fluir, y ningún programador desea ahogarse en un rio desbordado de dificultades. Usar buenos nombres en clases, métodos, variables, etc. es una muy buena manera de empezar a mejorar nuestro código, ayudándonos a nosotros mismos y ayudando también a otros. Y además, es tremendamente sencillo (dentro de su dificultad y otras peculiaridades que no vienen a cuento, again). Te animo a que adoptes los principios que se han expuesto en este artículo; si no te gustan siempre puedes volver a tu estilo habitual, pero lo más posible es que una vez los pruebes y veas como crece la calidad de tu trabajo, nunca des marcha atrás.
SOBRE EL LIBRO CLEAN CODE: A HANDBOOK OF AGILE SOFTWARE CRAFTMANSHIP Este artículo está basado en los apuntes que he ido tomando al leer el libro Clean Code: A Handbook of Agile Software Craftsmanship, de Robert C. Martin (más precisamente el capítulo 2: Meaningful Names). La mayoría de los principios que se han expuesto los he venido usando desde mucho antes de tener en mis manos el libro, pues no son desconocidos para la mayoría.
Este artículo no es una traducción ni tampoco un resumen del libro, es una recopilación y traslación de algunas reglas que en él se exponen, escritas por mi con mis propias palabras. Unos pocos fragmentos de código son muy similares a los que aparecen en la obra mientras que la mayoría son mios; así mismo, unas pocas frases me han inspirado por su simplicidad y energía, y las he incluido en el texto (siempre evitando la literalidad). No pretendo con esto quitarle a Robert C. Martin el enorme mérito que merece, pues este artículo es hijo de su obra, ni tampoco colgarmelo yo, pues la forma en como lo he escrito (el nombre de los principios y, salvo excepciones, el orden en que son presentados) está inspirada sin recelos en las páginas de dicha obra. Os recomiendo que os hagaís con ella, personalmente me parece una lectura tremendamente recomendable, divertida e inspiradora, escrita en un inglés muy fácil de leer.
Hoy se cumple un año desde la puesta en marcha de la web. Las más de 187.000 visitas y 560.000 páginas vistas desde 42 paises distintos dan sentido al humilde esfuerzo con el que empecé esta aventura, a mi misión primera y última: compartir mis conocimientos con el resto de la comunidad, al tiempo que aprendo de ella. Las traducciones a Español de los tutoriales de Spring e Hibernate han tenido una aceptación extraordinaria, además de servir de base para diferentes modificaciones que algunos habéis realizado de ellos. También ha tenido gran acogida mi blog personal, dentro del cual he publicado los tutoriales de JPA, Groovy, y diversos artículos relacionados con Java.
Los cientos de correos que he recibido agradeciendo mi trabajo han sido un gran motor para seguir adelante. Muchos de vosotros me habeís pedido también ayuda por email, ayuda que no siempre es facil de proporcionar pues cada problema que os surge con un proyecto concreto es un mundo. De cualquier modo, he contestado a todos y cada uno de los correos sin escatimar tiempo ni esfuerzo, y os invito a que os sigáis poniendo en contacto conmigo siempre que lo consideréis oportuno.
Para este...
Hoy se cumple un año desde la puesta en marcha de la web. Las más de 187.000 visitas y 560.000 páginas vistas desde 42 paises distintos dan sentido al humilde esfuerzo con el que empecé esta aventura, a mi misión primera y última: compartir mis conocimientos con el resto de la comunidad, al tiempo que aprendo de ella. Las traducciones a Español de los tutoriales de Spring e Hibernate han tenido una aceptación extraordinaria, además de servir de base para diferentes modificaciones que algunos habéis realizado de ellos. También ha tenido gran acogida mi blog personal, dentro del cual he publicado los tutoriales de JPA, Groovy, y diversos artículos relacionados con Java.
Los cientos de correos que he recibido agradeciendo mi trabajo han sido un gran motor para seguir adelante. Muchos de vosotros me habeís pedido también ayuda por email, ayuda que no siempre es facil de proporcionar pues cada problema que os surge con un proyecto concreto es un mundo. De cualquier modo, he contestado a todos y cada uno de los correos sin escatimar tiempo ni esfuerzo, y os invito a que os sigáis poniendo en contacto conmigo siempre que lo consideréis oportuno.
Para este nuevo año espero poder escribir mejores artículos y, espero (aunque no sean tan importante), en mayor cantidad. Algunos ya están planificados (como un tutorial de EJB 3.1 que algunos me habéis solicitado y que comenzaré a escribir en Febrero) y otros irán surgiendo sobre la marcha. También tengo en mente algunas modificaciones en la presentación de la página (como el prometido sistema de búsqueda de contenido que hace tiempo que debería de estar en marcha), y tal vez un lavado de cara a la presentacion visual.
No hay nada que me motive más que saber que aún quedan muchas cosas por hacer, tener delante un largo camino para unos pies que no se van a rendir porque disfrutan a cada paso que dan, por duro que sea. Un año de gracias a todos, y feliz 2011.
SpringSource Tool Suite (STS) es un IDE basado en la versión Java EE de Eclipse, pero altamente customizado para trabajar con Spring Framework. Entre las características más destacadas que STS proporciona se encuentran:
- soporte para Spring 3
- asistentes para la creación de proyectos Spring
- herramientas para la gestión de beans
- editores gráficos de archivos de configuración de Spring
- herramientas de desarrollo para Spring Web Flow y Spring Batch
STS puede descargarse desde aquí como un IDE completo, pero también puede integrarse sobre una instalación previa de Eclipse. Sobre esta última opción trata este artículo.
INSTALANDO ECLIPSE
Lo primero que necesitamos es tener Eclipse instalado en nuestro equipo. La versión que vamos a utilizar es la última en el momento de escribir este artículo: Eclipse IDE 3.6 for Java EE Developers (nombre en clave Helios), la cual incluye todas ...
SpringSource Tool Suite (STS) es un IDE basado en la versión Java EE de Eclipse, pero altamente customizado para trabajar con Spring Framework. Entre las características más destacadas que STS proporciona se encuentran:
- soporte para Spring 3
- asistentes para la creación de proyectos Spring
- herramientas para la gestión de beans
- editores gráficos de archivos de configuración de Spring
- herramientas de desarrollo para Spring Web Flow y Spring Batch
STS puede descargarse desde aquí como un IDE completo, pero también puede integrarse sobre una instalación previa de Eclipse. Sobre esta última opción trata este artículo.
INSTALANDO ECLIPSE
Lo primero que necesitamos es tener Eclipse instalado en nuestro equipo. La versión que vamos a utilizar es la última en el momento de escribir este artículo: Eclipse IDE 3.6 for Java EE Developers (nombre en clave Helios), la cual incluye todas las dependencias necesarias para instalar STS. También se dan las instrucciones necesarias para Eclipse 3.5 (Galileo). Los pasos para instalar STS en otras versiones de Eclipse pueden ser diferentes, por lo que no puedo garantizar que este artículo sea 100% funcional para dichas versiones.
PREPARANDO ECLIPSE
Ya dentro del IDE, el primer paso es desactivar todas sitios desde los que Eclipse puede descargar actualizaciones y nuevo software. Desde la barra de menús del IDE accede a:
Windows > Preferences > Install/Update > Available Software Sites
Desactiva todas los sitios de descarga que se encuentren activados desmarcando su casilla correspondiente. Comprueba que el status de cada uno de ellos ha cambiado de Enabled (Activado) a Disabled (Desactivado). No cierres esta ventana aún.
El siguiente paso es configurar los sitios de descarga específicos de STS. La manera más simple es descargando un fichero XML donde se encuentra dicha configuración. Descarga y guarda en tu equipo el archivo correspondiente a tu versión de Eclipse:
Ahora es el momento de volver a la pantalla de configuración de sitios de descarga, que aún deberias tener abierta. Si la has cerrado, recuerda que puedes volver a ella desde:
Windows > Preferences > Install/Update > Available Software Sites
Pulsa el botón 'Import' y selecciona el archivo XML previamente descargado. Verás que aparecen dos nuevos sitios de descarga, y que ambos han sido automáticamente activados (Enabled). De manera adicional, uno de los sitios de descarga de Eclipse también ha sido activado automáticamente. En total, deberemos tener exactamente tres sitios de descarga activados (si estas utilizando Eclipse 3.5 verás que cambia 'Helios' por 'Galileo', así como los números de versión):
- Helios
- SpringSource Update Site for Eclipse 3.6 (Snapshot)
- SpringSource Update Site for Eclipse 3.6 (Snapshot, Dependencies)
Pulsa OK para guardar los cambios.
INSTALANDO STS
El último paso es instalar los componentes que conforman STS. Para ello, accede al gestor de instalación de nuevo software de Eclipse:
Help > Install New Software
En el menú desplegable superior selecciona el sitio:
Work with > SpringSource Update Site for Eclipse 3.6 (Snapshot)
Ahora te aparecerán los componentes que están disponibles desde el sitio seleccionado. Que componentes seleccionar y cuales no es una decisión que dejo en tus manos. Hay quienes prefieren instalarlos todos, y hay quienes (como en mi caso) siempre customizan la instalación, de manera que solamente se instalen los componentes necesarios para una determinada tarea. Además, siempre puedes entrar al gestor de instalación de nuevo software y seleccionar componentes que aún no has instalado. Como guía, estos son los componentes que yo he seleccionado (ten presente que algunos de ellos son obligatorios):
- Core / Spring IDE (COMPLETO)
- Core / STS (COMPLETO)
- Extensions (Incubation) / Spring IDE (COMPLETO)
- Extensions / Spring IDE (PARCIAL)
- Grails Support
- Maven Support
- Runtime Error Analysis Support
- Integrations / Spring IDE (COMPLETO)
- Resources / Spring IDE (COMPLETO)
Una vez seleccionados los componentes a instalar, pulsamos 'Next' y llegamos a la ventana donde se muestran los detalles de la instalación que estamos a punto de realizar. Volvemos a pulsar 'Next', aceptamos las condiciones de la licencia marcando la casilla 'I accept the terms...' y pulsamos 'Finish'.
Si has realizado los pasos correctamente, verás como se abre una ventana donde se muestra la descarga e instalación de los componentes seleccionados (operación que suele durar varios minutos). Durante el proceso de descarga/instalación aparecerá una ventana con una alerta de seguridad (Security Warning), donde se nos informa que se está instalando software sin firmar; pulsa OK para continuar con la instalación. Finalmente, aparecerá una nueva ventana donde se nos informa que debemos reiniciar Eclipse para que los cambios tomen efecto. Por tanto, pulsamos 'Restart Now' para reiniciar (asegúrate de haber guardado todo el trabajo que pudieras haber estado realizando antes de instalar STS) o 'Not Now' si deseamos reiniciar más tarde.
DESACTIVANDO EL DASHBOARD
Una vez instalado STS y reiniciado Eclipse, veremos que al cargar el IDE nos aparece el Dashboard de STS. El dashboard (o tablero de mandos) es una ventana en la que:
- podemos gestionar proyectos Spring
- se muestran actualizaciones de STS
- se muestran feeds de notícias que son actualizadas períodicamente
- se pueden instalar nuevas extensiones para STS (plug-ins, soporte para otros lenguajes de programación, ...)
Para mantener el IDE lo más limpio posible y acelerar el arranque de Eclipse, recomiendo desactivar el dashboard. Lo primero es ir a la configuración del dashboard:
Window > Preferences > Spring > Dashboard
Una vez alli, desactiva la casilla 'Show Dashboard On Startup'. Pulsa OK y de nuevo en la pantalla principal de Eclipse cierra el dashboard si aún continua abierto (pulsando sobre la X de la pestaña superior). La próxima vez que inicies Eclipse el dashboard no aparecerá. Si en algún momento deseas mostrarlo, nada tan simple como ir a:
Help > Dashboard
UN EJEMPLO SIMPLE
La mejor manera de explicar que es y que ofrece STS es, como no, mediante un ejemplo muy sencillo. Vamos a crear un proyecto Spring y a continuacion su archivo de configuración. Para crear el proyecto Spring:
File > New > Other > Spring > Spring Project
Le damos un nombre al proyecto y pulsamos 'Finish'. Ahora, en la ventana del 'Project Explorer', expandimos el recien creado proyecto, pulsamos con el boton derecho del ratón sobre la carpeta 'src' y seleccionamos:
New > Other > Spring > Spring Bean Configuration File
Pulsamos 'Next', le damos un nombre al archivo de configuración (no olvides incluir la extensión XML o STS no sabrá gestionarlo) y pulsamos 'Finish'. El nuevo archivo de configuración se abrirá, y mediante sus pestañas inferiores (Source, Namespaces, ...) podremos gestionar el código fuente XML, los namespaces incluidos, crear nuevos beans mediante un asistente, ver representaciones gráficas de los beans así como sus relaciones, etc.
RESUMEN
SpringSource Tool Suite es una fantástica herramienta que te ayudará a manejar de manera muy potente tus proyectos Spring, con multitud de asistentes, editores, opciones y configuraciones. Aquellos que trabajan con Spring encontrarán en SpringSource Tool Suite un entorno desde el que desarrollar más eficazmente sus aplicaciones Spring, sin olvidar que también trae soporte para otras tecnologías como Groovy, Grails, OGSI, y más.
Desde la versión 3.0, Spring ofrece un lenguaje dinámico llamado Spring Expression Language (SpEL). SpEL permite la consulta y manipulación de objetos en tiempo real (de manera similar a Expression Language en JSP/JSF) además de otras interesantes características como la invocación de metodos. SpringSource Tool Suite (una variación de Eclipse altamente customizada para trabajar con Spring) provee soporte para SpEL, permitiendo autocompletado de expresiones además de otras caracteristicas realmente útiles para trabajar con el lenguaje. SpEL puede ser utilizado:
- de forma nativa en una aplicación Spring (insertado en archivos XML o anotaciones)
- a través de un parser, creando explicitamente toda la infraestructura necesaria para interpretarlo
REQUISITOS
Si usas Maven en tus proyectos, la única dependencia necesaria para usar empezar a usar SpEL es:
Desde la versión 3.0, Spring ofrece un lenguaje dinámico llamado Spring Expression Language (SpEL). SpEL permite la consulta y manipulación de objetos en tiempo real (de manera similar a Expression Language en JSP/JSF) además de otras interesantes características como la invocación de metodos. SpringSource Tool Suite (una variación de Eclipse altamente customizada para trabajar con Spring) provee soporte para SpEL, permitiendo autocompletado de expresiones además de otras caracteristicas realmente útiles para trabajar con el lenguaje. SpEL puede ser utilizado:
- de forma nativa en una aplicación Spring (insertado en archivos XML o anotaciones)
- a través de un parser, creando explicitamente toda la infraestructura necesaria para interpretarlo
REQUISITOS
Si usas Maven en tus proyectos, la única dependencia necesaria para usar empezar a usar SpEL es:
Si deseas gestionar tus librerías sin Maven, además de todos los JAR's necesarios para levantar una aplicación Spring 3.0 (org.springframework.context-3.0.X.RELEASE.jar, org.springframework.beans-3.0.X.RELEASE.jar, ...) necesitas incluir en tu classpath el siguiente JAR que encontrarás en el directorio dist de tu distribución de Spring 3:
org.springframework.expression-3.0.X.RELEASE.jar
SINTAXIS
La sintaxis de SpEL toma la siguiente forma cuando es usada de forma nativa:
#{EXPRESION}
Cuando la expresión es usada por un parser, debemos omitir el caracter de almohadilla y la pareja de llaves:
EXPRESION
A lo largo de este artículo, al introducir cada tipo de expresión se utilizara la primera versión, de manera que se enfatice su naturaleza y se diferencie más facilmente del resto del contenido. Solo cuando una expresión se utilice a través de un parser se utilizará la versión correspondiente, sin almohadilla ni llaves.
CONSTRUYENDO EXPRESIONES (1ª PARTE)
Comencemos viendo los tipos mas básicos de expresión para realizar una toma de contacto con el lenguaje, de manera que en las dos próximas secciones (SPEL DE FORMA NATIVA y SPEL A TRAVES DE UN PARSER) entendamos el 100% del código y no nos perdamos. El primer tipo de expresión que vamos a ver, y la más simple, es la de tipo literal:
#{12.7}
La expresión anterior es un literal del número flotante 12.7. Otro tipo de literal frecuentemente utilizado es la cadena de texto:
#{'Hola SpEL'}
Como puedes ver, las expresiones literales de tipo cadena de texto son construidas dentro una pareja de comillas simples. Ambas expresiones literales devuelven 'literalmente' su valor (de ahi su nombre).
El siguiente tipo de expresión es la de tipo booleano:
#{7 > 9}
El significado de la expresión anterior es totalmente obvio, y el resultado devuelto al evaluar la expresión sería false.
La siguiente de la lista es la llamada 'expresión estandar', la cual nos permite acceder a las propiedades de un bean o javabean mediante notación de puntos:
#{usuario.nombre}
La expresión anterior devolverá el valor de la propiedad nombre del bean llamado usuario que, evidentemente, debera estar registrado dentro del contexto de la aplicación Spring actual. Si lo que queremos es evaluar un bean en si mismo, nada tan facil como usar su nombre como expresión:
#{usuario}
Antes de continuar viendo más tipos de expresiones, veamos como y donde pueden ser usadas. Esto, además de completar el binomio 'sintaxis-uso' te permitirá experimentar con las expresiones mientras continuas leyendo el artículo.
SPEL DE FORMA NATIVA
Como se indicó al inicio de este artículo, SpEL puede ser usado de forma nativa en una aplicación Spring 3.0 mediante anotaciones o en archivos de configuración XML. Veamos algunos ejemplos:
class Login {
@Value("#{usuario.nombre}")
private String nombreUsuario;
// ...
}
La anotación @Value puede ser aplicada tanto en variables como metodos setter, así como en parámetros de un método o constructor. El valor devuelto por la expresion será el valor por defecto del campo donde se aplique, en el caso del ejemplo anterior la variable nombreUsuario. Para que Spring pueda detectar la presencia de la anotación @Value en nuestras clases debemos activar la configuración mediante anotaciones en el fichero de configuración de Spring:
El resultado es el mismo que al utilizar la anotación @Value.
SPEL A TRAVES DE UN PARSER
SpEl puede ser usado fuera de un archivo XML o anotación @Value mediante un parser. Este parser requiere el ensamblaje de cierta infraestructura:
El código anterior es bastante simple. Primero crea un parser de la clase SpelExpressionParser. A continuación crea una expresión llamando al método parseExpression(EXPRESION) del recien creado parser. Por último, obtenemos el resultado de la evaluación de la expresión a traves del objeto expresion. Cuando la expresión hace referencia a un objeto, debemos crear un contexto de ejecución para dicho objeto:
Usuario usuario = new Usuario("David Marco");
EvaluationContext contexto = new StandardEvaluationContext(usuario);
ExpressionParser parser = new SpelExpressionParser();
Expression expresion = parser.parseExpression("nombre");
String nombreUsuario = expresion.getValue(contexto, String.class);
En el ejemplo anterior hemos creado un objeto usuario así como un contexto de evaluación para dicho objeto. En el momento de crear la expresión, solo indicamos la propiedad que queremos evaluar del objeto que hemos pasado al contexto (en lugar del binomio 'objeto.propiedad' que se utilizaría con la anotación @Value o en un archivo de configuración XML). Finalmente, pasamos el contexto de evaluación a expresion.getValue() de manera que la expresión (la propiedad nombre) es aplicada contra el contexto (el objeto usuario) resultando en la evaluación de usuario.nombre. En caso de que el contexto de evaluación tienda a cambiar entre ejecuciones es preferible sustituir el contexto de evaluación que hemos creado explicitamente por el objeto que subyace a dicho contexto, y Spring creará por nosotros un nuevo contexto de evaluación en cada llamada a getValue():
List<Usuario> usuarios = new ArrayList<Usuario>();
// Inicializar la lista de usuarios
ExpresionParser parser = new SpelExpressionParser();
Expression expresion = parser.parseExpression("nombre");
for(Usuario usuario: usuarios) {
String nombreUsuario = expresion.getValue(usuario, String.class);
}
CONSTRUYENDO EXPRESIONES (2ª PARTE)
Ahora que ya hemos visto como y donde podemos escribir expresiones, sigamos viendo más tipos. Empecemos con las expresiones de clase:
#{T(java.lang.Math)}
El caracter T indica que queremos actuar sobre el tipo de una clase (en nuestro caso java.lang.Math), en lugar de sobre una instancia. Una vez obtenido el tipo, podemos asignarlo a una variable mediante @Value, XML o con el parser. También podemos actuar sobre el tipo devuelto por la expresión llamando a cualquiera de sus métodos estáticos dentro de la expresión:
#{T(java.lang.Math).random()}
SpEL nos permite definir expresiones para gestionar colecciones. Para acceder a un array o a un objeto de tipo List:
#{empresa.empleados[1]}
El acceso a listas y arrays se realiza mediante su índice, puesto que son colecciones indizadas. El acceso a una colección de tipo Map se realiza, como ya habras imaginado, mediante sus claves:
#{empresa.telefonosDepartamentos['Ventas']}
En ambos casos, se utiliza la notación de corchetes ya sea para obtener el índice en array y listas como la clave en mapas.
El siguiente tipo de expresión es la invocación de un método. Lo vimos en acción anteriormente al invocar random() en una expresión de clase, pero por su importancia merece un nuevo ejemplo:
#{'Hola SpEL'.toUpperCase()}
La expresión anterior es muy interesante, consistiendo en la invocación de un método sobre una expresión literal. Por supuesto, dicha invocación de método puede realizarse sobre cualquier expresión, clase, u objeto.
SpEL nos permite invocar el constructor de una clase con el siguiente tipo de expresión:
#{new Date()}
El siguiente tipo de expresión de nuestra lista es la expresión con operador ternario, que funciona exactamente igual de como lo haría en Java:
#{usuario.edad > 18 ? true : false}
Recuerda que cuando evaluamos una expresión sobre un objeto, y utilizamos el parser, omitimos el nombre del objeto puesto que dicho objeto ha sido o será pasado como contexto de evaluación:
Usuario usuario = new Usuario("David Marco", 30");
ExpressionParser parser = new SpelExpressionParser();
Expression expresion = parser.parseExpression("edad > 18 ? true : false");
Boolean mayorDeEdad = expresion.getValue(usuario, Boolean.class);
System.out.println("Es mayor de edad? " + mayorEdad);
También podemos hacer referencia al contexto de evaluación mediante una expresión de variable:
En la expresión anterior, #this hace referencia al objeto que conforma el contexto actual de evaluación, en nuestro caso usuario.
Los dos últimos tipos de expresión que vamos a mostrar en este artículo estan relacionadas con la manipulación de colecciones. La primera es proyección de colección:
#{usuarios.![nombre]}
En la expresión anterior, usuarios es una coleccion que contiene objetos de tipo Usuario. Una vez evaluada la expresión, el resultado devuelto es una lista que contiene los nombres (mediante la propiedad nombre) de todos los usuarios de la primera lista. El segundo tipo de expresión para manipular colecciones es selección de colección:
#{usuarios.?[nombre.startsWith('D')]}
Selección de colección permite, como muy bien expresa la sintaxis anterior, filtrar los objetos de una colección (usuarios y devolver una nueva colección que contiene solo aquellos objetos que cumplian las condiciones de filtrado.
CONCLUSIONES
Como puedes ver, Spring Expression Language provee un lenguaje rico y dinámico que te permitirá configurar los beans de tu aplicación Spring 3.0 de manera aún más flexible que en versiones anteriores, además de proveer un lenguaje de scripting que puedes usar dentro de tus clases a traves del parser.
En esta quinta entrega del tutorial de Groovy, vamos a ver el concepto de Metaprogramación. Mediante metaprogramación podemos escribir código que genera o modifica otro código (por ejemplo otros programas) o incluso a si mismos (código que tiene la habilidad de cambiar su comportamiento en tiempo de ejecución). Esto nos permite, entre otras cosas, manejar situaciones que no estaban previstas cuando se escribió el código, y sin necesidad de recompilar.
5.1 UN POCO DE REFLEXIÓN
La metaprogramación en Groovy, al igual que en Java, se basa en la capacidad del lenguaje llamada Reflexión. Mediante reflexión podemos conocer los miembros de una clase:
String.interfaces.each { println it }
String.constructors.each { println it }
String.methods.each { println it }
Cada una de las lineas del código anterior muestran los interfaces que implementa una clase, sus constructores y sus métodos, respectivamente. Para los dos últimos casos, se muestran solo los miembros públicos (public). Así mismo, todo objeto en Groovy tiene un método getClass() que devuelve el objeto Class...
En esta quinta entrega del tutorial de Groovy, vamos a ver el concepto de Metaprogramación. Mediante metaprogramación podemos escribir código que genera o modifica otro código (por ejemplo otros programas) o incluso a si mismos (código que tiene la habilidad de cambiar su comportamiento en tiempo de ejecución). Esto nos permite, entre otras cosas, manejar situaciones que no estaban previstas cuando se escribió el código, y sin necesidad de recompilar.
5.1 UN POCO DE REFLEXIÓN
La metaprogramación en Groovy, al igual que en Java, se basa en la capacidad del lenguaje llamada Reflexión. Mediante reflexión podemos conocer los miembros de una clase:
String.interfaces.each { println it }
String.constructors.each { println it }
String.methods.each { println it }
Cada una de las lineas del código anterior muestran los interfaces que implementa una clase, sus constructores y sus métodos, respectivamente. Para los dos últimos casos, se muestran solo los miembros públicos (public). Así mismo, todo objeto en Groovy tiene un método getClass() que devuelve el objeto Class asociado a su clase. Recuerda que podemos invocar la propiedad de una clase mediante su metodo getter, o directamente por su nombre (el getter es llamado en este caso implícitamente):
println String.class
Y hablando de propiedades, veamos como mostrar todas las propiedades de un objeto:
def s = new String("cadena de texto")
s.properties.each { propiedad ->
println propiedad
}
El ejemplo anterior nos muestra que todo objeto String contiene tres propiedades: class (la cual vimos en acción en el ejemplo anterior), bytes (un array de bytes que contiene el valor del String) y empty que como su nombre indica contiene un valor true en caso de que la cadena de texto tenga longitud cero y false en caso contrario. Ten presente que todas las propiedades de un objeto son deducidas por medio sus métodos getter/setter.
5.2 METACLASS
Toda clase Groovy dispone de un miembro llamado metaClass. Este miembro nos permite, entre otras cosas, comprobar si la clase dispone de una propiedad concreta gracias a su método hasProperty():
class Articulo {
String descripcion
double precio
}
def boligrafo = new Articulo(descripcion:"Boligrafo negro", precio:0.45)
if(boligrafo.metaClass.hasProperty(boligrafo, "precio")) {
// hacer algo
}
Así mismo, también podemos comprobar la existencia de un método con respondsTo():
if(boligrafo.metaClass.respondsTo(boligrafo, "getDescripcion")) {
// hacer algo
}
5.3 EJECUTANDO UN MÉTODO MEDIANTE GSTRING's
Una manera alternativa de invocar un método dinámicamente es construyendo la llamada con un objeto GString:
En la llamada al código anterior, cuando se ejecute el programa se sustituirá la expresión ${nombreDelMetodo} por su valor, produciendo así la llamada a getPrecio() en el objeto boligrafo.
5.4 PUNTEROS
En el capítulo 4 vimos como acceder directamente al valor de una variable (a una propiedad) mediante el operador @. Este 'acceso directo' se llama puntero:
println boligrafo.@precio
De manera parecida, mediante el operador &, podemos crear un puntero a un método:
En el código anterior, hemos creado un puntero (o alias) al método add() del objeto lista y lo hemos asignado a la variable insertar. Desde ese momento, podemos ejecutar comandos como insertar "valor1" y sería equivalente a ejecutar lista.add("valor1"). Esto puede ser muy intuitivo de usar para alguien que no entiende sobre programación en general y Groovy en particular. Esto es, hemos creado un DSL (Domain Specific Language - Lenguaje Especifico de Dominio). Un DSL es un lenguaje que se adapta al dominio al cual se refiere y aplica (por ejemplo el lenguaje jurídico para la jurisprudencia), etc. Así podemos crear un lenguaje fácil de usar, ya que su sintaxis refleja la terminología del problema que se quiere resolver.
5.5 EXPANDO
Un objeto Expando es como un objeto en blanco, al cual podemos añadir métodos y propiedades 'a la carta', y además de la forma más sencilla posible: dandoles un valor. Veamos un ejemplo:
En el código anterior hemos definido un objeto Expando llamado posicion, al cual le hemos proporcionado un valor para cada una de las propiedades longitud y longitud (propiedades que al no existir anteriormente para este Expando han sido creadas automáticamente). Dichas propiedades pueden ser después leidas como en un objeto normal. Pero vayamos un poquito más allá, vamos a definir un método para nuestro expando:
posicion.diferencia = { nuevaLatitud, nuevaLongitud ->
"""
Existe una diferencia de:
LATITUD: ${posicion.latitud - nuevaLatitud}
LONGITUD: ${posicion.longitud - nuevaLongitud}
con respecto al la posición anterior: ${posicion.latitud} ${posicion.longitud}
"""
}
Para definir un metodo simplemente tenemos que definir su nombre (evidentemente) y asignarle un closure. En el código anterior, dicho closure contiene un Heredoc donde se realizan algunos calculos con las variables pasadas como argumento y las que ya contenia el Expando, devolviendose finalmente una cadena de texto (recuerda que los Heredoc son cadenas de texto multilinea, y que al ser la última (y única) sentencia del closure es también su valor de retorno implícito). Ahora, podemos llamar al siguiente código:
posicion.diferencia(10.0, 1.0)
Como puedes ver, Expando es una herramienta tremendamente potente para crear nuevos objetos, y, como veremos un poco más adelante en este mismo artículo, para ayudarnos en nuestro camino a través de la metaprogramación.
5.6 PROPIEDADES DINÁMICAS
Aunque la clase Expando nos permite añadir propiedades de manera dinámica a un objeto de su mismo tipo, ¿que ocurre cuando queremos disponer de dicho dinamismo en nuestras propias clases? Groovy, como siempre, al rescate:
Groovy nos permite definir los métodos getProperty() y setProperty(), y el truco consiste en almacenar los valores pasados a setProperty() en un objeto Map (al que hemos llamado propiedades) para poder ser después leidos. Con esta infraestructura, ya podemos leer y escribir nuevas propiedades dinámicamente en nuestra clase Articulo:
def articulo = new Articulo()
articulo.codigoEAN = 84123445593
println articulo.codigoEAN
Cuando nuestro código llama a getCodigoEAN() la llamada es 'redirigida' internamente mediante getProperty('codigoEAN'), con total transparencia para nosotros y sin necesidad de que, en tiempo de compilación, exista esta propiedad ni sus correspondientes getters/setters. Un efecto secundario de definir propiedades dinámicas para una clase es que la lectura de una propiedad aún no definida (y por tanto inexistente) devolverá un valor null, en lugar de lanzar una excepción (un comportamiento en cierto modo parecido al producido por el operador de referenciación segura, como vimos en el punto 1.14, pero aplicado a propiedades en lugar de a objetos).
5.7 INTERCEPTANDO LLAMADAS A MÉTODOS
Groovy nos permite interceptar las llamadas a métodos que no existen:
class Articulo {
String descripcion
double precio
Object invokeMethod(String nombre, Object args) {
println "Invocado método ${nombre}() con los argumentos ${args}"
}
}
def articulo = new Articulo()
articulo.operacionInexistente('abc', 123, true)
En la clase anterior hemos definido el método invokeMethod(), el cual interceptará las llamadas a cualquier método no definido. Un efecto secundario de este comportamiento es que en tiempo de ejecución no se producirá una excepción informando de la no-existencia de dicho método. Sin embargo, la finalidad de invokeMethod() no es evitar este tipo de errores (ni debería serlo) si no, por ejemplo, la construcción de clases con un comportamiento totalmente dinámico, como constructores de XML y HTML (en los que los nombres de los métodos que pasemos pueden ser usados para darles nombre a los nodos XML-HTML del documento que estamos construyendo, y el cuerpo de cada método ser interpretado como un subnodo que será procesado de la misma manera).
Si lo que deseamos es interceptar las llamadas a métodos que si existen, además de definir el método invokeMethod() nuestra clase debe implementar la interface GroovyInterceptable. Dentro de invokeMethod() tenemos que obtener el método interceptado (a traves de su metamétodo) e invocarlo:
El código anterior define una sencilla clase de logging con dos métodos que muestran por pantalla información sobre el inicio o fin de un método. Ambos métodos son llamados en la primera y última linea dentro de invokeMethod(), de manera que cada vez que llamemos a un método de la clase Articulo se mostrará el mensaje correspondiente a su inicio y fin. Un tercera opción sería disponer de un método para loggear cualquier excepción que se produzca, pero este pequeño ejercicio lo dejo para tí. Una pregunta que podrías estar planteandote ahora es: ¿que ocurre si, con el código anterior, invocamos un método que no existe (como en nuestro primer ejemplo de este punto)? Pues que la variable metaMetodo contendrá un valor null (ya que getMetaMethod() no va a encontrar dicho método al no existir), y por tanto metaMetodo.invoke(this, args) producirá una excepción de tipo NullPointerException. La manera de evitar esto es tan simple como hacer uso del operador de referenciación segura:
def resultado = metaMetodo?.invoke(this, args)
Con la inclusion del operador de referenciación segura nuestra pequeña clase es capaz de interceptar las llamadas a TODOS sus métodos, existan o no:
articulo.noExiste()
5.8 CATEGORÍAS
Groovy permite la adición de nueva funcionalidad a una clase de la que, por ejemplo, no disponemos del código fuente. Esto es posible a traves de una Categoría, la cual implementa los métodos que deseamos añadir a dicha clase. Veamos un ejemplo aplicado a Articulo:
class Articulo {
String descripcion
double precio
}
Articulo articulo = new Articulo(descripcion:'Grapadora', precio:4.50)
use(ArticuloExtras) {
articulo.conImpuestos()
}
En el código anterior definimos una categoría llamada ArticuloExtras que define un método extra para la clase Articulo. Los metodos que deseamos añadir deben seguir la siguiente estructura:
Finalmente, aplicamos la categoría dentro de un bloque use, el cual requiere como parámetro el nombre de la categoría a usar. También podemos indicar más de una categoría dentro de un bloque use separando sus nombres con comas. Recuerda que los nuevos métodos solo estarán disponibles dentro del bloque use.
5.9 EXPANDOMETACLASS
El concepto de ExpandoMetaClass combina los conceptos de metaClass y Expando, permitiendo así añadir métodos a cualquier clase. El concepto es el mismo que ya hemos aplicado con las Categorias, solo que esta vez los métodos están disponibles a nivel global, sin necesidad de ceñirnos a su uso dentro de un bloque concreto. Un ejemplo típico del uso de ExpandoMetaClass es el siguiente:
Integer.metaClass.numeroAleatorio = {
def random = new Random()
random.nextInt(delegate.intValue())
}
50.numeroAleatorio()
El el código anterior añadimos el método numeroAleatorio() al objeto metaClass de la clase Integer, asignandole un closure que contiene el cuerpo del método. La primera linea dentro del closure define un objeto Random y lo asigna a una variable. Es en la segunda linea de código donde se genera la magia: llamamos al método Random.nextInt() pasandole como parámetro el propio objeto donde se está realizando la llamada a numeroAleatorio(), esto es, delegate (en este caso concreto será una instancia de Intgeger), y a continuación se llama a su método intValue() que devuelve un int en lugar del Integer original. Puesto que la capacidad de Java de hacer autoboxing y autounboxing tambíen está disponible en Groovy, podemos escribir el código de la siguiente manera:
random.nextInt(delegate)
Lo importante es que tengas presente que delegate hace referencia al objeto 'delegado', el objeto que estará disponible en tiempo de ejecución y desde el que se ejecutara el bloque de código con el que estamos tratando. Otro objeto que podemos alcanzar desde dentro del closure es el propio closure, en este caso con la palabra this. Espero que resulte sencillo de comprender (yo tuve mis problemas en su momento, aunque no lo parezca por su sencillez).
Hay todavía mucho que decir sobre metaprogramación, sobre todo ver ejemplos más complejos que los aquí mostrados, pero todos los temas estan tratados y dejo en tu mano profundizar más en el tema. La metaprogramación puede ser dificil de entender para los que venimos de lenguajes estáticos como Java, donde apenas se hace uso de las librerias de reflexión (si alguien las usa habitualmente que me perdone por ese comentario) y todo se deja más bien atado en tiempo de compilación. Sea como sea, la metaprogramación nos permite hacer cosas realmente increibles, y nunca esta de más tener unos conocimientos básicos sobre ella. En el próximo capítulo de este tutorial (que tal vez sean dos o más por su longitud) se verá como usar el lenguaje para tareas cotidianas, como parsear y generar XML, leer y escribir archivos, y algunos temas más; en resumen, un 'manos a la obra' con todo lo que hemos aprendido hasta ahora y algunas cositas nuevas.
En esta cuarta entrega del tutorial de introducción a Groovy vamos a ver que son y como funcionan los POGO's (Plain Old Groovy Objects). Los POGO's son el sustituto natural de Groovy a los POJO's de Java, tomando toda la funcionalidad y potencia de Groovy para construir objetos.
4.1 POGO's
Los POGO's, al igual que los POJO's en Java, son clases simples que no dependen de un framework en especial (cita de la Wikipedia). Esto es, clases que no heredan ni implementan ninguna clase/interface de un API en concreto y externo a nuestra aplicación, y que por tanto pueden ser reusados una y otra vez entre aplicaciones. Este concepto de reuso de clases es uno de los pilares básicos de la programación orientada a objetos. A efectos practicos, un POGO es una clase normal y corriente:
class Libro {
String titulo
String autor
int numPaginas
}
La clase del código anterior define un libro con tres atributos que pueden ser leidos/escritos directamente, ya que Groovy generará los método...
En esta cuarta entrega del tutorial de introducción a Groovy vamos a ver que son y como funcionan los POGO's (Plain Old Groovy Objects). Los POGO's son el sustituto natural de Groovy a los POJO's de Java, tomando toda la funcionalidad y potencia de Groovy para construir objetos.
4.1 POGO's
Los POGO's, al igual que los POJO's en Java, son clases simples que no dependen de un framework en especial (cita de la Wikipedia). Esto es, clases que no heredan ni implementan ninguna clase/interface de un API en concreto y externo a nuestra aplicación, y que por tanto pueden ser reusados una y otra vez entre aplicaciones. Este concepto de reuso de clases es uno de los pilares básicos de la programación orientada a objetos. A efectos practicos, un POGO es una clase normal y corriente:
class Libro {
String titulo
String autor
int numPaginas
}
La clase del código anterior define un libro con tres atributos que pueden ser leidos/escritos directamente, ya que Groovy generará los métodos getter/setter por nosotros en tiempo de compilación. Es más, a pesar de que dichos métodos estarán disponibles cuando compilemos y llamemos a la clase, podemos llamar a los atributos directamente por su nombre:
def libro = new Libro()
libro.titulo = "Introducción a Groovy"
libro.autor = "David Marco"
libro.numPaginas = 0
Esta sintaxis, basada en la notación de puntos (objeto.atributo) es también válida para leer valores:
println libro.titulo
4.2 GETTERS Y SETTERS
En ciertas ocasiones, sin embargo, debemos controlar como se almacenan y leen los valores de los atributos. Nada tan sencillo como sobreescribir el getter/setter correspondiente:
class Libro {
String titulo
String autor
int numPaginas
def libro = new Libro()
libro.titulo = "Introducción a Groovy"
libro.autor = "David Marco"
libro.numPaginas = 0
println libro.titulo
En la definición de la clase Libro del código anterior hemos proporcionado explícitamente un método setter para el atributo titulo, de manera que todas las llamadas a libro.titulo serán redirigidas a través de este método customizado. Es probable que hayas notado algo rato en la definición del método anterior (vuelve atrás y miralo bien...). ¿Ya? Como seguramente has observado, el método no es público, y la norma general es que los métodos getter/setter (sobre todo los getter) sean public, ya que de otra manera nuestra clase sería muy poco usable. Esto nos lleva al siguiente punto de este artículo.
4.3 NIVELES DE ACCESO
En Groovy prima en cierta manera el concepto de Convención sobre Configuración, esto es, que la sintaxis más simple refleja el comportamiento mas común de un componente. Lo vimos en el punto anterior con los getters y setters por defecto y tambíén aplica de la siguiente manera a los niveles de acceso por defecto en POGO's:
- Todas las clases son públicas por defecto
- Todos los métodos son públicos por defecto
- Todos los atributos son privados por defecto
Estas reglas, sin embargo, conllevan ciertas matizaciones que debemos comprender. Veamos un ejemplo:
class Libro {
String titulo
String autor
private int numPaginas
}
En el ejemplo anterior, hemos definido explícitamente el atributo numPaginas como privado. Esto indica al compilador que no debe generar métodos getter/setter para este atributo, de manera que el atributo no será visible de ninguna manera para un cliente Java. Y digo solamente clientes Java porque Groovy puede seguir accediendo al atributo, para leerlo y escribirlo. Esto es debido a que Groovy tiene ciertos privilegios al usar otro código Groovy, llamemoslo Despotismo Groovy. Incluso aunque proporcionemos métodos setter/getter privados para ese atributo, ¡Groovy podrá seguir accediendo a el!. Un efecto secundario de este comportamiento es que el nivel de visibilidad default-package (el que se venia aplicando hasta ahora en Java a todo método, campo y clase interna sin un nivel de visibilidad concreto) no existe en Groovy. No voy a discutir aquí los motivos por los que el lenguaje funciona de esta manera ni mucho menos a opinar al respecto, ambos temas caen fuera de los propositos de este tutorial. Sin embargo, debes tener muy presente este comportamiento cuando uses el lenguaje. Y como he dicho antes, desde Java el código se comportará como lo ha venido haciendo hasta ahora, respetando los niveles de acceso (que para eso están ahí), por lo que aún puedes controlar la visibilidad de los miembros de una clase escrita en Groovy (salvo el ya mencionado package-default que deja de existir) desde Java.
Un privilegio extra de Groovy es que puede leer y escribir un atributo directamente, sin pasar por el método getter/setter correspondiente (sea este generado implícitamente por el compilador o explícitamente por nosotros):
println libro.@titulo
En la sentencia anterior hemos utilizado el caracter @ para indicar que debemos acceder a la variable titulo directamente.
Por último, y aunque no está relacionado con la discusión sobre niveles de acceso, vamos a ver una tercera manera de leer y escribir atributos en un POGO:
def libro = new Libro()
libro.setProperty('titulo', 'Introducción a Groovy')
println libro.getProperty('titulo')
La sintaxis anterior puede resultar muy útil en ciertos casos, por ejemplo cuando no disponemos en tiempo de compilación de los nombres de los atributos que tenemos que llamar, pero si en tiempo de ejecución.
4.4 CONSTRUCTORES
Groovy nos ofrece una sintaxis especial para crear on POGO en una única linea:
def libro = new Libro(titulo:'Introducción a Groovy', autor:'David Marco', numPaginas:0)
Si recuerdas el capítulo 3, verás que los valores que se han pasado al constructor son en realizad una estructura de tipo Map. Por tanto, también podemos pasar al constructor un mapa previamente definido:
def mapa = [titulo:'Introducción a Grails', autor:'David Marco', numPaginas:0]
def libroGrails = new Libro(mapa)
println mapa.titulo
Pero, ¡aún hay más! Esta sintaxis es válida incluso para crear un POJO (un objeto Java), así que nos podemos beneficiar de ella cuando estemos usando objetos creados en Java. Ahora es el momento de preguntarnos: ¿Estamos obligados a pasar todos los atributos de un objeto al construir dicho objeto?. La respuesta es NO:
def libro = new Libro(titulo:'Introducción a Groovy')
El resto de los atributos pueden ser establecidos en cualquier momento posterior a la creación del objeto y de cualquiera de las maneras que hemos visto hasta ahora. Así de simple. Esto nos lleva a otra pregunta: ¿Debemos pasar los atributos en algún tipo de orden? La respuesta es, de nuevo NO, cualquier orden es valido.
Otra poderosa característica de los constructores en Groovy es que podemos proporcionar a cualquier atributo un valor por defecto si ninguno es proporcionado:
class Libro {
String titulo
String autor
int numPaginas
Libro(int numPaginas, String titulo='Sin título', String autor='Sin autor') {
this.titulo = titulo
this.autor = autor
this.numPaginas = numPaginas;
}
}
def libro = new Libro(10)
println libro.titulo
En el ejemplo anterior, se ha definido un constructor que además de inicializar los valores de los atributos de la clase, declara dos de ellos como opcionales dandoles un valor por defecto (titulo y autor). Por tanto, si instanciamos un objeto Libro y no proporcionamos los parámetros que corresponden a dichos atributos opcionales, se tomarán sus valores por defecto. Además, en la declaración del constructor, los parámetros opcionales deben aparecer después de los parámetros obligatorios para ese constructor. A la hora de diseñar el constructor debes ordenar cuidadosamente los parámetros opcionales, de manera que los que tienen más posibilidad de ser proporcionados deben aparecer primero (aunque esta anticipación al uso de la clase no es siempre posible). Nuestra nueva clase Libro es perfecta para mostrarnos este asunto:
def libro = new Libro(0, 'David Marco')
println libro.titulo
Mira bien el código anterior: ¿Que atributo estamos ajustando con el parámetro 'David Marco'? ¿titulo o autor (recuerda que ambos son String y ambos son opcionales)?. En este caso concreto, es titulo, aunque mi ingenua intención era ajustar el atributo autor, y por tanto he construido un objeto que, cuando sea usado, no se comportará como yo espero. Piensa cuidadosamente en el orden de los parámetros opcionales y, más aún, usalos con mucho cuidado y en situaciones donde realmente son necesarios y/o útiles.
Con este consejo termina la cuarta entrega del tutorial de introduccíón a Groovy (un poquito más corto de lo habitual, pero espero que igual de interesante). En la próxima entrega abordaremos la Metaprogramación en Groovy, un estilo de programar realmente increible. ¡Hasta pronto!
En los capítulos 1 y 2 del tutorial de Groovy vimos los conceptos básicos del lenguage, así como algunas estructuras de datos. En esta tercera entrega vamos a ver dos tipos nativos del lenguaje: Rangos y Colecciones.
3.1 RANGOS
Los rangos son un tipo de datos nativo, derivado de List, que nos permite definir una lista de valores secuenciales. Veamos el ejemplo mas simple:
def rango = 1..5
def rango = 1..<5
El primer rango arriba definido es una lista con los valores del 1 al 5, ambos inclusive. El segundo rango contiene una lista con los valores del 1 al 4, pues el último valor ha sido excluido. Ahora que sabemos como definir un rango, podemos iterarlo llamando al método each(), el cual acepta un closure como argumento (recuerda que si el único o último parámetro de un método es un closure, podemos definir dicho closure a continuación de la llamada al método):
rango.each {
println it
}
<...
En los capítulos 1 y 2 del tutorial de Groovy vimos los conceptos básicos del lenguage, así como algunas estructuras de datos. En esta tercera entrega vamos a ver dos tipos nativos del lenguaje: Rangos y Colecciones.
3.1 RANGOS
Los rangos son un tipo de datos nativo, derivado de List, que nos permite definir una lista de valores secuenciales. Veamos el ejemplo mas simple:
def rango = 1..5
def rango = 1..<5
El primer rango arriba definido es una lista con los valores del 1 al 5, ambos inclusive. El segundo rango contiene una lista con los valores del 1 al 4, pues el último valor ha sido excluido. Ahora que sabemos como definir un rango, podemos iterarlo llamando al método each(), el cual acepta un closure como argumento (recuerda que si el único o último parámetro de un método es un closure, podemos definir dicho closure a continuación de la llamada al método):
rango.each {
println it
}
En el código anterior, iteramos a través del rango imprimiendo cada uno de sus valores. Si no vamos a reusar el rango, podemos definirlo "al vuelo", insertandolo entre parentesis:
(1..5).each {
println it
}
Una forma alternativa de iterar a través de los valores de un rango es mediante un bucle for:
for(int contador in 1..5) {
println contador
}
Además de each(), los rangos disponen de una serie de atributos y métodos que podemos llamar en nuestra aplicación:
El atributo from devuelve el valor de inicio del rango, mientras que el atributo to devuelve el último valor. El método contains() devuelve true si su argumento está incluido en el rango, y size() devuelve el tamaño del rango. Por último, podemos obtener el valor almacenado en un índice del rango (recuerda que es una lista) mediante el método get(indice) o mediante notación de arrays (rango[indice]). Otra opción permitida por un rango es la de invertirlo, gracias al método reverse():
def rango = 10..15
def inverso = rango.reverse()
inverso.each {
println it
}
Hasta ahora hemos utilizado rangos que contenian valores enteros; sin embargo, cualquier clase que implemente el interface Comparable, así como los métodos next() y previous, puede ser usado como valor de un rango. Este es el caso, por ejemplo, de Date():
def hoy = new Date()
def dentroDeSieteDias = hoy + 7
(hoy..dentroDeSieteDias).each { dia ->
println dia
}
Otro ejemplo de clase soportada en rangos es String:
('a'..'z').each { letra ->
println letra
}
Los rangos ofrecen mucho más de lo aquí explicado, y como siempre te invito a que visites la documentación oficial de Groovy. Pero antes de terminar esta sección, veamos un último ejemplo que seguro te resulta util en el futuro: rangos como valores de comparación en un bucle switch:
def sueldo = 1700;
switch(sueldo) {
case 600..<1200:
println 'nivel 1'
break
case 1200..<1800:
println 'nivel 2'
break
case 1800..<2400:
println 'nivel 3'
break
}
El código es, sin duda, autoexplicativo.
3.2 COLECCIONES
Groovy ofrece soporte nativo para colecciones de tipo List y Map, así como conversión explícita de listas en colecciones de tipo Set y Queue. Veamos uno por uno estos tipos.
3.3 LISTAS
Las listas son colecciones secuenciales cuyos elementos pueden ser accedidos mediante un índice. Veamos como crear una lista vacia en Groovy:
def lista = []
println lista.class
El código anterior define una lista sin elementos y la almacena en la variable lista. A continuación imprime su tipo (ArrayList en mi versión de Groovy, 1.7.2). Es posible insertar elementos en la lista en el momento de su creación:
En el código anterior hemos definido una lista con dos elementos de tipo String. A continuación hemos llamado al método size() que nos devuelve el número de elementos de una lista. Ahora es el momento de añadir elementos a una lista previamente creada. Las dos maneras mas comunes de hacerlo son a traves del operador 'leftshift' (<<, un operador sobrecargado como se explicó en la sección 1.13) y del metodo add() del interface List, como habriamos hecho hasta ahora en Java:
Las dos primeras lineas del código anterior añaden un String cada a la lista paises. Ahora podemos obtener cualquier objeto de la lista por medio de su índice numérico, y también de dos maneras: mediante notación de arrays y mediante el método getAt():
def pais = paises[0]
def otroPais = paises.getAt(1)
Recuerda que las posiciones de una lista están basadas en índice cero: el primer elemento está en la posición 0, el segundo elemento en la posición 1, etc. La notación de arrays nos permite, además de sobreescribir el valor de una posición, insertar un nuevo elemento en una posición explícita, añadiendo valores null en todas las posiciones intermedias:
paises[3] = "Colombia"
paises[6] = "Ecuador"
El código anterior sobreescribe la posición número 3 con el valor Colombia, y a continuación define en la posición 6 el valor Ecuador. Las posiciones 4 y 5, las cuales aún no han podido ser definidas pero deben existir para mantener la secuencialidad, son 'rellenadas' con valores null; así mantenemos intacto el índice de la lista. Tras ejecutar el código anterior: nuestra lista de paises quedara de la siguiente manera:
Otra opción necesaria en una colección es la de eliminar objetos, operación que podemos llevar a cabo mediante el método remove(), el cual admite como argumento tanto el índice de la posición como el objeto a eliminar, y devuelve el valor eliminado:
Ambas lineas del código anterior realizar la misma operación para los valores concretos de nuestra lista: eliminar la posición 6 / eliminar el valor "Ecuador". Es importante que tengas en cuenta que si eliminamos por índice, y este no existe, obtendremos una excepción de tipo NullPointerException. Si eliminamos pasando un objeto que no existe obtendremos como resultado null, pero sin que se lance ninguna excepción. Antes de continuar vamos a eliminar los dos valores null que están embebidos en nuestra lista:
2.times {
paises.remove(null)
}
El código anterior ejecuta el método times de la clase Number (y por tanto heredado por Integer). Este método ejecuta el closure pasado como argumento tantas veces como el valor de la variable donde es invocado, en nuestro caso 2.
Una tercera manera de eliminar un elemento de una lista es mediante el método pop(), el cual devuelve y a elimina el último elemento de la lista (no el último elemento añadido, que podría haber sido insertado a traves de notación de arrays en medio de la lista, si no el elemento que tiene el índice más alto). Este método permite tratar una lista como una cola LIFO (Last In, First Out - El último en entrar, el primero en salir) aunque, como veremos después, existe una manera más adecuada de crear colas de datos:
Ahora el contenido de nuestra lista será el siguiente:
[España, Mexico, Argentina, Colombia]
Veamos ahora como iterar a través de los elementos de una lista:
paises.each {
println it.toUpperCase()
}
En el código anterior hemos llamado al método each(), el cual acepta un closure que es ejecutado por cada elemento de la lista. Dentro del closure hemos imprimido en mayúsculas cada elemento (recuerda que está lista contiene elementos de tipo String). Como explicamos en la sección 2.5, podemos utilizar una variable definida por nosotros cuando el closure acepta un unico parametro, sustituyendo así a la variable explicita it y haciendo el código más expresivo:
paises.each { pais ->
println pais.toUpperCase()
}
Una segunda manera de iterar una lista es mediante el método eachWithIndex, el cual acepta un closure con dos parámetros: uno para el valor de cada elemento iterado y otro para almacenar su posición (o número de iteración):
paises.eachWithIndex { pais, indice ->
println "${pais} se encuentra en la posición ${indice}"
}
Existe una forma alternativa de iteración, que ejecuta una acción en cada elemento de la lista y devuelve una nueva lista conteniendo el resultado de dichas ejecuciones. Esto es llevado a cabo mediante el método collect():
def paisesMayusculas = paises.collect { pais ->
pais.toUpperCase()
}
En el código anterior, hemos creado una nueva lista, llamada paisesMayusculas que contiene el resultado de convertir en mayusculas todos los valores de la lista paises:
[ESPAÑA, MEXICO, ARGENTINA, COLOMBIA]
En determinadas ocasiones, necesitamos ordenar una lista de manera natural, esto es, en base a los objetos que están contenidos en ella. Por ejemplo, para nuestra lista conteniendo objetos String la ordenación natural sería la alfabética:
paises.sort()
El fragmento de código anterior ordena la lista paises, modificando la lista original y dejando sus elementos (del tipo String) en orden alfabético):
[Argentina, Colombia, España, Mexico]
Otro método de utilidad es reverse() el cual devuelve una lista en sentido inverso, pero sin modificar la lista original. Por tanto, para capturar la nueva lista debemos asignarla a una variable:
def paisesInvertidos = paises.reverse()
Groovy nos permite añadir/sustraer una lista a/de otra mediante los operadores += y -=. Veamos un ejemplo:
Las dos primeras lineas del código anterior crean dos listas, una conteniendo números pares y otra conteniendo números impares. A continuación, en la tercera linea, añadimos la lista de números impares a la lista de números pares, y en la cuarta linea ordenamos la nueva lista mixta con sort() (como la lista contiene números enteros, la ordenación se realiza por valor numérico, esto es: 1, 2, 3, 4, ...). En la quinta y última linea, eliminamos de la lista ordenada todos los valores de la lista de números impares, dejandola evidentemente solo con los valores pares originales.
Otro método de gran utilidad es join(), el cual une los valores de una lista en una cadena de texto:
La primera llamada a join devuelve una cadena de texto conteniendo el valor abc, mientras que la segunda llamada devuelve también una cadena de texto pero, esta vez, con los valores de la lista separados por el String que hemos pasado como argumento; en nuestro caso, en el que hemos pasado como argumento un caracter de guión, el resultado es a-b-c. Ninguno de las versiones de join() modifica la lista original.
Groovy añade todavía más métodos de utilidad en las listas: max() y min, por ejemplo, devuelven los valores máximo y mínimo contenidos en una lista (como siempre, el concepto de máximo, mínimo, ordenación, etc. está relacionado con el tipo de objeto que almacena la lista):
println letras.max()
println letras.min()
En el código anterior, el método max() devuelve el valor c, ya que la ordenación de cadenas de texto en Java establece que c es un caracter 'mayor' que a o b. De igual manera, en la siguiente lista conteniendo números flotantes, el valor máximo corresponderia a 4.71 y el valor mínimo a 0.02:
Como puedes ver, el soporte de listas en Groovy es tremendamente util a la vez que sencillo. Aquí solo se han explicado algunos metodos de los muchos que provee el API, así que te recomiendo que estudies la documentación para encontrar información adicional.
3.4 ARRAYS, SET's Y COLAS
Por defecto, todas las listas en Groovy son implementaciones de la clase ArrayList. Sin embargo, podemos coaccionar al lenguaje para que convierta una lista en una implementación de Set (en la versión 1.7.2 HashSet), de Queue (LinkedList) o en un array:
def setPaises = paises as Set
def colaPaises = paises as Queue
def arrayPaises = paises as String[]
println setPaises.class
println colaPaises.class
println arrayPaises.class
También podemos aplicar la sintaxis anterior en el momento de la creación de la colección, para así trabajar con el tipo deseado desde el primer momento:
def paises = ["España", "Mexico"] as Set
Y por supuesto, podemos seguir instanciando nuestras colecciones mediante la antigua sintaxis de Java. Recuerda que el 99% del código Java es código Groovy válido:
Set lhs = new LinkedHashSet();
3.5 MAPS
Los mapas son colecciones de valores que pueden ser referidas por una clave, o lo que es lo mismo, una colección de parejas clave-valor. Groovy ofrece soporte nativo también para mapas, con una sixtaxis que en gran medida es muy similar a la de las listas:
def mapa = [:]
El código anterior crea un mapa vacio, en el cual podemos, por supuesto, ir añadiendo parejas clave-valor (posteriormente veremos como). Si deseas inicializar un mapa con valores predefinidos:
Las tres lineas de código anterior realizan la misma tarea, obtener un valor a partir de su clave. La primera utiliza el método get(clave) para obtener el valor asociado a dicha clave. La segunda linea utiliza la notacion de puntos, en la forma mapa.clave. La última versión utiliza la notación de arrays. Cual uses depende en gran medida de tus preferencias. Es importante subrayar que cuando utilices notación de puntos debes encerrar entre comillas el nombre de las claves que contengan ciertos caracteres especiales, por ejemplo, un caracter de punto:
def datos = ['correo.electronico', 'programacion@davidmarco.es']
println datos.'correo.electronico'
El mapa definido en el código anterior contiene una clave que contiene un caracter de punto, de manera que al usar notación de puntos debemos encerrar el nombre de la clave entre comillas (tanto comillas simples como dobles están permitidas). Si no lo hacemos, obtendremos un error de compilación, ya que al haber más de un punto este no sabría donde comienza el nombre de la clave. Veamos ahora como añadir elementos a un mapa:
Las tres lineas de código anterior insertan una nueva pareja clave-valor en el mapa capitales. Al igual que al obtener un valor, cuando insertamos mediante notación de puntos un valor que contiene un punto mediante debemos encerrar la clave entre comillas. La última operación básica que nos queda es la de borrar una pareja clave-valor mediante el método remove(clave):
capitales.remove('Argentina')
La forma de iterar las parejas clave-valor de un mapa es tan sencilla como en una lista:
capitales.each {
println it
}
El código anterior obtendria las parejas como tales. Si deseamos acceder a las claves y valores de forma independiente tenemos dos opciones: la primera consiste en usar los atributos key y/o value de la variable que almacena la pareja en cada iteración:
capitales.each {
println it.key
}
La segunda manera consiste en pasar como parámetro a each() un closure con dos variables, la primera de ellas haciendo referencia a la clave correspondiente y la segunda a su valor:
capitales.each { pais, capital ->
println "La capital de ${pais} es ${capital}"
}
Otros muchos métodos que vimos en la sección 3.3 (LISTAS) también están disponibles para mapas, como son collect() y sort(). Otros tienen una sintaxis ligeramente diferente (como reverseEach(Closure) en lugar de reverse()). Visita el API de Groovy para ver todos los métodos disponibles en Map. Otras operaciones, como adición y sustraccion entre mapas estan, también soportadas, aunque de nuevo de una manera un tanto diferente a como las realizabamos con listas. Por ejemplo, la adición de una lista a otra funciona de manera igual, mediante el operador +=:
Sin embargo, la sustracción mediante el operador -= no está soportada en mapas, por lo que hay que realizar un poco de trabajo extra para llevar a cabo esta operación:
angloParlantes.each {
capitales.remove(it.key)
}
En el código anterior iteramos a traves del mapa que queremos eliminar, seleccionamos cada uno de las claves, y se las pasamos al método remove del mapa del cual queremos sustraer los valores.
Antes de terminar esta sección vamos a ver cuatro nuevos métodos. Los dos primeros métodos son keySet() y values(). El primero devuelve una lista con todas las claves de un mapa, mientras que el segundo devuelve una lista conteniendo todos los valores de un mapa:
def paises = capitales.keySet()
El código anterior almacenaria en la variable paises una lista con las claves del mapa capitales:
[España, Mexico, Argentina]
Por ultimo, los métodos containsKey() y containsValue() devuelven true o false dependiendo de si el mapa donde los llamemos contiene cierta clave o cierto valor:
Para el código anterior, la primera linea devolverá true mientras que la segunda devolverá false.
En este capítulo hemos visto diversos tipos de colecciones, como rangos, listas y mapas. El API de estos objetos es mucho mas amplio de lo que, por motivos de espacio, podemos (y tal vez debemos) mostrar aquí. Sin embargo, con lo aquí citado podemos comenzar a trabajar comodamente con todos estos tipos de colecciones. En posteriores capítulos las veremos y usaremos de estas y otras maneras. En el próximo capítulo veremos como trabajar con POGO's, el equivalente "Grooviano" del clasico POJO. Hasta entonces, ¡un saludo!.
Como anuncié hace unos dias en el blog, he estado preparando un sistema de feeds RSS para que podáis seguir más facilmente la actividad de la web. He tenido que actualizar la aplicación Spring MVC con la que gestiono el blog para que permitiera la publicación de las entradas también en formato RSS, publicación que es llevada a cabo en última instancia a traves de una sencilla clase (menos de 25 lineas de código) escrita en Groovy. Podéis suscribiros al blog a traves de la url http://www.davidmarco.es/rss/blog.xml, o pinchando en el icono que encontrareis en la barra de direcciones de vuestro navegador. Por último, pero no por ello menos importante, quiero agradecer a Felix Ernesto Orduz la idea, hace ya muchas semanas, de este feed RSS.
En esta segunda entrega del tutorial de introducción a Groovy vamos a ver como trabajar con cadenas de texto, así como el concepto de Closure. En la primera entrega vimos las características más basicas del lenguaje, así como las reglas de sintaxis. Esta entrega es una continuacíon de la anterior, pues muestra caracteristicas del lenguaje que son comunmente utilizadas.
2.1 TIPOS DE CADENAS DE TEXTO
Groovy soporta de forma nativa tres tipos de cadenas de texto: Strings, GStrings y Heredocs. Cual usar en cada momento es una decisión que surgirá de las necesidades de nuestro código, aunque en última instancia todas ellas son intercambiales por cualquier otra.
2.2 STRINGS
Un String en Groovy es similar a un String en Java, con la particularidad de que puede ser construido usando tanto comillas simples como dobles:
def cadena1 = "Esto es válido en Java y Groovy"
def cadena2 = 'Esto es válido solo en Groovy'
println cadena1.class
println cadena2.class
Si observamos la salida de ambas sentencias println...
En esta segunda entrega del tutorial de introducción a Groovy vamos a ver como trabajar con cadenas de texto, así como el concepto de Closure. En la primera entrega vimos las características más basicas del lenguaje, así como las reglas de sintaxis. Esta entrega es una continuacíon de la anterior, pues muestra caracteristicas del lenguaje que son comunmente utilizadas.
2.1 TIPOS DE CADENAS DE TEXTO
Groovy soporta de forma nativa tres tipos de cadenas de texto: Strings, GStrings y Heredocs. Cual usar en cada momento es una decisión que surgirá de las necesidades de nuestro código, aunque en última instancia todas ellas son intercambiales por cualquier otra.
2.2 STRINGS
Un String en Groovy es similar a un String en Java, con la particularidad de que puede ser construido usando tanto comillas simples como dobles:
def cadena1 = "Esto es válido en Java y Groovy"
def cadena2 = 'Esto es válido solo en Groovy'
println cadena1.class
println cadena2.class
Si observamos la salida de ambas sentencias println veremos que las dos variables definidas previamente, cadena1 y cadena2, son de tipo java.lang.String. Un tipo de comillado puede contener al otro en su interior, sin necesidad de (aunque está permitido) escapar el comillado interior:
2.3 GSTRINGS
GStrings (o Groovy Strings) es una cadena de texto que contiene expresiones embebidas. Un GStrings solo puede ser construido con comillas dobles o triples (las cuales veremos en el apartado 2.4). Veamos un ejemplo simple:
def saldo = 1821.14
println "El saldo es de ${saldo} euros"
En el código anterior, vemos que dentro de la cadena de texto pasada a println hemos insertado la variable saldo como si fuera parte de la propia cadena de texto. Esta inserción se hace dentro del operador ${} y es evaluada en tiempo de ejecución. Como se indicaba en el párrafo anterior, podemos insertar cualquier expresión dentro de un GString:
def saldo = 1821.14
def mensaje = "El saldo a fecha ${new Date()} es de ${saldo} euros"
println mensaje
Es evidente que el código anterior es mucho mas natural, tanto al escribirlo como al leerlo, que su homologo en Java (el cual necesitaria unir cada parte de texto con cada expresión mediante el operador +). Es importante tener siempre presente que un GString no es un String, y que usarlos en combinación puede producir resultados inesperados:
La primera sentencia println devuelve false, ya que ambas clases implementan sus métodos equals y hashCode de forma diferente. El mismo problema puede surgir (y surgirá) si mezclamos String's y GString's en sitios tales como las claves de un Map. Una forma de solventar este problema es invocar el método toString() en el objeto GString, como puede verse en la segunda sentencia println del código anterior (la cual SI devuelve true pues ambos objetos a comparar ahora si son del mismo tipo).
2.4 HEREDOCS
El último tipo de cadena de texto soportado en Groovy es el Heredoc, el cual se forma con tres comillas simples o dobles:
println """Esto es un Heredoc"""
Los Heredoc's nos permiten tanto almacenar cadenas de texto multilinea en una única variable como mezclar comillas simples y dobles en su interior sin necesidad de escaparlas:
def multilinea = """
Primera linea
Segunda linea
Tercera linea con "comillas dobles" y 'comillas simples'
"""
println multilinea
Por las dos características arriba mencionadas, los Heredocs son tremendamente útiles para almacenar XML y HTML en una variable. Los Heredocs formados con comillas dobles permiten insertar cualquier expresión, resultando en un GString multilinea. Realmente los Heredocs no son un tipo (de ahí que no aparece su nombre en otra fuente, como ocurre por ejemplo con los String's y GString's), sino más bien una caracteristica del lenguaje para escribir cadenas multilinea. Esto podemos comprobarlo de la siguiente manera:
println '''heredoc con comillas simples'''.class
println """heredoc con comillas dobles""".class
println """heredoc con comillas dobles y expresión embebida: ${new Date()}""".class
Las dos primeras sentencias println del código anterior devuelven java.lang.String como tipo del heredoc, mientras que la tercera expresión devuelve org.codehaus.groovy.runtime.GStringImpl, ya que detecta la expresión embebida y automaticamente trata la cadena como si fuera un GString.
2.5 CLOSURES
Los closures son una de las caracteristicas mas potentes del Groovy, y para aquellos que solo han trabajado con Java tal vez la más dificil de comprender y utilizar correctamente (en otros lenguajes hay construcciones comparables a los closures de Groovy). Empecemos con su definición: un closure es un bloque de código autónomo (fuera de cualquier clase) que puede ser definido y usado en puntos distintos. Veamos el ejemplo más simple posible:
def saludar = { println '¡Hola Mundo de los Closures!' }
En el código anterior se define un closure, siempre entre llaves, y se asigna a una variable llamada saludar. Para ejecutar este closure podemos invocarlo como si fuera un método (a efectos practicos lo es):
En el ejemplo anterior, utilizamos dentro del closure la variable it, la cual es implícita para todos los closures que no definen ningun parametro. Para cerrar el círculo y entender que significa esto, declaremos un closure con parametros explícitos:
def saludar3 = { nombre ->
println "¡Hola ${nombre}!"
}
saludar3 "David"
En este último ejemplo, al contrario del anterior, hemos definido un parámetro explícito llamado nombre que sustituye a it y hace el código más legible y consonante con su función final (saludar usando un nombre). Este parámetro debe declararse antes del código del closure y separarse de este con los caracteres ->. Por supuesto, podemos declarar más de un parámetro para un closure:
Recuerda que, como se explicó en el punto 1.9 del capítulo anterior, Groovy permite omitir los parentesis al llamar a una función a la que le pasamos argumentos (y tal como hemos dicho unos parrafos mas arriba los closures son a efectos practicos funciones) de ahí que la llamada a los closures saludar2, saludar3 y saludar4 puedan parecer un poco extrañas.
2.6 CURRYING
Currying es una técnica que nos permite pre-cargar valores en los parámetros de un closure. Veamos un ejemplo:
En el ejemplo anterior hemos pre-cargado el valor "Anonimo" para el unico parametro del closure saludar5, y el closure resultante lo hemos almacenado en la variable anonimo. A continuación hemos llamado a dicho closure, resultando en una llamada equivalente a:
saludar5 "Anonimo"
Este ejemplo, tan simple como estúpido (espero que estés de acuerdo conmigo) no muestra la verdadera potencia del currying; su unica misión es mostrarnos el concepto de la manera más simple posible. Ahora es el momento de ver algo más práctico:
El currying muestra toda su potencia cuando trabajamos con múltiples parametros, ya que nos permite pre-cargar valores que serán siempre los mismos para una determinada función y pasar el resto en tiempo de ejecución. En el ejemplo anterior, hemos creado dos closures, doble y triple que hacen currying sobre el closure multiplicar, pre-cargando los valores 2 y 3 respectivamente en su parametro más a la izquierda, valor1. Así, al llamar a doble(7) y triple(7) lo que hacemos es asignar el valor 7 al primer parametro sin definir de multiplicar, en nuestro caso valor2 (ya que, vuelvo a insistir, valor1 fue asignado con un valor por defecto al hacer currying).
2.7 CLOSURES COMO PARAMETROS
Una de las caracteristicas más potentes de los closures es que pueden ser utilizadas como argumentos de una función:
def repetirClosure(int numRepeticiones, Closure closure) {
for(int i = 0; i < numRepeticiones; i++) {
closure.call(i)
}
}
def closure = { println it }
repetirClosure(5, closure)
En el ejemplo anterior, hemos definido un método (repetirClosure) que acepta como parámetro un closure. Dentro de dicho método, invocamos el closure dentro de un bucle mediante el método call(), al cual podemos pasarle cualquier parámetro que sera enviado directamente al closure (esto es equivalente a ejecutar el closure directamente, como hemos hecho hasta ahora, pero necesario cuando trabajamos con un objeto Closure en lugar de con la propia definición del closure). A continuación, y ya fuera de la definición del método, definimos un closure y se lo pasamos al metodo recién definido. Por otro lado, cuando el parámetro que acepta el closure es el situado más a la derecha en la definición del método (como ocurre en nuestro caso) podemos pasar el closure de la siguiente manera:
repetirClosure(5) { println it }
Esta última sintaxis la veremos de forma intensiva en el capitulo 3 cuando trabajemos con Rangos y Colecciones.
Los closures permiten hacer cosas muchisimo más complejas (y por supuesto utiles) que las aquí mostradas, pero todo lo que necesitamos saber para continuar con el tutorial se encuentra en este capítulo. Es importante que escribas tus propios closures para ganar confianza con esta potentisima herramienta del lenguaje, y que busques información adicional a la aquí descrita si quieres explorar/explotar todas sus caracteristicas. En el próximo capítulo veremos que son los Rangos, y como Groovy ha simplificado el uso de Colecciones respecto a Java. Hasta entonces, ¡feliz currying!.
Con este entrega comienza una serie de artículos de introducción a Groovy, paso previo a la próxima publicación del tutorial de Grails. El tutorial estará formado por lo siguientes capítulos (podrían ser más en el futuro):
1. Introducción a Groovy, reglas y características básicas del lenguaje
2. Strings, GStrings y Closures
3. Rangos y Colecciones
4. POGO's
5. Metaprogramación
6. Usando el API (manejo de archivos, manejo de xml, groovelets)
Para poder seguir el tutorial es necesario tener instalado Groovy en tu equipo, misión que puedes llevar a cabo de forma sencilla siguiendo los pasos del siguiente anexo. Podrás acceder al resto de artículos de esta serie desde la pagina de tutoriales.
1.1 PERSPECTIVA GENERAL
Groovy es un lenguaje dinámico, orientado a objetos, muy intimamente ligado a Java. EL 99% del código Java existente puede ser compilado mediante groovy, y el 100% del código Groovy es convertido en bytecode Java, y ejecutado en tu JVM de manera natural. Groovy simp...
Con este entrega comienza una serie de artículos de introducción a Groovy, paso previo a la próxima publicación del tutorial de Grails. El tutorial estará formado por lo siguientes capítulos (podrían ser más en el futuro):
1. Introducción a Groovy, reglas y características básicas del lenguaje
2. Strings, GStrings y Closures
3. Rangos y Colecciones
4. POGO's
5. Metaprogramación
6. Usando el API (manejo de archivos, manejo de xml, groovelets)
Para poder seguir el tutorial es necesario tener instalado Groovy en tu equipo, misión que puedes llevar a cabo de forma sencilla siguiendo los pasos del siguiente anexo. Podrás acceder al resto de artículos de esta serie desde la pagina de tutoriales.
1.1 PERSPECTIVA GENERAL
Groovy es un lenguaje dinámico, orientado a objetos, muy intimamente ligado a Java. EL 99% del código Java existente puede ser compilado mediante groovy, y el 100% del código Groovy es convertido en bytecode Java, y ejecutado en tu JVM de manera natural. Groovy simplifica la sintaxis de Java hasta lo realmente necesario para expresar lo que queremos hacer, y ademas añade una serie de metodos tremendamente utiles al JDK, convirtiendo multitud de tareas en un placer.
1.2 SINTAXIS
La sintaxis de Groovy es una especie de código Java minimalista, eliminando la mayor parte del código "innecesario" (por innecesario nos referimos al código que no es estrictamente necesario para ejecutar la acción que deseamos). Esto, que a priori podria parecernos una dificultad, haciendonos pensar que tenemos que aprender un nuevo lenguaje, no lo es, pues como se ha mencionado en la sección 1.1 casi todo el código Java puede ser compilado con Groovy. De esta manera, podemos introducirnos en Groovy poco a poco, utilizando su sintaxis exclusiva mientras la vamos aprendiendo y la sintaxis de Java el resto del tiempo. Esto es válido en una misma clase o método, de manera que la curva de aprendizaje del lenguaje es muy suave. Veamos un ejemplo
println "¡Hola Mundo!"
El código de arriba podria considerarse el ejemplo más simple de Groovy. Esa unica linea muestra por pantalla el mensaje "¡Hola Mundo!". No necesitamos escribir una clase contenedora, ni un método public static void main(String[] args) que rodee el código (llamaremos a esto un script). No hay llamada a System.out, hemos ignorado los paréntesis en la llamada al método println así como el punto y coma al final de la linea. Aquí es donde estriba la sencillez de Groovy: reduciendo el código a lo estrictamente necesario. Si deseamos escribir los paréntesis, el punto y coma o ambos, podemos hacerlo: el código seguira funcionando.
1.3 SCRIPTS
Existen diversas maneras de ejecutar código Groovy. Una de ellas es mediante scripts. Puedes escribir el código de la sección 1.2 en un archivo con extensión .groovy (p.e. holamundo.groovy) y ejecutarlo desde linea de comandos así:
groovy holamundo.groovy
Esto compilará el archivo en bytecode Java y sera ejecutado "al vuelo" en memoria. Este archivo será una clase con un metodo main, dentro del cual se insertará el contenido del script que este fuera de cualquier metodo, más los métodos del script que se insertarán dentro de la propia clase.
1.4 GROOVY SHELL
Una segunda opción para ejecutar codigo Groovy es mediante groovy shell. Para lanzar el groovy shell escribimos en la linea de comandos:
groovysh
Hecho esto, el prompt cambiara y podremos escribir código directamente en él. Para obtener ayuda, podemos escribir lo suguiente una vez que estamos dentro del shell:
help
Una caracteristica interesante del shell es que mantiene un historial de todos los comandos que hemos introducido anteriormente, incluso aquellos de una ejecucíon anterior del shell. Para movernos por ese historial, podemos usar las flechas arriba-abajo del cursor de nuestro teclado.
Es necesario mencionar que el shell debe ser utilizado para ejecutar pequeños scripts, ya que algunas caracteristicas mas complejas del lenguage aún no están soportadas. Utilizalo como una manera rapida de probar un trozo de código que, tal vez, estas escribiendo dentro de tu IDE favorito.
1.5 GROOVY CONSOLE
Una opcion mas potente para ejecutar código Groovy es mediante la consola de Groovy. Esta consola funciona en modo grafico y permite opciones mas potentes que el shell, como guardar y cargar archivos, opciones de edición de texto, etc. Para lanzar la consola de Groovy ejecuta lo siguiente desde la linea de comandos:
groovyconsole
La consola no padece de las limitaciones del shell a la hora de ejecutar ciertas características complejas del lenguaje. Mi recomendación es que sigas todos los ejemplos restantes del tutorial desde la consola, a menos que se especifique lo contrario.
1.6 COMPILAR ARCHIVOS
Todo archivo .groovy, ya contenga una clase o un script, puede ser compilado en un archivo .class. Este archivo es una clase Java normal que, por tanto, puede ser cargada desde Java y Groovy como cualquier otra clase. Realicemos un sencillo ejemplo para comprobarlo. Compila el archivo que hemos escrito en la sección 1.3 de la siguiente manera:
groovyc holamundo.groovy
Si miramos en el sistema de archivos, veremos que junto al archivo 'holamundo.groovy' también aparece el recién creado archivo 'holamundo.class'. Ahora crea un script nuevo, en un fichero llamado 'ejecutor.groovy' y escribe dentro de él lo siguiente:
holamundo hm = new holamundo()
hm.main()
println "este es otro archivo"
Si ahora ejecutamos este archivo desde linea de comandos:
groovy ejecutor.groovy
Obtenemos la siguiente salida:
¡Hola Mundo!
este es otro archivo
Como puedes ver, hemos instanciado una clase que fué escrita y compilada en Groovy, resultando en una clase Java normal.
1.7 IMPORTACIONES AUTOMATICAS
Groovy importa por defecto varios paquetes y clases, de manera que pueden ser utilizados inmediatamente sin necesidad de escribir sus correspondientes sentencias import. Dichos paquetes y clases son:
1.8 PUNTO Y COMA OPCIONAL
Como ya hemos visto en los ejemplos anteriores, el caracter de punto y coma al final de cada linea es opcional. Solo debe ser usado al escribir varias sentencias en una única linea:
Los paréntesis si que son obligatorios cuando se llama a un método sin argumentos (ya que el compilador no podria saber si es un metodo o una variable lo que estamos llamando).
1.10 RETURN OPCIONAL
La sentencia return al final de un método también es opcional en Groovy. Si se omite, el resultado devuelto por su última linea será implícitamente su valor de retorno (esto también aplica a scripts, que como vimos en el punto 1.3, en tiempo de ejecución son convertidos en una clase que se ejecuta en memoria):
void cuadrado(int numero) {
println "devolviendo cuadrado de " + numero
numero*numero
}
El método anterior, cuando es ejecutado, escribe un mensaje por la salida estandar y devuelve el cuadrado del número pasado como argumento. La sentecia return solo es necesario cuando, por ejemplo, necesitemos devolver un valor dentro de un bucle o un bloque switch En caso de que la última sentencia de un método o script no devuelva ningún valor (por ejemplo una sentencia println o la llamada a un método void), se devolverá null.
1.11 DECLARACION DE TIPOS OPCIONAL
En Groovy no tenemos que declarar el tipo de una variable cuando le asignamos un valor. Esto es debido a la naturaleza dinamica de Groovy, en contra de Java que es un lenguaje fuertemente tipado (o estático).
x = "Esto es un String"
println x.class
x = 12
println x.class
La variable x, que no ha sido declarada previamente en el código, es inicializada con un String primero y con un entero después. Las sentencias println demuestran esto mostrando por pantalla el tipo actual de la variable después de cada asignación. Esta naturaleza dinámica es conocida como 'duck typing', un término que significaria algo como 'escritura de pato', y proviene de un dicho inglés que dice que si anda como un pato, come como un pato y hace ruido como un pato, seguramente es un pato. En nuestro caso, si una variable contiene un String y se comporta como tal, entonces es un String (aunque no hayamos definido su tipo).
El código anterior, que funciona bien en scripts, no puede ser usado dentro de una clase: o declaramos el tipo explicitamente (String o int) o declaramos la variable con def:
Mi recomendación es utilizar siempre def, incluso en scripts, puesto que así podemos determinar en que momento se crea la variable y en que momento se le esta reasignando un valor diferente.
1.12 MANEJO DE EXCEPCIONES
En Groovy todas las excepciones son de tipo no chequeadas, por lo que no tenemos que declararlas ni capturarlas. Esto nos da la liberad de manejarlas solo cuando creamos necesario hacerlo, en contra del sistema de manejo de excepciones en Java donde el compilador nos obliga a hacerlo siempre que un metodo declare lanzar una excepción de tipo chequeada. Veamos un ejemplo:
FileReader fr = new FileReader("actividad.log")
En el ejemplo anterior, si el archivo 'actividad.log' no existe se lanzará una excepción del tipo FileNotFoundException. Pero imaginemos que esa linea se llama desde una aplicación donde ese archivo siempre existe (porque se ha creado 5 lineas de código antes o porque al iniciar la aplicación se crea automáticamente), o que estamos llamando a un archivo del sistema operativo que, de igual manera, debe estar siempre presente en disco. En estos dos casos los bloques try-catch solo añaden "ruido" al código, pues son a todas luces innecesarios. Groovy nos permite no declarar ni capturar cualquier excepcion, sea del tipo que sea. Por supuesto que, cuando no podamos asegurar que una excepción no va a ser lanzada, deberemos gestionarla correctamente. Dejame recordarte que, sobre todo en una aplicación seria, NUNCA debes capturar una excepción en un bloque catch vacio, pues tu aplicación podria estar funcionando mal y no ser consciente de ello.
1.13 SOBRECARGA DE OPERADORES
Groovy permite la sobrecarga de operadores, de manera que podemos definir un método (dentro de una de nuestras clases) que será llamado al usar cierto operador. La manera mas sencilla de entenderlo es, como siempre, viendo un ejemplo:
class MiInteger {
private int valor = 0;
public MiInteger(int valor) {
this.valor = valor
}
public int plus(MiInteger otro) {
valor + otro.valor
}
}
def mc1 = new MiInteger(8)
def mc2 = new MiInteger(7)
println mc1 + mc2
El ejemplo anterior es una clase wrapper simplificada de Integer. En su interior hay un método llamado plus() el cual sobrecarga el operador + (caracter más). Debajo de la definición de la clase hemos instanciado dos objetos de dicha clase y los hemos "sumado" con el citado operador +. El resultado ha sido la llamada al método plus(), el cual ha devuelto la suma de los valores contenidos en ambas instancias (recuerda que la última linea de un método es implícitamente una sentencia return. Puedes encontrar una lista con todos los operadores que pueden ser sobrecargados y sus métodos correspondientes aquí.
1.14 REFERENCIACIÓN SEGURA
El operador de referenciación segura (?) evita que se lance la temida excepción de tipo NullPointerException cuando llamamos a un método en un objeto con valor null. En su lugar, la llamada al método devolverá null:
MiInteger mi
mi?.plus(new MiInteger(10))
El código anterior declara una instancia de la clase MiInteger y llama a su método plus() usando el operador de referenciación segura (el caracter de interrogación) antes del punto. Al ejecutar el código, obtenemos que la llamada al método devuelve null en lugar de lanzar una excepción NullPointerException.
El operador de referenciación segura puede ser encadenado tantas veces se desee:
1.15 AUTOBOXING
En Groovy, TODO es un objeto. Incluso los tipos primitivos son tratados como objetos. Mira el siguiente código:
println println 2.floatValue()
El código anterior llama al método floatValue() que está presente en la clase Integer, a pesar de que 2 es un tipo primitivo. La razón de este comportamiento tan sorprendente como potente es que Groovy convierte (cuando es necesario) cualquier variable de tipo primitivo en su correspondiente clase wrapper.
1.16 DECIMALES
En Groovy, todas las expresiones numéricas con decimales son por defecto de tipo BigDecimal:
println 0.91.class
Si ejecutas el código anterior, verás que la expresión decimal 0.91 es una instancia de la clase BigDecimal. Este comportamiento del lenguaje esta destinado a evitar la inexactitud de las clases Float y Double. Aunque dichas clases siguen estando disponibles, evidentemente deben ser instanciadas explícitamente:
println new Double(0.91).class
1.17 VERDADERO Y FALSO
En Java, solamente true puede ser verdadero y solamente false puede ser falso. En Groovy el concepto de verdadero y falso ha sido extendido a multitud de situaciones más, siempre con la intención de simplificar el código y hacerlo tremendamente expresivo. Siempre que se evalue un valor cero, null, un String vacío, una coleccion vacía, un array de longitud cero o un StringBuilder/StringBuffer vacío, se obtendrá false. En cualquier otra situación, se obtendrá true. Esto nos permite, por ejemplo, lo siguiente:
def lista = []
// ...
if(lista) {
println "ArrayList con elementos"
} else {
println "ArrayList vacio"
}
La primera linea del código anterior declara un ArrayList vacío, y despues de varias lineas que no mostramos (que podrían añadir o eliminar objetos del ArrayList) se llama a una sentencia if pasandole como condición booleana la lista previamente creada. Si la lista contiene elementos, el bloque if será alcanzado; en caso contrario se ejecutará el bloque else.
En el próximo capítulo veremos como trabajar con cadenas de texto en Groovy, lo que nos permitirá introducir la clase GString (Groovy Strings) y los Heredocs. A continuación nos introduciremos en el concepto de Closure, una de las caracteristicas del lenguaje más potentes y totalmente necesaria para poder seguir con el resto del tutorial (ademas de elevar nuestro código hasta cotas inimaginables). Hasta entonces, ¡saludos!.
Hola a todos, desde mi última entrada en el blog (hace más de 3 meses) he estado muy liado con temas de trabajo, y la verdad es que tampoco tenia mucho material nuevo para mostrar. Sin embargo, en las últimas semanas he venido preparando un tutorial de introducción a Groovy, como siempre en varias entregas (como mínimo 6, más un anexo de puesta en marcha de un entorno de desarrollo, como ya se hizo con el curso de JPA) que precederá a un tutorial de introducción a Grails, un potentisimo framework para la creación de aplicacionés web. En unos dias comenzaré a publicar todo este material.
Al margen de esto, las visitas a la web se han mantenido constantes semana tras semana, superando las 30.000 a fecha de hoy. No dejo de sorprenderme por el enorme interés que habéis mostrado por la página, y de nuevo (y no me cansaré nunca) os doy las gracias. Espero que los próximos contenidos que publique os resulten tan útiles e interesantes como los anteriores.
Por último, y a petición de algunos de vosotros, es muy probable que en las próximas semanas implemente un sistema de feeds RSS, de maner...
Hola a todos, desde mi última entrada en el blog (hace más de 3 meses) he estado muy liado con temas de trabajo, y la verdad es que tampoco tenia mucho material nuevo para mostrar. Sin embargo, en las últimas semanas he venido preparando un tutorial de introducción a Groovy, como siempre en varias entregas (como mínimo 6, más un anexo de puesta en marcha de un entorno de desarrollo, como ya se hizo con el curso de JPA) que precederá a un tutorial de introducción a Grails, un potentisimo framework para la creación de aplicacionés web. En unos dias comenzaré a publicar todo este material.
Al margen de esto, las visitas a la web se han mantenido constantes semana tras semana, superando las 30.000 a fecha de hoy. No dejo de sorprenderme por el enorme interés que habéis mostrado por la página, y de nuevo (y no me cansaré nunca) os doy las gracias. Espero que los próximos contenidos que publique os resulten tan útiles e interesantes como los anteriores.
Por último, y a petición de algunos de vosotros, es muy probable que en las próximas semanas implemente un sistema de feeds RSS, de manera que podáis seguir la actividad de la web de una manera más sencilla. ¡Hasta pronto!
Este fín de semana la web ha superado las 10.000 visitas desde su puesta en marcha, con casi 30.000 páginas vistas desde más de 30 paises. Estas cifras eran para mi un sueño cuando empecé a publicar contenidos hace seis semanas.
No puedo decir más que... ¡¡10.000 gracias a todos!!
Ya está disponible la traducción completa del tutorial básico de Hibernate. Espero que os resulte tan util como anteriores trabajos publicados en esta web. El código fuente para seguir el tutorial puedes encontrarlo en la última distribución estable de Hibernate, dentro del directorio 'project/tutorials/web'.
En esta último artículo del tutorial de introducción a JPA, vamos a presentar JPQL (Java Persistence Query Language - Lenguaje de Consula de Persitencia en Java), un potente lenguaje de consulta orientado a objetos que va incluido con JPA. Aunque no es imprescindible, es recomendable tener unos conocimientos mínimos de lenguaje SQL. Si no es tu caso, algunos conceptos de JPQL te pueden parecer extraños. Además, independientemente del uso de sistemas ORM como JPA, siempre es aconsejable saber realizar consultas en una base de datos mediante SQL nativo.
En el primer y segundos artículos, vimos como configurar nuestras entidades para el mapeo. En el tercer artículo, vimos la manera de realizar operaciones de persistencia a través de la interface EntityManager, además de como definir nuestros propios métodos callback y clases listener. Ahora es el momento de potenciar las capacidades de consulta de nuestra aplicación.
4.1 JPQL BÁSICO...
En esta último artículo del tutorial de introducción a JPA, vamos a presentar JPQL (Java Persistence Query Language - Lenguaje de Consula de Persitencia en Java), un potente lenguaje de consulta orientado a objetos que va incluido con JPA. Aunque no es imprescindible, es recomendable tener unos conocimientos mínimos de lenguaje SQL. Si no es tu caso, algunos conceptos de JPQL te pueden parecer extraños. Además, independientemente del uso de sistemas ORM como JPA, siempre es aconsejable saber realizar consultas en una base de datos mediante SQL nativo.
En el primer y segundos artículos, vimos como configurar nuestras entidades para el mapeo. En el tercer artículo, vimos la manera de realizar operaciones de persistencia a través de la interface EntityManager, además de como definir nuestros propios métodos callback y clases listener. Ahora es el momento de potenciar las capacidades de consulta de nuestra aplicación.
4.1 JPQL BÁSICO
Como vimos en el artículo anterior, al usar la interface EntityManager estamos limitados a realizar consultas en la base de datos proporcionando la identidad de la entidad que deseamos obtener, y solo podemos obtener una entidad por cada consulta que realicemos. JPQL nos permite realizar consultas en base a multitud de criterios (como por ejemplo el valor de una propiedad, o condiciones booleanas), y obtener más de un objeto por consulta. Veamos el ejemplo de sintaxis JPQL más simple posible:
SELECT p FROM Pelicula p
La sentencia anterior obtiene todas las instancias de la clase Pelicula desde la base de datos. La expresión puede parecer un poco extraña la primera vez que se ve, pero es muy sencilla de entender. Las palabras SELECT y FROM tienen un significado similar a las sentencias homonimas del lenguaje SQL, indicando que se quiere seleccionar (SELECT) cierta información desde (FROM) cierto lugar. La segunda p es un alias para la clase Pelicula, y ese alias es usado por la primera p (llamada expresion) para acceder a la clase (tabla) a la que hace referencia el alias, o a sus propiedades (columnas). El siguiente ejemplo nos ayudará a comprender esto mejor:
SELECT p.titulo FROM Pelicula p
El ejemplo anterior es más sencillo de comprender, ¿verdad?. El alias p nos permite utilizar la expresión p.titulo para obtener los títulos de todas las películas almacenadas en la base de datos. La expresiones JPQL utilizan la notación de puntos, convirtiendo tediosas consultas en algo realmente simple:
SELECT c.propiedad.subPropiedad.subSubPropiedad FROM Clase c
JPQL también nos permite obtener resultados en base a más de una propiedad:
SELECT p.titulo, p.duracion FROM Pelicula p
Todas las sentencias anteriores (que más tarde veremos como ejecutar) devuelven, o un único valor, o un conjunto de ellos. Podemos eliminar los resultados duplicados mediante la clausula DISTINCT:
SELECT DISCTINCT p.titulo FROM Pelicula p
Así mismo, el resultado de una consulta puede ser el resultado de una función agregada aplicada a la expresión:
SELECT COUNT(p) FROM Pelicula p
COUNT() es una función agregada de JPQL, cuya misión es devolver el número de ocurrencias tras realizar una consulta. Por tanto, en el ejemplo anterior, el valor devuelto por la función agregada es el resultado de la sentencia al completo. Otras funciones agregadas son AVG para obtener la media aritmética, MAX para obtener el valor máximo, MIN para obtener el valor mínimo, y SUM para obtener la suma de todos los valores.
4.2 SENTENCIAS CONDICIONALES
Ahora que ya sabemos como realizar consultas básicas, vamos a introducir conceptos algo más complejos (pero aún simples). El primero de ellos es el de consulta condicional, el cual es aplicado añadiendo la clausula WHERE en nuestra sentencia JPQL. Mediante una consulta condicional, restringimos los resultados devueltos por una consulta, en base a ciertos criterios lógicos (desde ahora la mayoría de los ejemplos constarán de varias lineas, pero ten presente que todos ellos representan una única sentencia JPQL, no varias):
SELECT p FROM Pelicula p
WHERE p.duracion < 120
La sentencia anterior obtiene todas las instancias de Pelicula almacenadas en la base de datos con una duración inferior a 120 minutos. Esto es llevado a cabo gracias al operador de comparación <. Las sentencias condicionales pueden más de una condición:
SELECT p FROM Pelicula p
WHERE p.duracion < 120 AND p.genero = 'Terror'
La sentencia anterior obtiene todas las instancias de Pelicula con una duracion inferior a 120 minutos y cuya propiedad genero sea igual a Terror. Si en el caso anterior utilizamos un operador de comparación (<), en esta última sentencia hemos utilizado dos operadores de comparación (< y =), así como un operador lógico (AND). Los otros dos operadores lógicos disponibles en JPQL son OR y NOT. El primero de ellos, aplicado en el ejemplo anterior en lugar de AND, permitiría obtener todas peliculas con una duración inferior a 120 minutos, o las del género de Terror (solo una de las dos condiciones sería suficiente). El segundo de ellos, aplicado sobre un expresión, la niega:
SELECT p FROM Pelicula p
WHERE p.duracion < 120 AND NOT (p.genero = 'Terror')
En la sentencia anterior, se obtendrían todas las instancias de Pelicula con una duración menor a 120 minutos, y que no (NOT) son del género de terror. Veamos otro operador de comparación:
SELECT p FROM Pelicula p
WHERE p.duracion BETWEEN 90 AND 150
La sentencia anterior obtiene todas las instancias de Pelicula con una duraciÓn entre (BETWEEN) 90 y (AND) 150 minutos. BETWEEN puede ser convertido en NOT BETWEEN, en cuyo caso se obtendrían todas las películas que una duración que no (NOT) se encuentren dentro del margen (BETWEEN) 90-150 minutos. Otro operador de comparación muy útil es [NOT] LIKE (NOT es opcional, como en los ejemplos anteriores), el cual nos permite comparar una cadena de texto completa o solo definida en parte (esto último gracias al uso de comodines) con los valores de una propiedad almacenada en la base de datos. Veamos un ejemplo para comprenderlo mejor:
SELECT p FROM Pelicula p
WHERE p.titulo LIKE 'El%'
La sentencia anterior obtiene todas las instancias de Pelicula cuyo título sea como (LIKE) El% (el simbolo de porcentaje es un comodín que indica que en su lugar pueden haber entre cero y más caracteres). Resultados devueltos por esta consulta incluirían películas con un título como El Caballero Oscuro, El Violinista en el Tejado, o si existe, El. El otro comodín aceptado por LIKE es el caracter de barra baja (_), el cual representa un único caracter indefinido (ni cero caracteres ni más de uno; uno y solo uno). JPQL dispone de muchos operadores tanto de comparación como loógicos, y todavía más combinaciones posibles entre ellos, motivo por el cual no vamos a entrar en más detalles sobre el tema; puedes consultar la referencia completa de JPQL (en inglés) aquí.
4.3 PARÁMETROS DINÁMICOS
Podemos añadir parámetros dinámicamente a nuestras sentencias JPQL de dos formas: por posición y por nombre. La sentencia siguiente acepta un parámetro por posicion (?1):
SELECT p FROM Pelicula p
WHERE p.titulo = ?1
Y la siguiente, acepta un parámetro por nombre (:titulo):
SELECT p FROM Pelicula p
WHERE p.titulo = :titulo
En el momento de realizar la consulta, deberemos pasar los valores con los que queremos que sean sustituidos los parámetros dinámicos que hemos definido. Veremos como realizar esta operación en la sección 4.8.
4.4 ORDENAR LOS RESULTADOS
Cuando realizamos una consulta en la base de datos, podemos ordenar los resultados devueltos mediante la clausula ORDER BY (ordenar por), la cual admite ordenamiento ascendente (mediante la claúsula ASC, comportamiento por defecto si omitimos el tipo de ordenamiento), o en orden descendiente (mediante la claúsula DESC):
SELECT p FROM Pelicula p
ORDER BY p.duracion DESC
La sentencia anterior podría tener una clausula WHERE como las vistas en ejemplos anteriores entre SELECT y ORDER BY, para restringir los resultados devueltos. Además, puedes incluir múltiples expresiones de ordenación en la misma sentencia:
SELECT p FROM Pelicula p
WHERE p.genero = 'Comedia'
ORDER BY p.duracion DESC, p.titulo ASC
En la sentencia anterior, hemos filtrado la selección de películas a las del género de comedia (mediante la clausula WHERE), y hemos ordenado los resultados en base a dos criterios: por duración (DESC indica de mayor a menor duración en minutos), y entre las que tienen la misma duración, por título (ASC, que ya hemos dicho que es redundante por ser el comportamiento por defecto, indica de la A a la Z).
4.5 OPERACIONES DE ACTUALIZACIÓN
JPQL puede realizar operaciones de actualización en la base de datos mediante la sentencia UPDATE:
UPDATE Articulo a
SET a.descuento = 15
WHERE a.precio > 50
La sentencia anterior actualiza (UPDATE) todas las instancias de Articulo presentes en la base de datos cuyo precio (WHERE) sea mayor de 50, aplicándoles (SET) un descuento de 15.
4.6 OPERACIONES DE BORRADO
De forma muy similar a lo visto en la seccion anterior, JPQL puede realizar operaciones de borrado en la base de datos mediante la sentencia DELETE:
DELETE FROM Pelicula p
WHERE p.duracion > 190
La sentencia anterior elimina (DELETE) todas las instancias de Pelicula cuya duración sea mayor de 190 minutos. Ni que decir tiene que las sentencías UPDATE y DELETE deben ser usadas con cierta precaución, sobre todo cuando trabajamos con información que se encuentra en producción.
4.7 EJECUCION DE SENTENCIAS JPQL
El lenguaje JPQL es integrado a través de implementaciones de la interface Query. Dichas implementaciones se obtienen a través de nuestro querido amigo EntityManager, mediante diversos métodos de factoría. De estos, los tres más usados (y los únicos que explicaremos aquí) son:
public static void main(String[] args) {
EntityManagerFactory emf = Persistence
.createEntityManagerFactory("introduccionJPA");
EntityManager em = emf.createEntityManager();
String jpql = "SELECT p FROM Pelicula p";
Query query = em.createQuery(jpql);
List<Pelicula> resultados = query.getResultList();
for(Pelicula p : resultados) {
// ...
}
em.close();
emf.close();
}
}
En el ejemplo anterior, obtenemos una implementación de Query mediante el método createQuery(String) de EntityManager, al cual le pasamos una sentencia JPQL en forma de cadena de texto. Con el objeto Query ya inicializado, podemos realizar la consulta a la base de datos llamando a su método getResultList(), el cual devuelve un objeto List con todas las entidades devueltas por la sentencia JPQL. Esta sentencia es una sentencia dinámica, ya que es generada cada vez que se ejecuta. De manera adicional, el ejemplo nos muestra que al usar una colección parametizada (List<Pelicula>) nos evitamos tener que hacer ningun tipo de casting al manejar las entidades (al fin y al cabo JPA está devolviendo entidades de clases concretas, así que podemos aprovechar esta circunstancia usando colecciones genéricas).
4.8 EJECUCIÓN DE SENTENCIAS CON PARÁMETROS DINÁMICOS
En el punto 4.3 vimos como escribir sentencias JPQL con parámetros dinámicos. Ahora que hemos visto como funciona la interface Query, estamos listos para usar dichos parámetros dinámicos (y entender su utilidad):
String jpql = "SELECT p FROM Pelicula p WHERE p.duracion > ?1 AND p.genero = ?2"
Query query = em.createQuery(jpql);
query.setParameter(1, 180);
query.setParameter(2, "Accion");
List<Pelicula> resultados = query.getResultList();
En el ejemplo anterior, hemos insertado dinámicamente (mediante el método setParameter() los valores deseados para las expresiones p.duracion y p.genero, que en la sentencia JPQL original se corresponden con los parámetros por posicion ?1 y ?2, respectivamente. El primer argumento que pasamos a setParameter() indica que parámetro por posicion deseamos sustituir por el valor del segundo argumento. Si el valor que pasamos como segundo argumento no se corresponde con el valor esperado (por ejemplo, al pasar un una cadena de texto donde se espera un valor numérico), la aplicación lanzará una excepción de tipo IllegalArgumentException. Esto también ocurrira si intentamos dar un valor a un parámetro dinámico inexistente (como query.setParameter(3, "Valor") en nuestro ejemplo anterior).
El otro tipo de parámetro dinámico (por nombre) es tan fácil de aplicar como su versión numérica:
String jpql = "SELECT p FROM Pelicula p WHERE p.duracion > :duracion AND p.genero = :genero"
Query query = em.createQuery(jpql);
query.setParameter("duracion", 180);
query.setParameter("genero", "Accion");
List<Pelicula> resultados = query.getResultList();
En el ejemplo anterior, en lugar de utilizar ?1 y ?2 en la sentencia JPQL, hemos utilizado :duracion y :genero como parámetros dinámicos. Para poder darle valor a estos parámetros en el momento de realizar la consulta, setParameter() provee una versión cuyo primer argumento acepta un valor de tipo String con el que poder identificar el parámetro dinamico (query.setParameter("duracion", 180)). Que versión usar depende de preferencias personales, pues ambos cumplen exactamente la misma misión; sin embargo, es evidente que los parámetros dinámicos por nombre son más faciles de identificar, entender, mantener, y un largo etcetera de ventajas.
4.9 CONSULTAS CON NOMBRE (ESTATICAS)
Las consultas con nombre son diferentes de las sentencias diáamicas que hemos visto hasta ahora en el sentido de que una vez definidas, no pueden ser modificadas: son leidas y transformadas en sentencias SQL cuando el programa arranca por primera vez, en lugar de cada vez que son ejecutadas. Este comportamiento estático las hace más eficientes, y por tanto ofrecen un mejor rendimiento. Las consultas con nombre son definidas mediante metadatos (recuerda que los metadatos se definen mediante anotaciones o configuración XML), como puedes ver en este ejemplo:
@Entity
@NamedQuery(name="buscarTodos", query="SELECT p FROM Pelicula p")
public class Pelicula { ... }
El ejemplo anterior define una consulta con nombre a través de la anotación @NamedQuery. Esta anotación necesita dos atributos: name (que define el nombre de la consulta), y query (que define la sentencia JPQL a ejecutar). El nombre de la consulta debe ser único dentro de su unidad de persistencia, y por tanto no pueden existir dos entidades dentro de la citada unidad de persistencia que definan consultas estáticas con el mismo nombre. Para evitar que podamos modificar por error la sentencia, es una buena idea utilizar una constante definida dentro de la propia entidad, y usarla como nombre de la consulta:
@Entity
@NamedQuery(name=Pelicula.BUSCAR_TODOS, query="SELECT p FROM Pelicula p")
public class Pelicula {
public static final String BUSCAR_TODOS = "Pelicula.buscarTodos";
...
}
De manera adicional, esto nos permite crear una consulta con el mismo nombre en múltiples entidades (como BUSCAR_TODOS), pues ahora el nombre de la entidad se encuentra incluido en el nombre de la consulta, y por tanto seguimos sin violar la regla de nombres únicos para todas las consultas dentro de la misma unidad de persistencia. Consultas similares con nombres similares en entidades distintas harán nuestro código más fácil de escribir y mantener.
Una vez definida la consulta con nombre, podemos crear el objeto Query necesario mediante el segundo método de la lista que vimos en la sección 4.7: createNamedQuery():
createNamedQuery() requiere un parámetro de tipo String que contenga el nombre de la consulta (el cual hemos definido a través del parámetro name de @NamedQuery). Una vez creado el objeto Query, podemos trabajar con él de la manera habitual para obtener los resultados.
4.10 CONSULTAS NATIVAS SQL
El tercer y último tipo de consultas que nos queda por ver requiere una sentencia SQL nativa en lugar de una sentencia JPQL:
Las consultas SQL nativas pueden ser definidas de manera estática como hicimos con las consultas con nombre, obteniendo los mismos beneficios de eficiencia y rendimiento. Para ello, necesitamos utilizar, de nuevo, metadatos:
@Entity
@NamedNativeQuery(name=Pelicula.BUSCAR_TODOS, query="SELECT * FROM PELICULA")
public class Pelicula {
public static final String BUSCAR_TODOS = "Pelicula.buscarTodos";
...
}
Tanto las consultas con nombre como las consultas nativas SQL estáticas son muy útiles para definir consultas inmutables (como buscar todas las instancias de una entidad en la base de datos). Las candidatas son aquellas consultas que se mantienen inmutables entre distintas ejecuciones del programa (sin parámetros dinámicos), y que son usadas frecuentemente.
4.11 FINAL
A lo largo de este tutorial de 4 artículos hemos visto de forma introductoria y superficial gran parte de lo que nos ofrece JPA en su versión 2.0. Las características y posibilidades de esta API van mucho, mucho más lejos de lo que aquí he intentado exponer, así que ahora que (espero) ya te sientes óomodo con la especificación, puedes lanzarte de lleno y llevarla aún más alla.
Por último, gracias a todos los que seguís este blog de programación, y a los que con vuestros correos electrónicos me animaís a seguir publicando contenidos. No dudéis en enviarme todo el feedback que consideréis oportuno, así como cualquier notificación de erratas, ideas para futuros documentos, etc.
Esta misma noche se ha liberado la version 3.5 Candidate Release 2 de Hibernate, que parece ser la ultima CR antes de la liberacion del producto final. La sorpresa (por otro lado muy esperada) es que viene con el 99% de la documentacion en español traducida (si no me equivoco gracias al tremendo esfuerzo de Angela Garcia, de RedHat), de manera que la traduccion que yo mismo comence hace unas pocas semanas ya no tiene ningun sentido. Solo publicare el material referente al tutorial del capitulo 1, el cual puedes encontrar aqui.
Podeis descargar Hibernate 3.5-CR2 desde aqui y dentro del archivo comprimido encontrareis la documentacion en español en el directorio 'documentation\manual\es-ES\'.
Actualmente uno de los libros que estoy leyendo es 'Alfresco 3 Enterprise Content Management Implementation', escrito por Munwar Shariff y publicado por Packt Publishing. El libro (muy bueno por cierto) contiene la llamada desde linea de comandos a la JVM mas larga que mis ojos hayan podido ver jamas:
Actualmente uno de los libros que estoy leyendo es 'Alfresco 3 Enterprise Content Management Implementation', escrito por Munwar Shariff y publicado por Packt Publishing. El libro (muy bueno por cierto) contiene la llamada desde linea de comandos a la JVM mas larga que mis ojos hayan podido ver jamas:
El comando de marras no es que tenga ningun misterio, su longitud es solo debida a que el 83.47% de su longitud (me he tomado la molestia de calcularlo) es un tremendo classpath. Mi tambien tremendo sentido del humor me ha obligado a publicarlo en el blog. Espero que la proxima vez que tengais que pelearos con un (desde ahora) "pequeño" classpath de 5-6 rutas distintas os sintais mejor y con mas fuerzas. ¡Que no se diga!
Despues de traducir a español el tutorial Spring MVC paso a paso muchos me habeis pedido la traduccion de documentacion relacionada con Hibernate. La documentacion oficial de Hibernate es tan completa como extensa, por lo que descarte la idea en un principio. Sin embargo, y gracias a vuestro apoyo y el gran interes que mostrais por Hibernate, he decidido comenzar la traduccion del manual de referencia Hibernate Reference Documentation, que podeis encontrar en su version original en ingles aqui. Este manual comienza con un pequeño tutorial construido con Hibernate y Maven, continua con un completo analisis de mapeo relacional con Hibernate en su version 3.3.2 y finaliza con diversos ejemplos reales de aplicaciones Hibernate.
Como en ocasiones anteriores, el formato sera HTML y el diseño el mismo que la version original. Completar la traduccion del tutorial (que consta de 25 secciones) va a ser una empresa muy grande, que espero tener terminada en un plazo...
Despues de traducir a español el tutorial Spring MVC paso a paso muchos me habeis pedido la traduccion de documentacion relacionada con Hibernate. La documentacion oficial de Hibernate es tan completa como extensa, por lo que descarte la idea en un principio. Sin embargo, y gracias a vuestro apoyo y el gran interes que mostrais por Hibernate, he decidido comenzar la traduccion del manual de referencia Hibernate Reference Documentation, que podeis encontrar en su version original en ingles aqui. Este manual comienza con un pequeño tutorial construido con Hibernate y Maven, continua con un completo analisis de mapeo relacional con Hibernate en su version 3.3.2 y finaliza con diversos ejemplos reales de aplicaciones Hibernate.
Como en ocasiones anteriores, el formato sera HTML y el diseño el mismo que la version original. Completar la traduccion del tutorial (que consta de 25 secciones) va a ser una empresa muy grande, que espero tener terminada en un plazo de 12 semanas (Abril de 2010). A medida que disponga de secciones traducidas las ire publicando, de manera que podais seguir el tutorial sin esperar a su completa traduccion.
Espero que este material os resulte de extrema utilidad y, de nuevo, gracias a todos por vuestro apoyo.
ACTUALIZACION (25/Feb/2010):
Hibernate 3.5-CR2 ha sido liberado (mas informacion aqui y aqui) con el 99% de la traduccion a español completada, por lo detengo este proyecto.
En esta tercer artículo del tutorial de introducción a JPA, vamos a ver como realizar persistencia, lo que nos permitirá entender mejor que son los métodos callback y las clases listener. También introduciremos el concepto de transacción.
Hasta ahora, todo el material que hemos visto en tanto en el primer como en el segundo artículo, se ha limitado a teoría sobre la forma de declarar y configurar nuestras entidades para su mapeo. En esta parte sin embargo, además de los conceptos teóricos que vamos a analizar, vamos a tener la oportunidad de realizar dicho mapeo con una base de datos real, y completar así el concepto de mapeo relacional de objetos (ORM). Por esto, es muy aconsejable que dispongas de un entorno de desarrollo compatible con JPA. Si no es tu caso, puedes preparar uno de manera sencilla accediendo al anexo que acompaña a este tutorial. Los archivos de configuración que veremos en este capítulo están adaptados a la configuración de...
En esta tercer artículo del tutorial de introducción a JPA, vamos a ver como realizar persistencia, lo que nos permitirá entender mejor que son los métodos callback y las clases listener. También introduciremos el concepto de transacción.
Hasta ahora, todo el material que hemos visto en tanto en el primer como en el segundo artículo, se ha limitado a teoría sobre la forma de declarar y configurar nuestras entidades para su mapeo. En esta parte sin embargo, además de los conceptos teóricos que vamos a analizar, vamos a tener la oportunidad de realizar dicho mapeo con una base de datos real, y completar así el concepto de mapeo relacional de objetos (ORM). Por esto, es muy aconsejable que dispongas de un entorno de desarrollo compatible con JPA. Si no es tu caso, puedes preparar uno de manera sencilla accediendo al anexo que acompaña a este tutorial. Los archivos de configuración que veremos en este capítulo están adaptados a la configuración de este entorno en concreto, por lo que si vas a utilizar uno distinto (otra base de datos, otro proveedor de persistencia, etc), deberas modificar dichos archivos con la información correspondiente a tu entorno. Comencemos.
3.1 PERSISTENCIA
El concepto de persistencia implica el hecho de almacenar nuestras entidades (objetos Java de tipo POJO) en un sistema de almacenamiento, normalmente una base de datos relacional (tablas, filas, y columnas). Más alla del proceso de almacenar entidades en una base de datos, todo sistema de persistencia debe permitir recuperar, actualizar y eliminar dichas entidades. Estas cuatro operaciones se conocen como operaciones CRUD (Create, Read, Update, Delete - Crear, Leer, Actualizar, Borrar). JPA maneja todas las operaciones CRUD a través de la interface EntityManager. Comencemos definiendo una entidad que manejaremos a través de los futuros ejemplos:
@Entity
public class Pelicula {
@Id
@GeneratedValue
private Long id;
private String titulo;
private int duracion;
// Getters y Setters
}
Para que JPA pueda persistir esta entidad, necesita un archivo de configuración XML llamado 'persistence.xml', el cual debe estar ubicado en el directorio META-INF de nuestra aplicación. En nuestro caso, este archivo mostraría este aspecto:
El archivo persistence.xml contiene información necesaria para JPA, como el nombre de la unidad de persistencia, el tipo de transacciones que vamos a utilizar (concepto que veremos a continuación), las clases que deseamos que sean manejadas por el proveedor de persistencia, y los parámetros para conectar con nuestra base de datos.
3.2 TRANSACCIONES
En el archivo de configuración anterior hemos incluido la siguiente linea:
En ella se indica el nombre de la unidad de persistencia (el cual pasaremos más adelante a EntityManager para informarle de los parámetros de mapeo de un conjunto de entidades a gestionar, en nuestro caso solo una), y el tipo de transacción (RESOURCE_LOCAL) que utilizaremos al manejar este conjunto de entidades. El concepto de transacción representa un contexto de ejecución dentro del cual podemos realizar varias operaciones como si fuera una sola, de manera que o todas ellas son realizadas satisfactoriamente, o el proceso se aborta en su totalidad (cualquer operación ya realizada se revertirá si ocurre un error a lo largo de la transacción). Esto es muy importante para garantizar la integridad de los datos que queremos persistir: imaginemos que estamos persistiendo una entidad que contiene una lista de entidades en su interior (una asociación uno-a-muchos). Si cualquiera de las entidades de esta lista no pudiera ser persistida por algún motivo, no deseamos que el resto de entidades de la lista, así como la entidad que las contiene, sean persistidas tampoco. Si permitieramos que esto ocurriera, nuestros datos en la base de datos no reflejarían su estado real como objetos, y ni dichos datos ni nuestra aplicación serían fiables. JPA nos permite configurar en cada unidad de persistencia el tipo de transacción que queremos usar, que puede ser:
- Manejada por la aplicacion (RESOURCE_LOCAL)
- Manejada por el contenedor (JTA)
Nosotros hemos decidido manejar las transacciones desde la aplicación, y por tanto lo haremos desde nuestro código a través de la interface EntityTransaction. Aunque en una aplicación real utilizaríamos transacciones manejadas por el contenedor, en nuestros ejemplos utilizaremos transacciones manejadas por la aplicación. De esta manera, mantendremos los ejemplos simples (no necesitamos instalar y configurar un contenedor), además de alcanzar una comprensióm mayor sobre el funcionamiento de las transacciones.
3.3 ESTADO DE UNA ENTIDAD
Para JPA, una entidad puede estar en uno de los dos estados siguientes:
- Managed (gestionada)
- Detached (separada)
Cuando persistimos una entidad (como veremos en el punto 3.4), automáticamente se convierte en una entidad gestionada. Todos los cambios que efectuemos sobre ella dentro del contexto de una transacción se verán reflejados también en la base de datos, de forma transparente para la aplicación.
El segundo estado en el que puede estar una entidad es separado, en el cual los cambios realizados en la entidad no están sincronizados con la base de datos. Una entidad se encuentra en estado separado antes de ser persistida por primera vez, y cuando tras haber estado gestionada es separada de su contexto de persistencia (como veremos en la sección 3.6).
3.4 PERSISTIR UNA ENTIDAD
Ya estamos listos para persistir nuestra primera entidad.:
En el ejemplo anterior hemos obtenido una instancia de EntityManager a través de la la clase factoría EntityManagerFactory a la que le hemos pasado como argumento el nombre de la unidad de persistencia que vamos a utilizar (y que hemos configurado previamente en el archivo persistence.xml). A continuación hemos obtenido una transacción desde EntityManager, y hemos creado una instancia de la entidad Pelicula. En las lineas de codigo siguientes, hemos iniciado la transacción, persistido la entidad, y confirmado la transaccion mediante el método commit() de EntityManager. En caso de que el proveedor de persistencia lance una excepción, la capturamos en el bloque catch, donde cancelamos la transacción mediante el metodo rollback() de EntityManager (en los próximos ejemplos, y para simplificar el código, vamos a eliminar el bloque try/catch, incluyendo el código que cancela la transacción en caso de error). Cuando llamamos a em.persist(pelicula) la entidad es persistida en nuestra base de datos, pelicula y queda gestionada por el proveedor de persistencia mientras dure la transación. Por ello, cualquier cambio en su estado será sincronizado automáticamente y de forma transparente para la aplicación:
En el ejemplo anterior hemos modificado el estado de la entidad pelicula después de haber sido persistida, pero dentro de la misma transacción que realizo la persistencia, y por tanto la nueva información se sincronizará con la base de datos. Es importante tener presente que esta sincronización puede no ocurrir hasta que instamos a la transacción para completarse (tx.commit()) (el momento en que la sincronización se lleve a cabo depende enteramente de la implementación concreta del proveedor de persistencia que usemos). Sin embargo, podemos forzar a que cualquier cambio pendiente se guarde en la base de datos antes de terminar la transacción invocando el método flush() de EntityManager:
tx.begin();
em.persist(pelicula);
pelicula.setTitulo("otro titulo");
em.flush();
// otras operaciones
tx.commit();
En el ejemplo anterior, los cambios efectuados sobre la entidad película (setTitulo()) serán persistidos, si no lo estuvieran ya, antes de finalizar la transacción. Por supuesto, si esta terminase fallando (por ejemplo durante el bloque // otras operaciones), todos los cambios efectuados durante la transacción, incluida la persistencia inicial de la entidad, serían revertidos (recuerda el concepto de transacción: todo o nada).
Así mismo, podemos realizar la sincronización en sentido inverso, actualizando una entidad con los datos que actualmente se encuentran en la base de datos. Esto es útil cuando persistimos una entidad, y tras haber cambiado su estado deseamos recuperar el estado persistido (desechando así los últimos cambios realizados sobre la entidad, que como sabemos podrían haber sido sincronizados con la base de datos). Esto podemos llevarlo a cabo mediante el método refresh() de nuestro gran amigo EntityManager:
En el ejemplo anterior, la llamada a em.refresh() deshace los cambios realizados en pelicula en la linea anterior. Otro metodo muy útil en EntityManager es contains():
boolean gestionada = em.contains(pelicula);
// lógica de la aplicacion
El método contains() devuelve true si el objeto Java que le pasamos como parámetro se encuentra en estado gestionado por el proveedor de persistencia, y false en caso contrario.
3.5 LEER UNA ENTIDAD
Ahora que ya sabemos como persistir una entidad en nuestra base de datos, demos un paso más y veamos como realizar el proceso inverso: leer una entidad previamente persistida en la base de datos para construir un objeto Java. Podemos llevar a cabo esto de dos maneras distintas:
- Obteniendo un objeto real
- Obteniendo una referencia a los datos persistidos
De la primera manera, los datos son leidos (por ejemplo, con el método find()) desde la base de datos y almacenados en una instancia de la entidad:
Pelicula p = em.find(Pelicula.class, id);
La segunda manera de leer una entidad nos permite obtener una referencia a los datos almacenados en la base de datos, de manera que el estado de la entidad será leido de forma demorada (en el primer acceso a cada propiedad), no en el momento de la creacion de la entidad:
Pelicula p = em.getReference(Pelicula.class, id);
En ambos casos, el valor de id debe ser el valor de id con el que fue persistida la entidad. Esta forma de lectura es muy limitada, de manera que luego veremos un sistema mucho mas dinamico para buscar entidades persistidas: JPQL.
3.6 ACTUALIZAR UNA ENTIDAD
Antes de explicar como actualizar una entidad, vamos a explicar como separar una entidad del contexto de persistencia, explicación que dejamos pendiente al final del punto 3.3. Podemos separar una o todas las entidades gestionadas actualmente por el contexto de persistencia mediante los métodos detach() y clear(), respectivamente. Una vez que una entidad se encuentra separada, esta deja de estar gestionada, y por tanto los cambios en su estado dejan de ser sincronizados con la base de datos. Si intentáramos llamar de nuevo al método persist() sobre una entidad separada, se lanzará una excepción (de hecho, la invocación de cualquier método que trabaje sobre entidades gestionadas lanzará una excepción cuando sea usado sobre entidades separadas).
La pregunta que se nos planteraría en este momento es: ¿cómo podemos volver a tener nuestra entidad gestionada y sincronizada?. Mediante el método merge() de EntityManager:
tx.begin();
em.persist(pelicula);
tx.commit();
em.detach(pelicula);
// otras operaciones
em.merge(pelicula);
En el ejemplo anterior, la entidad pelicula es separada del contexto de persistencia mediante la llamada al método detach(). La última linea, sin embargo, indica al proveedor de persistencia que vuelva a gestionar la entidad, y de manera adicional sincronice los cambios que se hayan podido realizar mientras la entidad estuvo separada (este pequeño milagro ocurre gracias al ID de la entidad).
3.7 ELIMINAR UNA ENTIDAD
La última operación CRUD que nos queda por ver es la de eliminar una entidad. Cuando realizamos esta operación, la entidad es eliminada de la base de datos y separada del contexto de persistencia. Sin embargo, la entidad seguira existiendo como objeto Java en nuestro código hasta que el ámbito de la variable termine, hasta que hipotéticamente sea puesta a null y el colector de basura elimine la instancia de memoria, etc:
em.remove(pelicula);
pelicula.setTitulo("ya no soy una entidad, solo un objeto Java normal");
Cuando existe una asociación uno-a-uno y uno-a-muchos entre dos entidades, y eliminas la entidad dueña de la relación, la/s entidad/es del otro lado de la relación no son eliminada/s de la base de datos (este es el comportamiento por defecto), pudiendo dejar así entidades persistidas sin ninguna entidad que haga referencia a ellas. Estas entidades se conocen como entidades huérfanas. Sin embargo, podemos configurar nuestras asociaciones para que eliminen de manera automática todas las entidades subordinadas de la relación:
@Entity
public class Pelicula {
...
@OneToOne(orphanRemoval = true)
private Descuento descuento;
// Getters y setters
}
En el ejemplo anterior, informamos al proveedor de persistencia que cuando elimine una entidad de tipo Pelicula, debe eliminar también de la base de datos la entidad Descuento asociada.
3.8 OPERACIONES EN CASCADA
La operación de eliminación de entidades huerfanas que acabamos de ver es un tipo de operación llamada en cascada. Este tipo de operaciones permiten establecer la forma en que deben propagarse ciertos eventos (como persistir, eliminar, actualizar, etc) entre entidades que forman parte de una asociación. La forma general de establecer como se realizarán estas operaciones en cascada se define mediante el atributo cascade de las anotaciones de asociación:
El ejemplo anterior es similar a su predecesor. En él, el tipo de operación en cascada que deseamos propagar (CascadeType.REMOVE) se indica mediante constantes de la clase CascadeType. Estas constantes pueden tener los siguientes valores:
La última constante, CascadeType.ALL, hace referencia a todas las posibles operaciones que pueden ser propagadas, y por tanto engloba en si misma el comportamiento del resto de constantes. También podemos configurar varias operaciones en cascada de la lista superior usando un array de constantes CascadeType:
Recuerda que cualquier entidad que utilices en tu código debe estar debidamente registrada en el archivo 'persistence.xml', de manera que JPA pueda gestionarla:
3.9 MÉTODOS CALLBACK
Como hemos visto a lo largo de este tercer artículo del tutorial de introducción a JPA, una entidad puede estar en dos estados diferentes: gestionada y separada. Los métodos callback son métodos que se ejecutan cuando se producen ciertos eventos relacionados con el ciclo de vida de una entidad. Estos eventos se clasifican en cuatro categorias:
- Eventos de persistencia (métodos callback asociados anotados con @PrePersists y @PostPersist)
- Eventos de actualizacion (métodos callback asociados anotados con @PreUpdate y @PostUpdate)
- Eventos de borrado (métodos callback asociados anotados con @PreRemove y @PostRemove)
- Eventos de carga (método callback asociado anotado con @PostLoad)
Las anotaciones superiores están destinadas a marcar métodos dentro de la entidad, los cuales serán ejecutados cuando el evento correspondiente suceda:
@Entity
public class Pelicula {
...
@PrePersist
@PreUpdate
private void validar() {
// validar parametros antes de persistir/actualizar la entidad
}
}
En el ejemplo anterior, hemos añadido a nuestra entidad un método que reaccionará antes dos eventos: el momento antes de realizarse la persistencia (@PrePersist), y el momento previo a la actualización (@PreUpdate). Dentro de él podemos realizar ciertas acciones necesarias antes de que se produzcan los dos citados eventos, como comprobar que el estado de la entidad (la información que contiene) es correcta, etc). Al escribir métodos callback, debemos seguir algunas reglas para que nuestro código funcione:
- Un método callback no puede ser declarado static ni final - Cada anotación de ciclo de vida puede aparecer una y solo una vez en cada entidad
- Un método callback no puede lanzar excepcione de tipo checked - Un método callback pueden invocar métodos de las clases EntityManager y/o Query
Ten presente que cuando existe herencia entre entidades, los metodos @Pre/PostXxx de las superclases son invocados antes que los de las subclases. Así mismo, los eventos de ciclo de vida se propagan en cascada cuando existen asociaciones.
3.10 CLASES LISTENER
Cuando necesitamos aplicar un mismo método callback a varias entidades, es preferible extraer de la entidad este método y ponerlo en una clase externa, la cual podrá ser aplicada a varias entidades (reduciendo así la duplicación de código). Estas clases son llamadas clases listener:
public class ValidadorListener {
@PrePersist
@PreUpdate
public void validar(Pelicula pelicula) {
// validar parametros antes de persistir/actualizar la entidad
}
}
En el ejemplo anterior hemos definido una clase listener, moviendo las anotaciones que representan a métodos callback en ella. En este momento, podemos aplicar la clase listener a nuestras entidades:
@Entity
@EntityListeners(ValidadorListener.class)
public class Pelicula { ... }
En tiempo de ejecución, cuando se produzca un evento que se encuentre definido en la clase listener, el proveedor de JPA pasará a su método validar() una referencia de la entidad Pelicula. Y si deseamos que una clase listener pueda ser aplicada a más de un tipo de entidad, podemos aprovechar la naturaleza polimórfica de Java, usando una superclase (o mejor aún, una interface) común a dichas entidades. En su defecto, siempre podemos usar un argumento de tipo Object en el método callback:
public class ValidadorListener {
@PrePersist
@PreUpdate
public void validar(Object obj) {
// hacer un casting y validar parametros antes de persistir/actualizar la entidad
}
}
De manera adicional, la anotación @EntityListeners puede hacer referencia a varias clases listener usando un array de objetos .class. Cuando una entidad es asociada con varias clases listener, los métodos callback de dichas clases que hagan referencia a un mismo evento serán invocados en el orden en que fueron declarados dentro del array. Así mismo, los métodos callback de las clases listener serán invocados de manera previa a los métodos callback que hagan referencia al mismo tipo de evento y que estén declarados dentro de la entidad:
@Entity
@EntityListeners({
ValidadorListener.class,
OtroListener.class})
public class Pelicula {
// ...
@PrePersist
@PreUpdate
private void validar() {
// validar parametros antes de persistir y actualizar la entidad
}
}
En el ejemplo anterior, los métodos @PrePersist y @PreUpdate se ejecutarán en este orden: ValidatorListener, OtroListener, Pelicula.
En el próximo y último artículo veremos que es y como usar JPQL, un potente lenguage orientado a objetos con el que podemos manejar entidades y grupos de entidades en base a multitud de criterios.
Gracias al esfuerzo de David Villacé, el tutorial Spring MVC Paso a Paso ahora esta tambien disponible en formato PDF. Podeis descargarlo desde aqui y desde el indice de la version HTML (Anexo C).
A peticion de algunos de vosotros, he subido a la web el proyecto completo para Eclipse del tutorial Spring MVC paso a paso. El proyecto incluye todos los archivos de codigo fuente, scripts y librerias necesarias. Podeis descargarlo desde aqui y desde el indice del tutorial (bajo el Anexo A). Es importante que leais el archivo LEEME.txt que encontrareis en el interior del archivo descargado.
Ademas he solucionado algunos errores menores de traduccion, parte del codigo fuente de la version original que era incorrecto o estaba incompleto y algunos links que todavia redirigian a la version inglesa.
En este segundo artículo del tutorial de introducción a JPA, vamos a ver dos conceptos relacionados con ORM que merecen un artículo propio: asociaciones y herencia.
2.1 ASOCIACIONES
En el artículo anterior, vimos como realizar ORM basico, y una de las opciones que JPA nos permite es la de mapear colecciones de objetos simples (ver seccion 1.8 COLECCIONES BÁSICAS). Sin embargo, cuando queremos mapear colecciones de entidades, debemos usar asociaciones. Estas asociaciones pueden ser de dos tipos:
Las asociaciones unidireccionales reflejan un objeto que tiene una referencia a otro objeto (la información puede viajar en una dirección). Por el contrario, las asociaciones bidireccionales reflejan dos objetos que mantienen referencias al objeto contrario (la información puede viajar en dos direcciones). Además del concepto de dirección, existe otro concepto llamado cardinalidad, que determina cuantos objetos pueden haber en cada extremo de ...
En este segundo artículo del tutorial de introducción a JPA, vamos a ver dos conceptos relacionados con ORM que merecen un artículo propio: asociaciones y herencia.
2.1 ASOCIACIONES
En el artículo anterior, vimos como realizar ORM basico, y una de las opciones que JPA nos permite es la de mapear colecciones de objetos simples (ver seccion 1.8 COLECCIONES BÁSICAS). Sin embargo, cuando queremos mapear colecciones de entidades, debemos usar asociaciones. Estas asociaciones pueden ser de dos tipos:
Las asociaciones unidireccionales reflejan un objeto que tiene una referencia a otro objeto (la información puede viajar en una dirección). Por el contrario, las asociaciones bidireccionales reflejan dos objetos que mantienen referencias al objeto contrario (la información puede viajar en dos direcciones). Además del concepto de dirección, existe otro concepto llamado cardinalidad, que determina cuantos objetos pueden haber en cada extremo de la asociación.
2.2 ASOCIACIONES UNIDIRECCIONALES
Para entender el concepto de unidireccionalidad, veamos un ejemplo de asociación unidireccional:
@Entity
public class Cliente {
@Id
@GeneratedValue
private Long id;
@OneToOne
private Direccion direccion;
// Getters y setters
}
@Entity
public class Direccion {
@Id
GeneratedValue
private Long id;
private String calle;
private String ciudad;
private String pais;
private Integer codigoPostal;
// Getters y setters
}
Como puedes ver en el ejemplo anterior, cada entidad Cliente manteniene una referencia a una entidad Direccion. Esta relación es de tipo uno-a-uno (one-to-one) unidireccional (puesto que una entidad Cliente contiene una referencia a una entidad Direccion, relación que ha sido declarada mediante la anotación @OneToOne. Cliente es el dueño de la relación de manera implícita, y por tanto cada entidad de este tipo contendrá por defecto una columna adicional en su tabla correspondiente de la base de datos (mediante la que podremos acceder al objeto Direccion). Esta columna es una clave foranea (foreign key), un concepto muy importante en bases de datos relaciones que abordaremos a lo largo de todo este artículo.
Cuando JPA realice el mapeo de esta relación, cada entidad será almacenada en su propia tabla, añadiendo a la tabla donde se almacenán los clientes (la dueña de la relación) una columna con las claves foraneas necesarias para asociar cada fila con la fila correspondiente en la tabla donde se almacenan las direcciones. Recuerda que JPA utiliza configuración por defecto para realizar el mapeo, pero podemos customizar este proceso definiendo el nombre de la columna que contendrá la clave foranea mediante la anotación @JoinColumn:
Otro tipo de asociación muy común es la de tipo uno-a-muchos (one-to-many) unidireccional. Veamos un ejemplo:
@Entity
public class Cliente {
@Id
@GeneratedValue
private Long id;
@OneToMany
private List direcciones;
// Getters y setters
}
En el ejemplo anterior, cada entidad de tipo Cliente mantiene una lista de direcciones a travás de la propiedad direcciones. Puesto que un objeto List puede albergar múltiples objetos en su interior, debemos anotarlo con @OneToMany. En este caso, en vez de utilizar una clave foranea en Cliente, JPA utilizará por defecto una tabla de unión (join table). Cuando ocurre esto, las tablas donde se almacenan ambas entidades contienen una clave foranea a una tercera tabla con dos columnas; esta tercera tabla es llamada tabla de unión, y es donde se estata la asociación entre las entidades relacionadas. Como siempre, podemos configurar el mapeo mediante metadatos (anotaciones/XML) para que se ajuste a nuestras necesidades:
2.3 ASOCIACIONES BIDIRECCIONALES
En las asociaciones bidireccionales, ambos extremos de la relación mantienen una referencia al extremo contrario. En este caso, el dueño de la relación debe ser especificado explícitamente, de manera que JPA pueda realizar el mapeo correctamente. Veamos un ejemplo de bidireccionalidad en una relación de tipo uno-a-uno:
@Entity
public class Mujer {
@Id
@GeneratedValue
private Long id;
@OneToOne
private Marido marido;
// Getters y setters
}
@Entity
public class Marido {
@Id
@GeneratedValue
private Long id;
@OneToOne(mappedBy = "marido")
private Mujer mujer;
}
En el ejemplo anterior (y como suele ocurrir en el mundo real...), Mujer es la dueña de la relación: puesto que la relación es bidireccional, ambos lados de la relación deben estar anotados con @OneToOne, pero ahora uno de ellos debe indicar de manera explícita que la parte contraria es dueña de la relacion. Esto lo hacemos añadiendo el atributo mappedBy en la anotación de asociación de la parte no-dueña. El valor de este atributo es el nombre de la propiedad asociada en la entidad que es dueña de la relación. El atributo mappedBy puede ser usado en relaciones de tipo @OneToOne, @OneToMany y @ManyToMany. ünicamente el cuarto tipo de relación, @ManyToOne, no permite el uso de este atributo.
2.4 LECTURA TEMPRANA Y LECTURA DEMORADA DE ASOCIACIONES
En el punto 1.5 vimos que significaban los conceptos de lectura temprana y lectura demorada. Las asociaciones son parte del mapeo relacional, y por tanto también son afectadas por este concepto. El tipo de lectura por defecto para las relaciones uno-a-uno y muchos-a-uno es temprana (eager). Por contra, el tipo de lectura para los dos tipos de relaciones restantes (uno-a-muchos y muchos-a-muchos), es demorada (lazy). Por supuesto, ambos comportamientos pueden ser modificados:
@OneToMany(fetch = FetchType.EAGER)
private List pedidos;
Al igual que se indicó en la sección 1.5, debes ser consciente del impacto en el rendimiento de la aplicación que puede causar una configuración erronea en el tipo de lectura. En el caso de las asociaciones, donde se pueden ver involucrados muchos objetos, leerlos de manera temprana puede ser, además de innecesario, inadecuado. En otros casos, una lectura temprana puede ser necesaria: si la entidad que es dueña de la relación se desconecta de gestor de persistencia (como veremos en el próximo artículo) sin haber inicializado aún una asociación con lectura demorada, al intentar acceder a dicha asociación se producirá una excepción de tipo LazyInitializationException (podemos encontrar una analogía a este comportamiento cuando tenemos un objeto sin inicializar (con valor null), que al no apuntar a ninguna instancia en memoria produce un error si es usado). Sea como sea, configura siempre los tipos de lectura de la forma más adecuada para tu aplicación.
2.5 ORDENACIÓN DE ASOCIACIONES
Puedes ordenar los resultados devueltos por una asociacion mediante la anotación @OrderBy:
@OneToMany
@OrderBy("nombrePropiedad asc")
private List pedidos;
El atributo de tipo String que hemos proporcionado a @OrderBy se compone de dos partes: el nombre de la propiedad sobre la que queremos que se realice la ordenación, y ,opcionalmente, el sentido en que se realizara, que puede ser:
- Ascendente (añadiendo asc al final del atributo)
- Descendente (añadiendo desc al final del atributo)
Así mismo, podemos mantener ordenada la colección a la que hace referencia una asociación en la propia tabla de la base de datos; para ello, debemos usar la anotación @OrderColumn. Sin embargo, el impacto en el rendimiento de la base de datos que puede producir este comportamiento es algo que debes tener muy presente, ya que las tablas afectadas tendrán que reordenarse cada vez que se hayan cambios en ellas (en tablas de cierto tamaño, o en aquellas donde se inserten o modifiquen registros con cierta frecuencia, es totalmente desaconsejable forzar una ordenación automática).
2.6 HERENCIA
En las aplicaciones Java, el concepto de herencia es usado de forma intensiva. Por supuesto, JPA nos permite gestionar la forma en que nuestros objetos son mapeados cuando en ellos interviene el concepto de herencia. Esto puede hacerse de maneras distintas:
- Una tabla por familia (comportamiento por defecto)
- Unión de subclases
- Una tabla por clase concreta
El mapeo por defecto es una tabla por familia. Una familia no es, ni más ni menos, que todas las subclases que están relacionadas por herencia con una clase madre, inclusive. Todas las clases que forman parde de una misma familia son almacenadas en una única tabla. En esta tabla existe una columna por cada atributo de cada clase y subclase de la familia, además de una columna adicional donde se almacena el tipo de clase al que hace referencia cada fila. Imaginemos el ejemplo siguiente:
@Entity
public class SuperClase {
@Id
@GeneratedValue
private Long id;
private int propiedadUno;
private String propiedadDos;
// Getters y Setters
}
@Entity
public class SubClase extends SuperClase {
@Id
@GeneratedValue
private Long id;
private float propiedadTres;
private float propiedadCuatro;
// Getters y setters
}
En el ejemplo anterior, tanto las instancias de las entidades SuperClase y SubClase seran almancenadas en una única tabla que tendrá el nombre por defecto de la clase raiz (SuperClase). Dentro de esta tabla habrá seis columnas que se corresponderán con:
- Una columna para la propiedad id (válida para ambas entidades, pues en ambas se mapearía a una columna con el mismo nombre)
- Cuatro columnas para las propiedades propiedadUno, propiedadDos, propiedadTres y propiedadCuatro - Una última columna discriminatoria, donde se almacenará el tipo de clase al que hace referencia cada fila
La columna discriminatoria suele contener el nombre de la clase. Esta columna es necesaria para que al recuperar un objeto desde la base de datos, JPA sepa que clase concreta debe instanciar, y que propiedades leer. Las propiedades que son propias de cada clase no deben ser configuradas como not null, ya que al intentar persistir un objeto de la misma familia pero de otra clase (y que tal vez no dispone de las mismas propiedades) obtendríamos un error. Aunque una tabla por familia es el comportamiento por defecto cuando mapeamos entidades en las que interviene la herencia entre clases, podemos declararlo explícitamente mediante el atributo SINGLE_TABLE (tabla única) de la anotación @Inheritance:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class SuperClass { ... }
Ten presente que los valores definidos en @Inheritance son heredados por todas las subclases de la entidad anotada, aunque estas pueden sobreescribir dichos valores si desean adoptar una política de mapeo diferente. En lo que refiere al nombre de la columna discriminatoria y a su tipo, estos son por defecto DTYPE y String (este último es un tipo SQL, evidentemente). Podemos cambiar ambos mediante las anotaciones @DiscriminatorColumn y @DiscriminatorValue:
@Entity
@Inheritance
@DiscriminatorColumn(name = "...", discriminatorType = CHAR)
@DiscriminatorValue("C")
public class SuperClase { ... }
La anotación @DiscriminatorColumn solo debe ser usada en la clase raiz de la herencia (a no ser que una subclase desee cambiar los parametros del mapeo, en cuyo caso ella se convertiría en clase raiz por derecho). El atributo discriminatorType de la citada anotación nos permite cambiar el tipo de valor que almacenará la columna discriminatoria. Los tipos soportados son STRING (por defecto), CHAR e INTEGER. Si cambiamos en la clase raiz el tipo de valor que almacenará la columna discriminatoria a otro distinto de STRING, cada subclase tendrá que indicar de forma explicita el valor que lo representará en la columna discriminatoria. Esto lo hacemos mediante la anotación DiscrimitadorValue:
<@Entity
@DiscriminatorValue("S")
public class SubClase extends SuperClase { ... }
En el ejemplo anterior, la columna discriminatoria (que es de tipo CHAR) contendrá un valor C si la instancia correspondiente es de tipo SuperClase, y una S si es de tipo SubClase. Por supuesto, no estaría permitido usar @DiscriminatorValue("C") si se ha definido una columna discriminatoria de tipo INTEGER, etc.
El segundo tipo de mapeo cuando existe herencia es unión de subclases, en el que cada clase y subclase (sea abstracta o concreta) será almacenada en su propia tabla:
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class SuperClase { ... }
La tabla raiz contiene una columna con una clave primaria usada por todas las tablas, así como la columna discriminatoria. Cada subclase almacenará en su propia tabla únicamente sus atributos propios (nunca los heredados), así como una clave foranea que hace referencia a la clave primaria de la tabla raiz. La mayor ventaja de este sistema es que es intuitivo. Por contra, su mayor inconveniente es que, para construir un objeto de una subclase, hay que hacer una (o varias) operaciones JOIN en la base de datos, de manera que se puedan unir los atributos de la subclase con los de sus superclases. Por tanto, una subclase que esté varios niveles por debajo de la superclase en la herencia, necesitará realizar múltiples operaciones JOIN (una por nivel), lo cual puede producir un impacto en el rendimiento que tal vez no deseemos.
El tercer y último tipo de mapeo cuando existe herencia es una tabla por clase concreta. Mediante este comportamiento, cada entidad será mapeada a su propia tabla (incluyendo todos los atributos propios y heredados). Con este sistema no hay tablas compartidas, columnas compartidas, ni columna discriminatoria.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class SuperClase { ... }
El único requerimiento al utilizar una tabla por clase concreta es que todas las tablas dentro de una misma familia compartan el valor de la clave primaria. De esta manera, cuando almacenemos una subclase, tanto su tabla como las de sus superclases contendrán los mismos valores para las propiedades comunes, gracias a que todas ellas comparten la misma ID. Este sistema puede provocar en determinadas circunstancias problemas de rendimiento, ya que al igual que con la union de subclases, la base de datos tendra que realizar múltiples operaciones JOIN ante determinadas solicitudes.
2.7 MAPPED SUPERCLASSES, CLASES ABSTRACTAS Y NO-ENTIDADES
Para terminar este segundo artículo del tutorial de introducción e JPA 2.0, veamos de manera muy superficial la forma en la que gestiona JPA tres tipos de clases no vistas hasta ahora: Mapped Superclases, clases abstractas, y no-entidades.
Mapped Superclasses son clases que no son manejadas por el proveedor de persistencia, pero que comparten sus propiedades con cualquier entidad que extienda de ellas:
@MappedSuperclass
@Inheritance(strategy = InheritanceType.XXX)
public class MapedSuperClase {
private String propiedadUno;
private int propiedadDos;
// Getters y setters
}
@Entity
public class SuperClase extends MappedSuperClase { ... }
En el ejemplo anterior, únicamente SuperClase será manejada por el proveedor de persistencia. Sin embargo, durante el mapeo se incluirán todas las propiedades heredadas de MappedSuperClase.
Por otro lado, todas las clases abstractas son tratadas exactamente igual que si fueran clases concretas, y por tanto son entidades 100% usables siempre que sean declaradas como tal (@Entity, etc). Precisamente por esto último, toda clase que no sea declarada como entidad, será ignorada a la hora de realizar el mapeo relacional. A este tipo de clases se las conoce como no-entidades.
En el próximo artículo veremos como persistir, leer, actualizar, y borrar de una base de datos entidades como las que hemos visto en los dos primeros artículos, así como tres conceptos que afectan a nuestras entidades cuando se encuentran gestionadas el servidor de aplicaciones donde se encuentra el proveedor de JPA: transacciones, metodos callback, y clases listener.
Por fin, y cumpliendo con la fecha que me propuse hace algunas semanas, he terminado de traducir el tutorial Spring MVC Paso a Paso, publicado originalmente por SpringSource. He intentado realizar la traduccion de la manera mas precisa posible a pesar del hecho de que el documento original esta escrito en un lenguage muy directo, y que algunos terminos y conceptos son dificiles de expresar en español. Podeis enviarme las correcciones y sugerencias que creais oportunas, si asi podemos mejorar el documento y hacerlo aun mas accesible.
Espero que este tutorial os resulte util y ameno, y que entreis con fuerza en el framework Spring MVC.
El proximo 19 de Febrero se celebra el Madrid el Spring 2GX Day, el evento de Spring, Groovy y Grails mas importante de España. A lo largo de todo un dia podras participar en charlas y talleres, asi como conocer a la comunidad Spring española. Puedes entontrar toda la informacion relacionada en la web del evento. La inscripcion es totalmente gratuita.
Con esta entrada se da comienzo a una serie de cuatro artículos que introducen JPA en su versión 2.0. En ellos vamos a ver, de forma introductoria, como realizar ORM de forma sencilla mediante JPA. Los artículos seguirán este orden:
1. ORM Básico
2. Asociaciones y Herencia
3. Persistencia, Transacciones, Callbacks y Lísteners
4. JPQL
Tanto los cuatro artículos del tutorial, así como el anexo que los acompaña (donde se explica como poner en marcha un entorno de desarrollo para probar todo el código que se irá mostrando), están disponibles en la página de tutoriales.
1.1 INICIO
En Java solucionamos problemas de negocio a través de objetos, los cuales tienen estado y comportamiento. Sin embargo, las bases de datos relacionales almacenan la información mediante tablas, filas, y columnas, de manera que para almacenar un objeto hay que realizar una correlación entre el sistema orientado a objetos de Java y el sistema relacional de nuestra base de datos. JPA (Java Persistence API - API de Persistencia en Java) es una abstracción sobre JDBC que nos permite realizar dicha correlación de form...
Con esta entrada se da comienzo a una serie de cuatro artículos que introducen JPA en su versión 2.0. En ellos vamos a ver, de forma introductoria, como realizar ORM de forma sencilla mediante JPA. Los artículos seguirán este orden:
1. ORM Básico
2. Asociaciones y Herencia
3. Persistencia, Transacciones, Callbacks y Lísteners
4. JPQL
Tanto los cuatro artículos del tutorial, así como el anexo que los acompaña (donde se explica como poner en marcha un entorno de desarrollo para probar todo el código que se irá mostrando), están disponibles en la página de tutoriales.
1.1 INICIO
En Java solucionamos problemas de negocio a través de objetos, los cuales tienen estado y comportamiento. Sin embargo, las bases de datos relacionales almacenan la información mediante tablas, filas, y columnas, de manera que para almacenar un objeto hay que realizar una correlación entre el sistema orientado a objetos de Java y el sistema relacional de nuestra base de datos. JPA (Java Persistence API - API de Persistencia en Java) es una abstracción sobre JDBC que nos permite realizar dicha correlación de forma sencilla, realizando por nosotros toda la conversión entre nuestros objetos y las tablas de una base de datos. Esta conversión se llama ORM (Object Relational Mapping - Mapeo Relacional de Objetos), y puede configurarse a través de metadatos (mediante xml o anotaciones). Por supuesto, JPA también nos permite seguir el sentido inverso, creando objetos a partir de las tablas de una base de datos, y también de forma transparente. A estos objetos los llamaremos desde ahora entidades (entities).
JPA establece una interface común que es implementada por un proveedor de persistencia de nuestra elección (como Hibernate, Eclipse Link, etc), de manera que podemos elegir en cualquier momento el proveedor que más se adecue a nuestras necesidades. Así, es el proveedor quién realiza el trabajo, pero siempre funcionando bajo la API de JPA.
1.2 UNA ENTIDAD SIMPLE
Este es el ejemplo más simple de entidad:
@Entity public class Pelicula {
@Id
@GeneratedValue private Long id;
private String titulo;
private int duracion;
// Getters y Setters
}
Las entidades suelen ser POJOs. La clase Pelicula se ha anotado con @Entity, lo cual informa al proveedor de persistencia que cada instancia de esta clase es una entidad. Para ser válida, toda entidad debe:
- Tener un constructor por defecto
- Ser una clase de primer nivel (no interna)
- No ser final
- Implementar la interface java.io.Serializabe si es accedida remotamente
La configuración de mapeo puede ser especificada mediante un archivo de configuración XML, o mediante anotaciones. A esta configuración del mapeo la llamamos metadatos. Para mantener las cosas simples, este tutorial utilizará la segunda opción (anotaciones). Cual de ellas elegir depende de preferencias tanto personales como del proyecto. El descriptor XML tiene como ventaja que no es necesario recompilar nuestro código para cambiar la configuración de mapeo; por contra, exige mantener un archivo externo. La configuración mediante anotaciones permite tener en un mismo lugar el código Java y sus instrucciones de comportamiento; por contra, exige una recompilación cada vez que deseamos cambiar el comportamiento del mapeo, además de resultar en código menos portable (otra aplicación no-JPA que use nuestras entidades deberá incluir todas las librerías JPA en su classpath para poder compilar correctamente). Cuando ambas opciones son utilizadas al mismo tiempo, la configuración XML prevalece sobre las anotaciones, de manera que si el descriptor XML establece unos atributos para una propiedad que también ha sido configurada mediante anotaciones, serán los primeros los que el proveedor de persistencia tenga en cuenta.
1.3 IDENTIDAD
Todas las entidades tienen que poseer una identidad que las diferencie del resto, por lo que deben contener una propiedad marcada con la anotación @Id (es aconsejable que dicha propiedad sea de un tipo que admita valores null, como Integer en lugar de int). Existen formas más complejas de identidad (@EmbeddedId, @IdClass) que no vamos a explicar aquí, para mantener los ejemplos sencillos.
De manera adicional, y en lo que respecta a este tutorial, la identidad de una entidad va a ser gestionada por el proveedor de persistencia, así que será dicho proveedor quien le asigne un valor la primera vez que almacene la entidad en la base de datos. Para tal efecto, le añadimos a la propiedad de identidad la anotación @GeneratedValue.
1.4 CONFIGURACIÓN POR DEFECTO
JPA aplica a las entidades que maneja una configuración por defecto, de manera que una entidad es funcional con una mínima cantidad de información (las anotaciones @Entity y @Id en nuestro caso). Con esta configuración por defecto, todas las entidades del tipo Pelicula serán mapeadas a una tabla de la base de datos llamada PELICULA, y cada una de sus propiedades será mapeada a una columna con el mismo nombre (la propiedad id será mapeada en la columna ID, la propiedad titulo será mapeada en la columna TITULO, etc). Sin embargo, no siempre es posible ceñirse a los valores de la configuración por defecto: imaginemos que tenemos que trabajar con una base de datos heredada, con nombres de tablas y filas ya definidos. En ese caso, podemos configurar el mapeo de manera que se ajuste a dichas tablas y filas:
@Entity
@Table(name = "TABLA_PELICULAS")
public class Pelicula {
@Id
@GeneratedValue
@Column(name = "ID_PELICULA")
private Long id;
...
}
La anotación @Table nos permite configurar el nombre de la tabla donde queremos almacenar la entidad mediante el atributo name. Así mismo, mediante la anotación @Column podemos configurar el nombre de la columna donde se almacenará una propiedad, si dicha propiedad puede contener un valor null, etc. Puedes descargar la especificación completa de JPA 2.0 desde aquí.
1.5 LECTURA TEMPRANA Y LECTURA DEMORADA
Cuando leemos una entidad desde la base de datos, ciertas propiedades pueden no ser necesarias en el momento de la creación del objeto. JPA nos permite leer una propiedad desde la base de datos la primera vez que un cliente intenta leer su valor (lectura demorada), en lugar de leerla cuando la entidad que la contiene es creada (lectura temprana). De esta manera, si la propiedad nunca es accedida, nos evitamos el coste de crearla. Esto puede ser útil si la propiedad contiene un objeto de gran tamaño:
La propiedad portada es un objeto que representa una imagen de la portada de una película (un objeto de gran tamaño). Puesto que el coste de crear este objeto al leer la entidad Pelicula es muy alto, lo hemos marcado como una propiedad de lectura demorada mediante la anotación @Basic(fetch = FetchType.LAZY). El comportamiento por defecto de la mayoría de tipos Java es el contrario (lectura temprana). Este comportamiento, a pesar de ser implícito, puede ser declarado explícitamente de la siguiente manera:
Solo los objetos de gran tamaño y ciertos tipos de asociación (concepto que veremos en el segundo artículo de este tutorial) deben ser leidos de forma demorada. Si, por ejemplo, marcamos todas las propiedades de tipo int, String o Date de una entidad con lectura demorada, cada vez que accedamos por primera vez a cada una de ellas el proveedor de persistencia tendrá que hacer una llamada a la base de datos para leerla. Esto, evidentemente, va a provocar que se efectúen multitud de llamadas a la base de datos cuando con solo una (al crear la entidad en memoria) podrían haberse leido todas con apenas coste. Por tanto, es importante tener presente que la manera de configurar el tipo de lectura de una propiedad puede afectar enormemente al rendimiento de nuestra aplicación.
Por otro lado, la anotación @Basic solo puede ser aplicada a tipos primitivos, sus correspondientes clases wrapper, BigDecimal, BigInteger, Date, arrays, algunos tipos del paquete java.sql, enums, y cualquier tipo que implemente la interface Serializable. Además del atributo fetch, la anotación @Basic admite otro atributo, optional, el cual permite definir si la propiedad sobre la que se está aplicando la anotación puede contener el valor null (esto es debido a que en bases de datos relacionales, algunas columnas pueden definir una constricción de tipo non null, la cual impide que se inserte un valor null; por tanto, con @Basic(optional=false) nos ajustaríamos a la citada constricción).
1.6 TIPOS ENUMERADOS
JPA puede mapear los tipos enumerados (enum) mediante la anotación Enumerated:
@Enumerated
private Genero genero;
La configuración por defecto de JPA mapeará cada valor ordinal de un tipo enumerado a una columna de tipo numérico en la base de datos. Por ejemplo, siguiendo el ejemplo anterior, podemos crear un tipo enum que describa el género de una película:
public enum Genero {
TERROR,
DRAMA,
COMEDIA,
ACCION,
}
Si la propiedad genero del primer ejemplo tiene el valor Genero.COMEDIA, en la columna correspondiente de la base de datos de insertará el valor 2 (que es el valor ordinal de Genero.COMEDIA). Sin embargo, si en el futuro introducimos un nuevo tipo de genero en una posición intermedia, o reordenamos las posiciones de los generos, nuestra base de datos contendrá valores erroneos que no se corresponderán con los nuevos valores ordinales del tipo enumerado. Para evitar este problema potencial, podemos forzar a la base de datos a utilizar una columna de texto en lugar de una columna numérica: de esta manera, el valor almacenado será el nombre del valor enum, y no su valor ordinal:
@Enumerated(EnumType.STRING)
private Genero genero;
1.7 TRANSIENT
Ciertas propiedades de una entidad pueden no representar su estado. Por ejemplo, imaginemos que tenemos una entidad que representa a una persona:
@Entity
public class Persona {
@Id
@GeneratedValue
private Long id;
private String nombre;
private String apellidos
private Date fechaNacimiento;
private int edad;
// getters y setters
}
Podemos considerar que la propiedad edad no representa el estado de Persona, ya que si no es actualizada cada cumpleaños, terminará conteniendo un valor erroneo. Ya que su valor puede ser calculado gracias a la propiedad fechaNacimiento, no vamos a almacenarlo en la base de datos, si no a calcular su valor en tiempo de ejecución cada vez que lo necesitemos. Para indicar al proveedor de persistencia que ignore una propiedad cuando realice el mapeo, usamos la anotación @Transient:
@Transient
private int edad;
Ahora, para obtener el valor de edad utilizamos su metodo getter:
public int getEdad() {
// calcular la edad y devolverla
}
1.8 COLECCIONES BÁSICAS
Una entidad puede tener propiedades de tipo java.util.Collection y/o java.util.Map que contengan tipos básicos (todos los tipos wrapper, String, BigDecimal, BigInteger, tipos enumerados y tipos insertables; estos últimos los veremos en la próxima sección). Los elementos de estas colecciones serán almacenados en una tabla diferente a la que contiene la entidad donde están declarados. El tipo de colección tiene que ser concreto (como ArrayList o HashMap) y nunca una interface.
private ArrayList comentarios;
El código de arriba es mapeado por defecto con unos valores predeterminados por JPA, como vimos en la sección 1.4. Y por supuesto, podemos cambiar dicha configuración por defecto mediante diversas anotaciones, entre las que se encuentran:
@ElementCollection permite definir el tipo de lectura (temprana o demorada), así como la clase de objetos que contiene la colección (obligatorio en caso de que la colección no sea de tipo generico). Por otro lado, @CollectionTable nos permite definir el nombre de la tabla donde queremos almacenar los elementos de la colección. Si nuestra coleción es de tipo Map, podemos añadir la anotación @MapKeyColumn(name = "NOMBRE_COLUMNA"), la cual nos permite definir el nombre de la columna donde se almacenarán las claves del Map.
1.9 TIPOS INSERTABLES
Los tipos insertables (embeddables) son objetos que no tienen identidad, por lo que para ser persistidos deben ser primero insertados dentro de una entidad (u otro insertable que será a su vez insertado en una entidad, etc). Cada propiedad del tipo insertable será mapeada a la tabla de la entidad que lo contenga, como si fuera una propiedad declarada en la propia entidad. Definimos un tipo insertable con la anotación @Embeddable:
@Embeddable
public class Direccion {
private String calle;
private int codigoPostal;
...
}
Y lo insertamos en una entidad (u otro tipo insertable) con la anotación @Embedded
@Entity
public class Persona {
...
@Embedded
private Direccion direccion;
}
Ahora, la tabla que contenga las entidades de tipo Persona contendrá sus propias columnas, más las definidas en el tipo insertable Direccion.
1.10 TIPO DE ACCESO
Para terminar este primer artículo del tutorial de introduccion a JPA, vamos a ver que son los tipos de acceso, y como aplicarlos correctamente. JPA permite definir dos tipos de acceso:
- Acceso a variable (Field access)
- Acceso a propiedad (Property access)
El tipo de acceso que usará una entidad está definido por el lugar donde situemos sus anotaciones de mapeo. Si las anotaciones están en las variables que conforman la clase (como hemos hecho hasta ahora), estaremos indicando a JPA que debe realizar acceso a variable<. Si, por el contrario situamos las anotaciones de mapeo en los métodos getter, estaremos indicando un acceso a propiedad. A efectos prácticos, no existe diferencia alguna entre ambas opciones (más alla de gustos personales y de organización de código). Sin embargo, en determinadas ocasiones debemos ser consecuentes con el tipo de acceso que elijamos, evitando mezclar tipos de acceso (lo cual puede inducir a JPA a actuar de forma erronea). Un ejemplo típico es el uso de clases insertables: salvo que se especifique lo contrario, las clases insertables heredan el tipo de acceso de la entidad donde son insertadas, ignorando cualquier anotación que se haga hecho sobre ellas previamente. Veamos un ejemplo donde se muestra este problema:
@Embeddable
public class Insertable {
private int variable;
@Column(name = "VALOR_DOBLE")
public int getVariable() {
return variable * 2;
}
public void setVariable(int variable) {
this.variable = variable;
}
}
@Entity
public class Entidad
@Id
@GeneratedValue
private Long id;
@Embedded
private Insertable insertable;
}
En el ejemplo anterior, la clase Insertable define un tipo de acceso a propiedad (ya que las anotaciones se han definido en los metodos getter), y queremos que se acceda a traves de estos metodos al valor de las variables (tal vez necesitemos realizar cierto procesamiento sobre la variable, como multiplicarla por dos antes de devolverla). Sin embargo, la clase Entidad define un tipo de acceso a variable (ya que las anotaciones se han definido las variables). Puesto que el tipo insertable va a heredar el tipo de acceso de la entidad donde se encuentra definido, cuando accedamos al valor de Entidad.insertable.variable obtendremos un valor no esperado (el valor de variable, en lugar de su valor multiplicado por dos que devuelve getVariable()). Para evitar estos problemas debemos indicar explícitamente el tipo de acceso de Insertable mediante la anotación @Access:
@Embeddable
@Access(AccessType.PROPERTY)
public class Insertable { ... }
Anotando la clase Insertable con @Access(AccessType.PROPERTY), cuando accedamos a Entidad.insertable.variable lo haremos a través de su método getter, a pesar de que en Entidad se está usando un tipo de acceso a variable. Un efecto de definir explícitamente el tipo de acceso, es que las anotaciones que no correspondan con ese tipo de acceso serán ignoradas (a no ser que vengan acompañadas a su vez con la anotación @Access):
@Embeddable
@Access(AccessType.FIELD)
public class Insertable {
private int variable;
@Column(name = "VALOR_DOBLE")
public int getVariable() {
return variable * 2;
}
public void setVariable(int variable) {
this.variable = variable;
}
}
En el ejemplo anterior, se ha definido un acceso a variable de forma explícito (con @Access(AccessType.FIELD)), por lo que la anotación @Column será ignorada (no accederemos a la variabla a través del metodo getter, a pesar de estar anotado). Toda la discusión sobre tipos de acceso está directamente relacionada con la capacidad de JPA para acceder a todas las variables y metodos de una clase, independientemente de su nivel de acceso (public, protected, package-default, o private).
En el próximo artículo veremos como trabajar con asociaciones y con herencia cuando realizamos ORM.
Actualmente, en mi tiempo libre, me encuentro traduciendo el tutorial Developing a Spring Framework MVC Application Step by Step. En este tutorial se muestra de forma simple como desarrollar una aplicacion de tipo MVC utilizando Spring MVC. A pesar que dicha aplicacion es muy basica, se muestra la potencia y funcionalidad del framework, y es un muy buen punto de partida para comenzar a usar tanto el modelo MVC (Model-View-Controller) como Spring Framework en su version 2.5. La traduccion estara finalizada en Enero de 2010 y podreis acceder a ella desde aqui.
Si encontrais alguna errata tipografica o de traduccion, podeis comunicarmelo mediante la pagina de contacto. El codigo es el mismo que en la version original, por lo que se asume que funciona correctamente. Espero y deseo que este fantastico tutorial os sea de gran utilidad.
Bienvenidos a mi blog personal sobre programacion. Todo el sistema de blogging lo estoy diseñando y programado por mi mismo, utilizando Java, Spring Framework e Hibernate. Las entradas se encuentran respaldadas en una base de datos, y el acceso a dichas entradas, asi como la navegacion por ellas, estan programados en PHP, tambien por mi mismo. Mas adelante añadire mas funcionalidades a esta navegacion (como un sistema de archivo para consultar todas las entradas publicadas, un sistema de busqueda, etc). Mi intencion es desarrollar toda la aplicacion con software 100% escrito por mi, creando las librerias necesarias para ello y adaptando la funcionalidad al diseño de la web. Asi pues, este blog es un sistema (todavia) basico, pero completamente funcional.
La idea de desarrollar enteramente el blog surgio de la imposibilidad de encontrar una herramienta de blogging que se adaptara a mis necesidades, con lo cual decidi construir mi propio sistema de blogs. Aunque aun falta mucho trabajo por hacer para que presente el aspecto que pretendo, espero que disfrutes de el tanto como yo disfruto programandolo.