3. JPA en una arquitectura multicapa
Para estudiar el API JPA, hemos utilizado la siguiente arquitectura de prueba:
![]() |
Nuestros programas de prueba eran aplicaciones de consola que consultaban directamente la capa JPA. En esta ocasión, descubrimos los principales métodos de la capa JPA. Nos encontrábamos en un entorno denominado «Java SE» (Standard Edition). JPA funciona tanto en un entorno Java SE como en uno Java EE5 (Enterprise Edition).
Ahora que ya dominamos tanto la configuración del puente relacional/objeto como el uso de los métodos de la capa JPA, volvemos a una arquitectura multicapa más clásica:
![]() |
Se accederá a la capa [JPA] a través de una arquitectura de dos capas: [metier] y [dao]. Se utilizarán el framework Spring [7] y, a continuación, el contenedor EJB3 de JBoss y [8] para vincular estas capas entre sí.
Como se ha mencionado anteriormente, JPA está disponible en los entornos SE y EE5. El entorno Java EE5 ofrece numerosos servicios en el ámbito del acceso a datos persistentes, en particular grupos de conexiones, gestores de transacciones, etc. Puede resultar interesante para un desarrollador aprovechar estos servicios. El entorno Java EE5 aún no está muy extendido (mayo de 2007). Actualmente se encuentra en el servidor de aplicaciones Sun Application Server 9.x (Glassfish). Un servidor de aplicaciones es, esencialmente, un servidor de aplicaciones web. Si se crea una aplicación gráfica independiente de tipo Swing, no se puede disponer del entorno EE ni de los servicios que ofrece. Esto supone un problema. Empiezan a aparecer entornos «autónomos» EE y c.a.d. que pueden utilizarse fuera de un servidor de aplicaciones. Es el caso de JBoss y EJB3, que vamos a utilizar en este documento.
En un entorno EE5, las capas se implementan mediante objetos denominados EJB (Enterprise Java Bean). En las versiones anteriores de EE, los EJB (EJB y 2.x) tenían fama de ser difíciles de implementar y de probar, y en ocasiones ofrecían un rendimiento deficiente. Se distingue entre los EJB2.x «entity» y los EJB2.x «session». En resumen, un EJB2.x «entity» es la representación de una fila de una tabla de base de datos y un EJB2.x «session» es un objeto utilizado para implementar las capas [metier], [dao] de una arquitectura multicapa. Una de las principales críticas que se hacen a las capas implementadas con EJB es que solo se pueden utilizar dentro de contenedores EJB, un servicio proporcionado por el entorno EE. Esto dificulta las pruebas unitarias. Así, en el esquema anterior, las pruebas unitarias de las capas [metier] y [dao], construidas con EJB, requerirían la puesta en marcha de un servidor de aplicaciones, una operación bastante engorrosa que no anima precisamente al desarrollador a realizar pruebas con frecuencia.
El framework Spring surgió como respuesta a la complejidad de los EJB2. Spring proporciona, en un entorno SE, un gran número de los servicios que suelen ofrecer los entornos EE. Así, en el ámbito de la «persistencia de datos», que es el que nos interesa aquí, Spring proporciona los grupos de conexiones y los gestores de transacciones que necesitan las aplicaciones. La aparición de Spring ha fomentado la cultura de las pruebas unitarias, que de repente se han vuelto mucho más fáciles de implementar. Spring permite implementar las capas de una aplicación mediante objetos Java clásicos (POJO, Plain Old/Ordinary Java Object), lo que permite su reutilización en otro contexto. Por último, integra numerosas herramientas de terceros de forma bastante transparente, en particular herramientas de persistencia como Hibernate, Ibatis, etc.
Java EE5 se diseñó para subsanar las deficiencias de la especificación anterior EE. Los EJB y 2.x se han convertido en los EJB3. Estos son POJOs etiquetados con anotaciones que los convierten en objetos especiales cuando se encuentran dentro de un contenedor EJB3. Dentro de este, el EJB3 podrá beneficiarse de los servicios del contenedor (grupo de conexiones, gestor de transacciones, etc.). Fuera del contenedor EJB3, el EJB3 se convierte en un objeto Java normal. Sus anotaciones EJB se ignoran.
Anteriormente, hemos representado Spring y JBoss EJB3 como una posible infraestructura (framework) de nuestra arquitectura multicapa. Es esta infraestructura la que proporcionará los servicios que necesitamos: un grupo de conexiones y un gestor de transacciones.
- Con Spring, las capas se implementarán mediante POJOs. Estos tendrán acceso a los servicios de Spring (grupo de conexiones, gestor de transacciones) mediante la inyección de dependencias en dichos POJOs: al crearlos, Spring les inyecta referencias a los servicios que van a necesitar.
- JBoss EJB3 es un contenedor EJB que puede funcionar fuera de un servidor de aplicaciones. Su principio de funcionamiento (para el desarrollador) es análogo al descrito para Spring. Encontraremos pocas diferencias.
Terminaremos el documento con un ejemplo de aplicación web de tres capas, básico pero representativo:
![]() |
3.1. Ejemplo 1: Spring / JPA con la entidad «Persona»
Tomamos la entidad Personne estudiada en el apartado 2.1 y la integramos en una arquitectura multicapa en la que la integración de las capas se realiza con Spring y la capa de persistencia se implementa mediante Hibernate.
![]() |
Se da por supuesto que el lector tiene conocimientos básicos sobre Spring. Si no fuera así, puede consultar el siguiente documento, que explica el concepto de inyección de dependencias, que constituye el núcleo de Spring:
[ref3]: Spring IoC (Inversión de control) [http://tahe.developpez.com/java/springioc].
3.1.1. El proyecto « » de Eclipse / Spring / Hibernate
El proyecto de Eclipse es el siguiente:
![]() |
![]() |
- en [1]: el proyecto Eclipse. Se encuentra en [6], dentro de los ejemplos del tutorial [5]. Lo importaremos.
- en [2]: los códigos Java de las capas presentados en paquetes:
- [entites]: el paquete de entidades JPA
- [dao]: la capa de acceso a los datos, que se basa en la capa JPA
- [service]: una capa de servicios más que de negocio. En ella se utilizará el servicio de transacciones de los contenedores.
- [tests]: agrupa los programas de pruebas.
- en [3]: la biblioteca [jpa-spring] agrupa los archivos JAR necesarios para Spring (véanse también [7] y [8]).
- en [4]: la carpeta [conf] recoge los archivos de configuración de Spring para cada uno de los SGBD utilizados en este tutorial.
3.1.2. Las entidades JPA
![]() |
Aquí solo se gestiona una entidad, la entidad Personne, que se ha analizado en el apartado 2.1 y cuya configuración se recuerda a continuación:
package entites;
...
@Entity
@Table(name="jpa01_hb_personne")
public class Personne {
@Id
@Column(name = "ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column(name = "VERSION", nullable = false)
@Version
private int version;
@Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
@Column(name = "PRENOM", length = 30, nullable = false)
private String prenom;
@Column(name = "DATENAISSANCE", nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(name = "MARIE", nullable = false)
private boolean marie;
@Column(name = "NBENFANTS", nullable = false)
private int nbenfants;
// constructores
public Personne() {
}
public Personne(String nom, String prenom, Date datenaissance, boolean marie,
int nbenfants) {
...
}
// toString
public String toString() {
return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(),
getNom(), getPrenom(), new SimpleDateFormat("dd/MM/yyyy")
.format(getDatenaissance()), isMarie(), getNbenfants());
}
// métodos getter y setter
...
}
3.1.3. La capa [dao]
![]() | ![]() |
La capa [dao] presenta la siguiente interfaz IDao:
package dao;
import java.util.List;
import entites.Personne;
public interface IDao {
// obtener una persona mediante su identificador
public Personne getOne(Integer id);
// obtener todas las personas
public List<Personne> getAll();
// guardar un usuario
public Personne saveOne(Personne personne);
// actualizar un usuario
public Personne updateOne(Personne personne);
// eliminar a una persona mediante su identificador
public void deleteOne(Integer id);
// obtener los usuarios cuyo nombre se ajuste a un patrón
public List<Personne> getAllLike(String modele);
}
La implementación [Dao] de esta interfaz es la siguiente:
package dao;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import entites.Personne;
public class Dao implements IDao {
@PersistenceContext
private EntityManager em;
// eliminar a una persona mediante su identificador
public void deleteOne(Integer id) {
Personne personne = em.find(Personne.class, id);
if (personne == null) {
throw new DaoException(2);
}
em.remove(personne);
}
@SuppressWarnings("unchecked")
// obtener todas las personas
public List<Personne> getAll() {
return em.createQuery("select p from Personne p").getResultList();
}
@SuppressWarnings("unchecked")
// obtener las personas cuyo nombre se ajusta a un patrón
public List<Personne> getAllLike(String modele) {
return em.createQuery("select p from Personne p where p.nom like :modele")
.setParameter("modele", modele).getResultList();
}
// obtener una persona mediante su identificador
public Personne getOne(Integer id) {
return em.find(Personne.class, id);
}
// guardar un usuario
public Personne saveOne(Personne personne) {
em.persist(personne);
return personne;
}
// actualizar un usuario
public Personne updateOne(Personne personne) {
return em.merge(personne);
}
}
- En primer lugar, cabe destacar la simplicidad de la implementación [Dao]. Esto se debe al uso de la capa JPA, que realiza la mayor parte del trabajo de acceso a los datos.
- Línea 10: la clase [Dao] implementa la interfaz [IDao]
- línea 13: el objeto de tipo [EntityManager] que se utilizará para manipular el contexto de persistencia JPA. Por conveniencia, a veces lo confundiremos con el propio contexto de persistencia. El contexto de persistencia contendrá entidades Personne.
- Línea 12: en ninguna parte del código se inicializa el campo [EntityManager em]. Spring lo inicializará al iniciar la aplicación. Es la anotación JPA @PersistenceContext de la línea 12 la que solicita a Spring que inyecte en em un gestor de contexto de persistencia.
- Líneas 26-28: la lista de todas las personas se obtiene mediante una consulta JPQL.
- líneas 32-35: la lista de todas las personas cuyo nombre se ajusta a un determinado modelo se obtiene mediante una consulta JPQL.
- líneas 38-40: la persona con dicho identificador se obtiene mediante el método find de API JPA. Devuelve un puntero null si la persona no existe.
- líneas 43-46: una persona se hace persistente mediante el método «persist» de API JPA. El método hace que la persona sea persistente.
- líneas 49-51: la actualización de una persona se lleva a cabo mediante el método «merge» de API JPA. Este método solo tiene sentido si la persona así actualizada estaba previamente desvinculada. El método hace que la persona así creada sea persistente.
- líneas 16-22: la eliminación de la persona cuyo identificador se nos pasa como parámetro se realiza en dos pasos:
- línea 17: se busca en el contexto de persistencia
- líneas 18-20: si no se encuentra, se lanza una excepción con el código de error 2
- línea 21: si se ha encontrado, se elimina del contexto de persistencia mediante el método remove de API JPA.
- Lo que no se ve por el momento es que cada método se ejecutará dentro de una transacción iniciada por la capa [service].
La aplicación tiene su propio tipo de excepción denominado [DaoException]:
package dao;
@SuppressWarnings("serial")
public class DaoException extends RuntimeException {
// código de error
private int code;
public DaoException(int code) {
super();
this.code = code;
}
public DaoException(String message, int code) {
super(message);
this.code = code;
}
public DaoException(Throwable cause, int code) {
super(cause);
this.code = code;
}
public DaoException(String message, Throwable cause, int code) {
super(message, cause);
this.code = code;
}
// getter y setter
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- línea 4: [DaoException] deriva de [RuntimeException]. Por lo tanto, se trata de un tipo de excepción que el compilador no nos obliga a gestionar mediante un try/catch ni a incluir en la firma de los métodos. Por este motivo, [DaoException] no figura en la firma del método [deleteOne] de la interfaz [IDao]. Esto permite que dicha interfaz sea implementada por una clase que lance otro tipo de excepciones, siempre que esta también derive de [RuntimeException].
- Para diferenciar los errores que pueden producirse, se utiliza el código de error de la línea 7. Los tres constructores de las líneas 14, 19 y 24 son los de la clase padre [RuntimeException], a los que se ha añadido un parámetro: el del código de error que se desea asignar a la excepción.
3.1.4. La capa [metier / service]
![]() |
La capa [service] presenta la siguiente interfaz [IService]:
package service;
import java.util.List;
import entites.Personne;
public interface IService {
// obtener un usuario mediante su identificador
public Personne getOne(Integer id);
// obtener todos los usuarios
public List<Personne> getAll();
// guardar un usuario
public Personne saveOne(Personne personne);
// Actualizar un usuario
public Personne updateOne(Personne personne);
// eliminar a una persona mediante su identificador
public void deleteOne(Integer id);
// obtener las personas cuyo nombre se ajusta a un patrón
public List<Personne> getAllLike(String modele);
// eliminar varias personas a la vez
public void deleteArray(Personne[] personnes);
// guardar varias personas a la vez
public Personne[] saveArray(Personne[] personnes);
// actualizar varias personas a la vez
public Personne[] updateArray(Personne[] personnes);
}
- líneas 8-24: la interfaz [IService] recoge los métodos de la interfaz [IDao]
- línea 27: el método [deleteArray] permite eliminar un conjunto de personas dentro de una transacción: se eliminan todas las personas o ninguna.
- líneas 30 y 33: métodos análogos a [deleteArray] para guardar (línea 30) o actualizar (línea 33) un conjunto de personas dentro de una transacción.
La implementación [Service] de la interfaz [IService] es la siguiente:
package service;
...
// Todos los métodos de la clase se ejecutan en una transacción
@Transactional
public class Service implements IService {
// capa [dao]
private IDao dao;
public IDao getDao() {
return dao;
}
public void setDao(IDao dao) {
this.dao = dao;
}
// eliminar varias personas a la vez
public void deleteArray(Personne[] personnes) {
for (Personne p : personnes) {
dao.deleteOne(p.getId());
}
}
// Eliminar una persona mediante su identificador
public void deleteOne(Integer id) {
dao.deleteOne(id);
}
// obtener todas las personas
public List<Personne> getAll() {
return dao.getAll();
}
// obtener los usuarios cuyo nombre se ajusta a un patrón
public List<Personne> getAllLike(String modele) {
return dao.getAllLike(modele);
}
// obtener un usuario mediante su identificador
public Personne getOne(Integer id) {
return dao.getOne(id);
}
// guardar varias personas a la vez
public Personne[] saveArray(Personne[] personnes) {
Personne[] personnes2 = new Personne[personnes.length];
for (int i = 0; i < personnes.length; i++) {
personnes2[i] = dao.saveOne(personnes[i]);
}
return personnes2;
}
// guardar una persona
public Personne saveOne(Personne personne) {
return dao.saveOne(personne);
}
// actualizar varias personas a la vez
public Personne[] updateArray(Personne[] personnes) {
Personne[] personnes2 = new Personne[personnes.length];
for (int i = 0; i < personnes.length; i++) {
personnes2[i] = dao.updateOne(personnes[i]);
}
return personnes2;
}
// actualizar un usuario
public Personne updateOne(Personne personne) {
return dao.updateOne(personne);
}
}
- línea 6: la anotación Spring @Transactional indica que todos los métodos de la clase deben ejecutarse dentro de una transacción. Se iniciará una transacción antes de que comience la ejecución del método y se cerrará tras la ejecución. Si se produce una excepción de tipo [RuntimeException] o derivada durante la ejecución del método, una reversión automática anula toda la transacción; de lo contrario, una confirmación automática la valida. Cabe destacar que el código Java no tiene que preocuparse por las transacciones, ya que estas son gestionadas por Spring.
- Línea 10: una referencia a la capa [dao]. Más adelante veremos que Spring inicializa esta referencia al iniciar la aplicación.
- Los métodos de [Service] se limitan a llamar a los métodos de la interfaz [IDao dao] de la línea 10. Dejamos que el lector examine el código. No presenta dificultades particulares.
- Anteriormente hemos dicho que cada método de [Service] se ejecuta en una transacción. Esta está vinculada al hilo de ejecución del método. En este hilo se ejecutan métodos de la capa [dao]. Estos se vincularán automáticamente a la transacción del hilo de ejecución. El método [deleteArray] (línea 21), por ejemplo, debe ejecutar N veces el método [deleteOne] de la capa [dao]. Estas N ejecuciones se llevarán a cabo dentro del hilo de ejecución del método [deleteArray], es decir, dentro de la misma transacción. Por lo tanto, o bien se validarán todas (commit) si todo va bien, o bien se anularán todas (rollback) si se produce una excepción en cualquiera de las N ejecuciones del método [deleteOne] de la capa [dao].
3.1.5. Configuración de las capas
![]() | ![]() |
La configuración de las capas [service], [dao] y [JPA] se realiza mediante los dos archivos mencionados anteriormente: [META-INF/persistence.xml] y [spring-config.xml]. Ambos archivos deben encontrarse en el directorio classpath de la aplicación, lo que explica que estén en la carpeta [src] del proyecto Eclipse. El nombre del archivo [spring-config.xml] puede elegirse libremente.
persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />
</persistence>
- línea 4: el archivo declara una unidad de persistencia denominada jpa que utiliza transacciones «locales», c.a.d, no proporcionadas por un contenedor EJB3. Estas transacciones son creadas y gestionadas por Spring y se configuran en el archivo [spring-config.xml].
spring-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
<!-- capas de aplicación -->
<bean id="dao" class="dao.Dao" />
<bean id="service" class="service.Service">
<property name="dao" ref="dao" />
</bean>
<!-- capa de persistencia JPA -->
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean
class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<!--
<property name="showSql" value="true" />
-->
<property name="databasePlatform"
value="org.hibernate.dialect.MySQL5InnoDBDialect" />
<property name="generateDdl" value="true" />
</bean>
</property>
<property name="loadTimeWeaver">
<bean
class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
</property>
</bean>
<!-- la fuente de datos DBCP -->
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/jpa" />
<property name="username" value="jpa" />
<property name="password" value="jpa" />
</bean>
<!-- el gestor de transacciones -->
<tx:annotation-driven transaction-manager="txManager" />
<bean id="txManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory"
ref="entityManagerFactory" />
</bean>
<!-- traducción de excepciones -->
<bean
class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<!-- anotaciones de persistencia -->
<bean
class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
</beans>
- líneas 2-5: la etiqueta raíz <beans> del archivo de configuración. No comentaremos los distintos atributos de esta etiqueta. Hay que tener cuidado al copiar y pegar, ya que un error en cualquiera de estos atributos provoca errores que a veces son difíciles de entender.
- línea 8: el bean «dao» es una referencia a una instancia de la clase [dao.Dao]. Se creará una única instancia (singleton) que implementará la capa [dao] de la aplicación.
- Líneas 9-11: instanciación de la capa [service]. El bean «service» es una referencia a una instancia de la clase [service.Service]. Se creará una única instancia (singleton) que implementará la capa [service] de la aplicación. Hemos visto que la clase [service.Service] tenía un campo privado [IDao dao]. Este campo se inicializa en la línea 10 mediante el bean «dao» definido en la línea 8.
- En definitiva, las líneas 8-11 han configurado las capas [dao] y [service]. Más adelante veremos cuándo y cómo se instanciarán.
- Líneas 35-42: se define una fuente de datos. Ya hemos visto el concepto de fuente de datos al estudiar las entidades JPA con Hibernate:
![]() |
En el ejemplo anterior, [c3p0], denominado «grupo de conexiones», podría haberse denominado «fuente de datos». Una fuente de datos proporciona el servicio de «grupo de conexiones». Con Spring, utilizaremos una fuente de datos distinta de [c3p0]. Se trata de [DBCP], del proyecto Apache Commons DBCP [http://jakarta.apache.org/commons/dbcp/]. Los archivos de [DBCP] se han colocado en la biblioteca de usuario [jpa-spring]:
![]() |
- líneas 38-41: para establecer conexiones con la base de datos de destino, la fuente de datos necesita conocer el controlador JDBC utilizado (línea 38), la URL de la base de datos (línea 39), el usuario de la conexión y su contraseña (líneas 40-41).
- líneas 14-32: configuran la capa JPA
- Líneas 14-15: definen un bean de tipo [EntityManagerFactory] capaz de crear objetos de tipo [EntityManager] para gestionar los contextos de persistencia. La clase instanciada [LocalContainerEntityManagerFactoryBean] la proporciona Spring. Necesita una serie de parámetros para instanciarse, definidos en las líneas 16-31.
- línea 16: la fuente de datos que se utilizará para obtener conexiones con SGBD. Se trata de la fuente [DBCP] definida en las líneas 35-42.
- Líneas 17-27: la implementación JPA que se va a utilizar
- líneas 18-26: definen Hibernate (línea 19) como la implementación JPA que se va a utilizar
- líneas 23-24: el dialecto SQL que Hibernate debe utilizar con el SGBD de destino, en este caso MySQL5.
- línea 25: solicita que, al iniciar la aplicación, se genere la base de datos (drop y create).
- Líneas 28-31: definen un «cargador de clases». No sabría explicar con claridad la función de este bean utilizado por el EntityManagerFactory de la capa JPA. En cualquier caso, implica pasar a la clase JVM, que ejecuta la aplicación, el nombre de un archivo cuyo contenido se encargará de cargar las clases al iniciar la aplicación. En este caso, dicho archivo es [spring-agent.jar], ubicado en la biblioteca de usuario [jpa-spring] (véase más arriba). Veremos que Hibernate no necesita este agente, pero que Toplink sí lo requiere.
- líneas 45-50: definen el gestor de transacciones que se va a utilizar
- línea 45: indica que las transacciones se gestionan mediante anotaciones Java (también podrían haberse declarado en spring-config.xml). Se trata, en concreto, de la anotación @Transactional que aparece en la clase [Service] (línea 6).
- líneas 46-50: el gestor de transacciones
- línea 47: el gestor de transacciones es una clase proporcionada por Spring
- líneas 48-49: el gestor de transacciones de Spring necesita conocer la clase EntityManagerFactory, que gestiona la capa JPA. Se trata de la clase definida en las líneas 14-32.
- líneas 57-58: definen la clase que gestiona las anotaciones de persistencia de Spring que se encuentran en el código Java, como la anotación @PersistenceContext de la clase [dao.Dao] (línea 12).
- líneas 53-54: definen la clase de Spring que gestiona, entre otras cosas, la anotación @Repository, la cual hace que una clase así anotada sea apta para la traducción de las excepciones nativas del controlador JDBC de SGBD a excepciones genéricas de Spring de tipo [DataAccessException]. Esta conversión encapsula la excepción nativa de JDBC en un tipo [DataAccessException] que tiene varias subclases:

Esta conversión permite al programa cliente gestionar las excepciones de forma genérica, independientemente del SGBD de destino. No hemos utilizado la anotación @Repository en nuestro código Java. Por lo tanto, las líneas 53-54 son innecesarias. Las hemos dejado ahí simplemente a título informativo.
Ya hemos terminado con el archivo de configuración de Spring. Es complejo y hay muchas cosas que siguen sin estar claras. Se ha extraído de la documentación de Spring. Afortunadamente, su adaptación a diversas situaciones suele reducirse a dos modificaciones:
- la de la base de datos de destino: líneas 38-41. Daremos un ejemplo con Oracle.
- la de la implementación JPA: líneas 14-32. Daremos un ejemplo con Toplink.
3.1.6. Programa cliente [InitDB]
Nos ponemos manos a la obra con la escritura de un primer cliente de la arquitectura descrita anteriormente:
![]() |
El código de [InitDB] es el siguiente:
package tests;
...
public class InitDB {
// capa de servicio
private static IService service;
// constructor
public static void main(String[] args) throws ParseException {
// Configuración de la aplicación
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// capa de servicio
service = (IService) ctx.getBean("service");
// se vacía la base de datos
clean();
// se rellena
fill();
// se comprueba visualmente
dumpPersonnes();
}
// visualización del contenido de la tabla
private static void dumpPersonnes() {
System.out.format("[personnes]%n");
for (Personne p : service.getAll()) {
System.out.println(p);
}
}
// rellenar tabla
public static void fill() throws ParseException {
// creación de personas
Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// que se guardan
service.saveArray(new Personne[] { p1, p2 });
}
// eliminación de elementos de la tabla
public static void clean() {
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
}
- línea 12: se utiliza el archivo [spring-config.xml] para crear un objeto [ApplicationContext ctx], que es una imagen en memoria del archivo. Los beans definidos en [spring-config.xml] se instancian en este momento.
- línea 14: se solicita al contexto de aplicación ctx una referencia a la capa [service]. Se sabe que esta está representada por un bean denominado «service».
- Línea 16: se vacía la base de datos mediante el método clean de las líneas 41-45:
- líneas 42-44: se solicita la lista de todas las personas al contexto de persistencia y se recorre dicha lista para eliminarlas una a una. Quizá recordemos que [spring-config.xml] especifica que la base de datos debe generarse al iniciar la aplicación. Por lo tanto, en nuestro caso, la llamada al método clean es innecesaria, ya que partimos de una base vacía.
- línea 18: el método fill rellena la base de datos. Esta se define en las líneas 32-38:
- líneas 34-35: se crean dos personas
- línea 37: se solicita a la capa [service] que las haga persistentes.
- línea 20: el método dumpPersonnes muestra las personas persistentes. Se define en las líneas 24-29
- líneas 26-28: se solicita la lista de todas las personas persistentes a la capa [service] y se muestran en la consola.
La ejecución de [InitDB] ofrece el siguiente resultado:
3.1.7. Pruebas unitarias [TestNG]
La instalación del complemento [TestNG] se describe en el apartado 5.2.4. El código del programa [TestNG] es el siguiente:
package tests;
....
public class TestNG {
// capa de servicio
private IService service;
@BeforeClass
public void init() {
// registro
log("init");
// configuración de la aplicación
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// capa de servicio
service = (IService) ctx.getBean("service");
}
@BeforeMethod
public void setUp() throws ParseException {
// se vacía la base de datos
clean();
// se rellena
fill();
}
// registros
private void log(String message) {
System.out.println("----------- " + message);
}
// visualización del contenido de la tabla
private void dump() {
log("dump");
System.out.format("[personnes]%n");
for (Personne p : service.getAll()) {
System.out.println(p);
}
}
// rellenar tabla
public void fill() throws ParseException {
log("fill");
// creación de personas
Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// que se guardan
service.saveArray(new Personne[] { p1, p2 });
}
// eliminación de elementos de la tabla
public void clean() {
log("clean");
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
@Test()
public void test01() {
...
}
...
}
- línea 9: la anotación @BeforeClass indica el método que se debe ejecutar para inicializar la configuración necesaria para las pruebas. Se ejecuta antes de que se ejecute la primera prueba. La anotación @AfterClass, que no se utiliza aquí, indica el método que debe ejecutarse una vez que se hayan ejecutado todas las pruebas.
- Líneas 10-17: el método init, anotado con @BeforeClass, utiliza el archivo de configuración de Spring para instanciar las diferentes capas de la aplicación y obtener una referencia a la capa [service]. A continuación, todas las pruebas utilizan esta referencia.
- línea 19: la anotación @BeforeMethod indica el método que se debe ejecutar antes de cada prueba. La anotación @AfterMethod, que aquí no se utiliza, indica el método que se debe ejecutar después de cada prueba.
- líneas 20-25: el método setUp, anotado con @BeforeMethod, vacía la base de datos (clean, líneas 52-56) y, a continuación, la rellena con dos personas (fill, líneas 42-49).
- línea 59: la anotación @Test indica un método de prueba que se debe ejecutar. A continuación describimos estas pruebas.
@Test()
public void test01() {
log("test1");
dump();
// lista de personas
List<Personne> personnes = service.getAll();
assert 2 == personnes.size();
}
@Test()
public void test02() {
log("test2");
// búsqueda de personas por su nombre
List<Personne> personnes = service.getAllLike("p1%");
assert 1 == personnes.size();
Personne p1 = personnes.get(0);
assert "Paul".equals(p1.getPrenom());
}
@Test()
public void test03() throws ParseException {
log("test3");
// creación de una nueva persona
Personne p3 = new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// se guarda
service.saveOne(p3);
// volver a solicitarla
Personne loadedp3 = service.getOne(p3.getId());
// se muestra
System.out.println(loadedp3);
// verificación
assert "p3".equals(loadedp3.getNom());
}
- líneas 2-8: la prueba 01. Hay que recordar que, al inicio de cada prueba, la base de datos contiene dos personas llamadas p1 y p2, respectivamente.
- línea 6: se solicita la lista de personas
- línea 7: se comprueba que el número de personas de la lista obtenida sea 2
- línea 14: se solicita la lista de personas cuyo apellido comience por p1
- se comprueba que la lista obtenida solo tiene un elemento (línea 15) y que el nombre de la única persona obtenida es «Paul» (línea 17)
- línea 24: se crea una persona llamada p3
- línea 25: se guarda en el almacenamiento persistente
- línea 28: se vuelve a solicitar al contexto de persistencia para verificarla
- línea 32: se comprueba que la persona obtenida tiene efectivamente el nombre p3.
@Test()
public void test04() throws ParseException {
log("test4");
// se carga el usuario p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// se muestra
System.out.println(p1);
// se comprueba
assert "p1".equals(p1.getNom());
int version1 = p1.getVersion();
// se modifica el nombre
p1.setPrenom("x");
// se guarda
service.updateOne(p1);
// se vuelve a cargar
p1 = service.getOne(p1.getId());
// se muestra
System.out.println(p1);
// se comprueba que la versión se ha incrementado
assert (version1 + 1) == p1.getVersion();
}
- línea 5: se solicita la persona p1
- línea 10: se comprueba su nombre
- línea 11: se anota su número de versión
- línea 13: se modifica su nombre
- línea 15: se guarda el cambio
- línea 17: se vuelve a solicitar la persona p1
- línea 21: se comprueba que su número de versión ha aumentado en 1
@Test()
public void test05() {
log("test5");
// se carga la persona p2
List<Personne> personnes = service.getAllLike("p2%");
Personne p2 = personnes.get(0);
// se muestra
System.out.println(p2);
// se comprueba
assert "p2".equals(p2.getNom());
// se elimina la persona p2
service.deleteOne(p2.getId());
// se vuelve a cargar
p2 = service.getOne(p2.getId());
// se comprueba que se ha obtenido un puntero nulo
assert null == p2;
// se muestra la tabla
dump();
}
- línea 5: se solicita la persona p2
- línea 10: se comprueba su nombre
- línea 12: se elimina
- línea 14: se vuelve a buscar
- línea 16: se comprueba que no se ha encontrado
@Test()
public void test06() throws ParseException {
log("test6");
// se crea una tabla con dos personas con el mismo nombre (se incumple la regla de unicidad del nombre)
Personne[] personnes = { new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2)};
// se guarda esta tabla; debería producirse una excepción y una reversión
boolean erreur = false;
try {
service.saveArray(personnes);
} catch (RuntimeException e) {
erreur = true;
}
// volcado
dump();
// comprobaciones
assert erreur;
// búsqueda de persona con el nombre p3
List<Personne> personnesp3 = service.getAllLike("p3%");
assert 0 == personnesp3.size();
// volcado
dump();
}
- línea 5: se crea una tabla con tres personas, dos de las cuales tienen el mismo nombre «p4». Esto incumple la regla de unicidad del nombre de la @Entity Personne:
@Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
- línea 11: el array de las tres personas se introduce en el contexto de persistencia. La adición de la segunda persona, p4, debería fallar. Como el método [saveArray] se ejecuta en una transacción, todas las inserciones que se hayan podido realizar anteriormente se anularán. Al final, no se realizará ninguna adición.
- línea 18: se comprueba que [saveArray] haya lanzado efectivamente una excepción
- líneas 20-21: se comprueba que la persona p3, que podría haberse añadido, no lo haya sido.
@Test()
public void test07() {
log("test7");
// prueba de bloqueo optimista
// se carga la persona p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// se muestra
System.out.println(p1);
// se aumenta su número de hijos
int nbEnfants1 = p1.getNbenfants();
p1.setNbenfants(nbEnfants1 + 1);
// se guarda p1
Personne newp1 = service.updateOne(p1);
assert (nbEnfants1 + 1) == newp1.getNbenfants();
System.out.println(newp1);
// se guarda por segunda vez; debería producirse una excepción, ya que p1 ya no tiene la versión correcta
// es newp1 quien lo tiene
boolean erreur = false;
try {
service.updateOne(p1);
} catch (RuntimeException e) {
erreur = true;
}
// verificación
assert erreur;
// se aumenta el número de hijos de newp1
int nbEnfants2 = newp1.getNbenfants();
newp1.setNbenfants(nbEnfants2 + 1);
// se guarda newp1
service.updateOne(newp1);
// se vuelve a cargar
p1 = service.getOne(p1.getId());
// se comprueba
assert (nbEnfants1 + 2) == p1.getNbenfants();
System.out.println(p1);
}
- línea 6: se solicita la persona p1
- línea 12: se incrementa en 1 su número de hijos
- línea 14: se actualiza la persona p1 en el contexto de persistencia. El método [updateOne] hace que la nueva versión newp1 sea persistente de p1. Se diferencia de p1 por su número de versión, que debe haberse incrementado.
- línea 15: se comprueba el número de hijos de newp1.
- línea 21: se vuelve a solicitar una actualización de la persona p1 a partir de la versión anterior p1. Debe producirse una excepción, ya que p1 no es la última versión de la persona p1. Esta última versión es newp1.
- línea 23: se comprueba que el error se ha producido efectivamente
- líneas 27-35: se comprueba que, si se realiza una actualización a partir de la última versión newp1, todo va bien.
@Test()
public void test08() {
log("test8");
// prueba de reversión en updateArray
// se carga la persona p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// se muestra
System.out.println(p1);
// se aumenta su número de hijos
int nbEnfants1 = p1.getNbenfants();
p1.setNbenfants(nbEnfants1 + 1);
// se guardan dos modificaciones, de las cuales la segunda debe fallar (persona mal inicializada)
// debido a la transacción, ambas deben cancelarse
boolean erreur = false;
try {
service.updateArray(new Personne[] { p1, new Personne() });
} catch (RuntimeException e) {
erreur = true;
}
// Comprobaciones
assert erreur;
// se vuelve a cargar la persona p1
personnes = service.getAllLike("p1%");
p1 = personnes.get(0);
// su número de hijos no debería haber cambiado
assert nbEnfants1 == p1.getNbenfants();
}
- La prueba 8 es similar a la prueba 6: comprueba el rollback sobre un updateArray que opera sobre una matriz de dos personas, en la que la segunda no se ha inicializado correctamente. Desde el punto de vista de JPA, la operación «merge» sobre la segunda persona, queexiste ya generará una orden SQL insert que fallará debido a las restricciones nullable=false que existen en algunos de los campos de la entidad Personne.
@Test()
public void test09() {
log("test9");
// prueba de reversión en deleteArray
// volcado
dump();
// se carga la persona p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// se muestra
System.out.println(p1);
// se realizan dos eliminaciones, de las cuales la segunda debe fallar (persona desconocida)
// debido a la transacción, ambas deben anularse
boolean erreur = false;
try {
service.deleteArray(new Personne[] { p1, new Personne() });
} catch (RuntimeException e) {
erreur = true;
}
// comprobaciones
assert erreur;
// se vuelve a cargar la persona p1
personnes = service.getAllLike("p1%");
// verificación
assert 1 == personnes.size();
// volcado
dump();
}
- La prueba 9 es similar a la anterior: comprueba el rollback en un deleteArray que opera sobre una tabla de dos personas en la que la segunda no existe. Sin embargo, en este caso, el método [deleteOne] de la capa [dao] lanza una excepción.
// bloqueo optimista: acceso multihilo
@Test()
public void test10() throws Exception {
// se añade una persona
Personne p3 = new Personne("X", "X", new SimpleDateFormat("dd/MM/yyyy").parse("01/02/2006"), true, 0);
service.saveOne(p3);
int id3 = p3.getId();
// creación de N subprocesos para actualizar el número de hijos
final int N = 20;
Thread[] taches = new Thread[N];
for (int i = 0; i < taches.length; i++) {
taches[i] = new ThreadMajEnfants("thread n° " + i, service, id3);
taches[i].start();
}
// se espera a que finalicen los subprocesos
for (int i = 0; i < taches.length; i++) {
taches[i].join();
}
// se recupera la persona
p3 = service.getOne(id3);
// debe tener N hijos
assert N == p3.getNbenfants();
// eliminación de la persona p3
service.deleteOne(p3.getId());
// verificación
p3 = service.getOne(p3.getId());
// debe haber un puntero nulo
assert p3 == null;
}
- La idea de la prueba 10 es iniciar N subprocesos (línea 9) para incrementar en paralelo el número de hijos de una persona. Se quiere comprobar que el sistema de números de versión resiste bien este caso concreto. Se creó precisamente para ello.
- Líneas 5-6: se crea una persona llamada p3 y se guarda en la memoria persistente. Al principio tiene 0 hijos.
- Línea 7: se anota su identificador.
- Líneas 9-14: se inician N subprocesos en paralelo, todos encargados de incrementar en 1 el número de hijos de p3.
- líneas 16-18: se espera a que finalicen todos los hilos
- línea 20: se solicita ver a la persona p3
- línea 22: se comprueba que ahora tiene N hijos
- línea 24: se elimina a la persona p3.
El hilo [ThreadMajEnfants] es el siguiente:
package tests;
...
public class ThreadMajEnfants extends Thread {
// nombre del hilo
private String name;
// referencia en la capa [service]
private IService service;
// el ID de la persona con la que se va a trabajar
private int idPersonne;
// constructor
public ThreadMajEnfants(String name, IService service, int idPersonne) {
this.name = name;
this.service = service;
this.idPersonne = idPersonne;
}
// núcleo del hilo
public void run() {
// seguimiento
suivi("lancé");
// se repite el bucle hasta que se consiga incrementar en 1
// el número de hijos de la persona idPersonne
boolean fini = false;
int nbEnfants = 0;
while (!fini) {
// se recupera una copia de la persona de idPersonne
Personne personne = service.getOne(idPersonne);
nbEnfants = personne.getNbenfants();
// seguimiento
suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
// incrementa en 1 el número de hijos de la persona
personne.setNbenfants(nbEnfants + 1);
// espera de 10 ms para liberar el procesador
try {
// seguimiento
suivi("début attente");
// se interrumpe para dejar libre al procesador
Thread.sleep(10);
// seguimiento
suivi("fin attente");
} catch (Exception ex) {
throw new RuntimeException(ex.toString());
}
// espera finalizada: se intenta validar la copia
// Mientras tanto, otros subprocesos han podido modificar el original
try {
// se intenta modificar el original
service.updateOne(personne);
// Se ha completado: el original se ha modificado
fini = true;
} catch (javax.persistence.OptimisticLockException e) {
// versión incorrecta del objeto: se ignora la excepción para volver a intentarlo
} catch (org.springframework.transaction.UnexpectedRollbackException e2) {
// excepción de Spring que surge de vez en cuando
} catch (RuntimeException e3) {
// Otro tipo de excepción: se remite
throw e3;
}
}
// seguimiento
suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
}
// seguimiento
private void suivi(String message) {
System.out.println(name + " [" + new Date().getTime() + "] : " + message);
}
}
- líneas 15-19: el constructor almacena la información que necesita para funcionar: su nombre (línea 16), la referencia en la capa [service] que debe utilizar (línea 17) y el identificador de la persona p cuyo número de hijos debe incrementar (línea 18).
- líneas 22-66: el método [run] ejecutado por todos los subprocesos en paralelo.
- línea 29: el hilo intenta repetidamente incrementar el número de hijos de la persona p. Solo se detiene cuando lo ha conseguido.
- línea 31: se consulta a la persona p
- línea 36: se incrementa en memoria su número de hijos
- líneas 38-47: se realiza una pausa de 10 ms. Esto permitirá que otros subprocesos obtengan la misma versión de la persona p. Así, en ese mismo momento habrá varios subprocesos que tengan la misma versión de la persona p y quieran modificarla. Esto es lo que se busca.
- línea 52: una vez finalizada la pausa, el hilo solicita a la capa [service] que persista la modificación. Sabemos que de vez en cuando se producirán excepciones, por lo que hemos rodeado la operación con un try / catch.
- Línea 55: las pruebas muestran que se producen excepciones de tipo [javax.persistence.OptimisticLockException]. Es normal: se trata de la excepción lanzada por la capa JPA cuando un hilo quiere modificar la persona p sin disponer de su última versión. Esta excepción se ignora para permitir que el hilo vuelva a intentar la operación hasta que lo consiga.
- Línea 57: las pruebas muestran que también se producen excepciones de tipo [org.springframework.transaction.UnexpectedRollbackException]. Es molesto e inesperado. No tengo ninguna explicación que dar. Ahora dependemos de Spring, cuando lo que queríamos era evitarlo. Esto significa que, si ejecutamos nuestra aplicación en JBoss Ejb3, por ejemplo, habrá que modificar el código del hilo. La excepción de Spring también se ignora aquí para permitir que el hilo vuelva a intentar la operación de incremento.
- Línea 59: los demás tipos de excepción se transmiten a la aplicación.
Al ejecutar [TestNG] se obtienen los siguientes resultados:

Las 10 pruebas se han superado con éxito.
La prueba 10 merece algunas explicaciones adicionales, ya que el hecho de que haya tenido éxito tiene algo de mágico. Volvamos primero a la configuración de la capa [dao]:
public class Dao implements IDao {
@PersistenceContext
private EntityManager em;
- línea 4: se inyecta un objeto [EntityManager] en el campo «em» mediante la anotación JPA @PersistenceContext. La capa [dao] se instancia una única vez. Se trata de un singleton utilizado por todos los subprocesos que emplean la capa JPA. Por lo tanto, el objeto EntityManager «em» es común a todos los subprocesos. Esto se puede comprobar mostrando el valor de «em» en el método [updateOne] utilizado por los hilos [ThreadMajEnfants]: se obtiene el mismo valor para todos los hilos.
Por lo tanto, cabe preguntarse si los objetos persistentes de los distintos hilos, manipulados por el em de EntityManager —que es el mismo para todos los hilos—, no se mezclarán y crearán conflictos entre sí. Un ejemplo de lo que podría ocurrir se encuentra en [ThreadMajEnfants]:
while (!fini) {
// se recupera una copia de la persona de idPersonne
Personne personne = service.getOne(idPersonne);
nbEnfants = personne.getNbenfants();
// seguimiento
suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
// incrementa en 1 el número de hijos de la persona
personne.setNbenfants(nbEnfants + 1);
// espera 10 ms para liberar el procesador
try {
// seguimiento
suivi("début attente");
// se interrumpe para ceder el procesador
Thread.sleep(10);
// seguimiento
suivi("fin attente");
} catch (Exception ex) {
throw new RuntimeException(ex.toString());
}
- línea 3: un hilo T1 recupera a la persona p
- línea 8: incrementa el número de hijos de p
- línea 14: el hilo T1 se detiene
Un hilo T2 toma el control y también ejecuta la línea 3: solicita la misma persona p que T1. Si el contexto de persistencia de los hilos fuera el mismo, la persona p, que ya se encuentra en el contexto gracias a T1, debería devolverse a T2. De hecho, el método [getOne] utiliza el método [EntityManager].Este método busca en API y JPA, y solo accede a la base de datos si el objeto solicitado no forma parte del contexto de persistencia; de lo contrario, devuelve el objeto del contexto de persistencia. Si fuera así, T1 y T2 tendrían la misma persona p. T2 incrementaría entonces el número de hijos de p en 1 de nuevo (línea 8). Si uno de los subprocesos logra actualizar el objeto tras la pausa, el número de hijos de p se habrá incrementado en 2 y no en 1 como estaba previsto. Cabría esperar entonces que los N subprocesos aumentaran el número de hijos no a N, sino a un valor superior. Sin embargo, este no es el caso. Por lo tanto, podemos concluir que T1 y T2 no tienen la misma referencia p. Lo comprobamos haciendo que los hilos muestren la dirección de p: es diferente para cada uno de ellos.
Por lo tanto, parece que los hilos:
- compartan el mismo gestor de contexto de persistencia (EntityManager)
- pero cada uno tiene su propio contexto de persistencia.
No son más que suposiciones y sería útil contar con la opinión de un experto en este caso.
3.1.8. Cambiar a SGBD
![]() |
Para cambiar de SGBD, basta con sustituir el archivo [src/spring-config.xml] [2] por el archivo [spring-config.xml] del SGBD en cuestión de la carpeta [conf] [1].
El archivo [spring-config.xml] de Oracle es, por ejemplo, el siguiente:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
...
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<!--
<property name="showSql" value="true" />
-->
<property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
<property name="generateDdl" value="true" />
</bean>
</property>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
</property>
</bean>
<!-- la fuente de datos DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.OracleDriver" />
<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="username" value="jpa" />
<property name="password" value="jpa" />
</bean>
...
</beans>
Solo cambian algunas líneas con respecto al mismo archivo utilizado anteriormente para MySQL5:
- línea 14: el dialecto SQL que debe utilizar Hibernate
- líneas 25-28: las características de la conexión JDBC con el SGBD
Se invita al lector a repetir las pruebas descritas para MySQL5 con otros SGBD.
3.1.9. Cambiar la implementación de JPA
Volvamos a la arquitectura de las pruebas anteriores:
![]() |
Sustituimos la implementación JPA / Hibernate por una implementación JPA / Toplink. Dado que Toplink no utiliza las mismas bibliotecas que Hibernate, utilizamos un nuevo proyecto de Eclipse:
![]() |
- en [1]: el proyecto de Eclipse. Es idéntico al anterior. Lo único que cambia es el archivo de configuración [spring-config.xml] [2] y la biblioteca [jpa-toplink], que sustituye a la biblioteca [jpa-hibernate].
- En [3]: la carpeta de ejemplos de este tutorial. En [4], el proyecto de Eclipse que hay que importar.
El archivo de configuración [spring-config.xml] para Toplink queda así:
<?xml version="1.0" encoding="UTF-8"?>
<!-- JVM debe ejecutarse con el argumento -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar
(à remplacer par le chemin exact de spring-agent.jar)-->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
<!-- capas de aplicación -->
<bean id="dao" class="dao.Dao" />
<bean id="service" class="service.Service">
<property name="dao" ref="dao" />
</bean>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
<!--
<property name="showSql" value="true" />
-->
<property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
<property name="generateDdl" value="true" />
</bean>
</property>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
</property>
</bean>
<!-- la fuente de datos DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/jpa" />
<property name="username" value="jpa" />
<property name="password" value="jpa" />
</bean>
<!-- el gestor de transacciones -->
<tx:annotation-driven transaction-manager="txManager" />
<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<!-- traducción de excepciones -->
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<!-- persistencia -->
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
</beans>
Solo hay que modificar unas pocas líneas para pasar de Hibernate a Toplink:
- línea 19: la implementación JPA ahora la realiza Toplink
- línea 23: la propiedad [databasePlatform] tiene un valor diferente al de Hibernate: el nombre de una clase propia de Toplink. En el apartado 2.1.15.2 se explica dónde encontrar este nombre.
Eso es todo. Cabe destacar la facilidad con la que se puede cambiar de SGBD o de la implementación JPA con Spring.
Sin embargo, aún no hemos terminado del todo. Al ejecutar [InitDB], por ejemplo, se produce una excepción que no es fácil de entender:
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [spring-config.xml]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.
Caused by: java.lang.IllegalStateException: Must start with Java agent to use
El mensaje de error de la línea 1 nos lleva a consultar la documentación de Spring. Allí descubrimos un poco más el papel que desempeña una declaración poco clara del archivo [spring-config.xml]:
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<!--
<property name="showSql" value="true" />
-->
<property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
<property name="generateDdl" value="true" />
</bean>
</property>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
</property>
</bean>
La línea 1 de la excepción hace referencia a una clase llamada [InstrumentationLoadTimeWeaver], clase que se encuentra en la línea 13 del archivo de configuración de Spring. La documentación de Spring explica que esta clase es necesaria en algunos casos para cargar las clases de la aplicación y que, para que funcione, la clase JVM debe ejecutarse con un agente. Este agente lo proporciona Spring y se llama [spring-agent]:
![]() |
- El archivo [spring-agent.jar] se encuentra en la carpeta <ejemplos>/lib [1]. Se incluye con la distribución de Spring 2.x (véase el apartado 5.11).
- En [3], se crea una configuración de ejecución [Run/Run...]
- en [4], se crea una configuración de ejecución Java (hay varios tipos de configuraciones de ejecución)
![]() |
- En [5], se selecciona la pestaña [Main]
- en [6], se asigna un nombre a la configuración
- en [7], se indica el nombre del proyecto de Eclipse al que corresponde esta configuración (utilizar el botón «Browse»)
- en [8], se nombra la clase Java que contiene el método [main] (utilizar el botón «Browse»)
- en [9], se accede a la pestaña [Arguments]. En ella se pueden especificar dos tipos de argumentos:
- en [9], los que se pasan al método [main]
- en [10], los que se pasan a JVM, que ejecutará el código. El agente Spring se define mediante el parámetro -javaagent:valor de JVM. El valor es la ruta del archivo [spring-agent.jar].
- en [11]: se valida la configuración
- en [12]: se crea la configuración
- en [13]: se ejecuta
Una vez hecho esto, se ejecuta [InitDB] y ofrece los mismos resultados que con Hibernate. Para [TestNG], hay que proceder de la misma manera:
![]() |
- en [1], se crea una configuración de ejecución [Run/Run...]
- En [2], se crea una configuración de ejecución TestNG
- en [3], se selecciona la pestaña [Test]
- En [4], se asigna un nombre a la configuración
- en [5], se indica el nombre del proyecto de Eclipse al que corresponde esta configuración (utilizar el botón «Browse»)
- en [6], se asigna un nombre a la clase de pruebas (utilizar el botón «Browse»)
![]() |
- en [7], se pasa a la pestaña [Arguments].
- En [8]: se establece el argumento -javaagent de JVM.
- En [9]: se valida la configuración
- en [10]: se crea la configuración
- en [11]: se ejecuta
Una vez hecho esto, se ejecuta [TestNG] y ofrece los mismos resultados que con Hibernate.
3.2. Ejemplo 2: JBoss EJB3 / JPA con la entidad «Persona»
Retomamos el mismo ejemplo que antes, pero lo ejecutamos en un contenedor EJB3, el de JBoss:
![]() |
Normalmente, un contenedor EJB3 se integra en un servidor de aplicaciones. JBoss proporciona un contenedor EJB3 «autónomo» que se puede utilizar fuera de un servidor de aplicaciones. Veremos que ofrece servicios similares a los que ofrece Spring. Intentaremos averiguar cuál de estos contenedores resulta más práctico.
La instalación del contenedor JBoss EJB3 se describe en el apartado 5.12.
3.2.1. El proyecto Eclipse / JBoss EJB3 / Hibernate
El proyecto de Eclipse es el siguiente:
![]() |
![]() |
- en [1]: el proyecto Eclipse. Se encuentra en [6], en los ejemplos del tutorial [5]. Lo importaremos.
- en [2]: los códigos Java de las capas presentados en paquetes:
- [entites]: el paquete de entidades JPA
- [dao]: la capa de acceso a los datos, que se basa en la capa JPA
- [service]: una capa de servicios más que de negocio. En ella se utilizará el servicio de transacciones del contenedor EJB3.
- [tests]: agrupa los programas de pruebas.
- en [3]: la biblioteca [jpa-jbossejb3] agrupa los archivos JAR necesarios para JBoss EJB3 (véanse también [7] y [8]).
- en [4]: la carpeta [conf] reúne los archivos de configuración para cada uno de los SGBD utilizados en este tutorial. Siempre hay dos: [persistence.xml], que configura la capa JPA, y [jboss-config.xml], que configura el contenedor Ejb3.
3.2.2. Las entidades JPA
![]() |
Aquí solo se gestiona una entidad: la entidad Personne, analizada anteriormente en el apartado 3.1.2.
3.2.3. La capa [dao]
![]() |
La capa [dao] presenta la interfaz [IDao] descrita anteriormente en el apartado 3.1.3.
La implementación [Dao] de esta interfaz es la siguiente:
package dao;
...
@Stateless
public class Dao implements IDao {
@PersistenceContext
private EntityManager em;
// eliminar a una persona mediante su identificador
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void deleteOne(Integer id) {
Personne personne = em.find(Personne.class, id);
if (personne == null) {
throw new DaoException(2);
}
em.remove(personne);
}
// obtener todas las personas
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public List<Personne> getAll() {
return em.createQuery("select p from Personne p").getResultList();
}
// obtener las personas cuyo nombre se ajusta a un patrón
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public List<Personne> getAllLike(String modele) {
return em.createQuery("select p from Personne p where p.nom like :modele")
.setParameter("modele", modele).getResultList();
}
// obtener un usuario mediante su identificador
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Personne getOne(Integer id) {
return em.find(Personne.class, id);
}
// guardar un usuario
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Personne saveOne(Personne personne) {
em.persist(personne);
return personne;
}
// actualizar un usuario
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Personne updateOne(Personne personne) {
return em.merge(personne);
}
}
- Este código es idéntico en todos los aspectos al que teníamos con Spring. Solo cambian las anotaciones de Java, y eso es lo que vamos a comentar.
- Línea 4: la anotación @Stateless convierte la clase [Dao] en un EJB sin estado. La anotación @Stateful convierte una clase en un EJB con estado. Un EJB con estado tiene campos privados cuyo valor debe conservarse a lo largo del tiempo. Un ejemplo clásico es el de una clase que contiene información relacionada con el usuario web de una aplicación. Una instancia de esta clase está vinculada a un usuario concreto y, cuando finaliza el hilo de ejecución de una solicitud de ese usuario, la instancia debe conservarse para que esté disponible en la siguiente solicitud del mismo cliente. Un EJB @Stateless no tiene estado. Si retomamos el mismo ejemplo, al finalizar el hilo de ejecución de una solicitud de un usuario, el EJB @Stateless pasará a formar parte de un grupo de EJB @Stateless y quedará disponible para el hilo de ejecución de una solicitud de otro usuario.
- Para el desarrollador, el concepto de EJB 3 @Stateless es similar al del singleton de Spring. Lo utilizará en los mismos casos.
- Línea 7: la anotación @PersistenceContext es la misma que la que aparece en la versión Spring de la capa [dao]. Designa el campo que recibirá el EntityManager, lo que permitirá a la capa [dao] manipular el contexto de persistencia.
- línea 11: la anotación @TransactionAttribute aplicada a un método sirve para configurar la transacción en la que se ejecutará el método. Estos son algunos de los posibles valores de esta anotación:
- TransactionAttributeType.REQUIRED: el método debe ejecutarse en una transacción. Si ya se ha iniciado una transacción, las operaciones de persistencia del método tienen lugar en ella. De lo contrario, se crea y se inicia una transacción.
- TransactionAttributeType.REQUIRES_NEW: el método debe ejecutarse en una transacción nueva. Esta se crea y se inicia.
- TransactionAttributeType.MANDATORY: el método debe ejecutarse en una transacción existente. Si esta no existe, se lanza una excepción.
- TransactionAttributeType.NEVER: el método nunca se ejecuta en una transacción.
- ...
La anotación podría haberse colocado en la propia clase:
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Dao implements IDao {
De este modo, el atributo se aplica a todos los métodos de la clase.
3.2.4. La capa [metier / service]
![]() |
La capa [service] presenta la interfaz [IService] analizada anteriormente en el apartado 3.1.4. La implementación [Service] de la interfaz [IService] es idéntica a la implementación analizada anteriormente en el apartado 3.1.4, salvo por tres detalles:
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Service implements IService {
// capa [dao]
@EJB
private IDao dao;
public IDao getDao() {
return dao;
}
public void setDao(IDao dao) {
this.dao = dao;
}
- línea 2: la clase [Service] es un EJB sin estado
- línea 3: todos los métodos de la clase [Service] deben ejecutarse dentro de una transacción
- líneas 7-8: el contenedor EJB inyectará una referencia al EJB de la capa [dao] en el campo [IDao dao] de la línea 8. Es la anotación @EJB de la línea 7 la que solicita esta inyección. El objeto inyectado debe ser un EJB. Se trata de una diferencia importante con respecto a Spring, donde se puede inyectar cualquier tipo de objeto en otro objeto.
3.2.5. Configuración de las capas
![]() |
La configuración de las capas [service], [dao] y [JPA] se realiza mediante los siguientes archivos:
- [META-INF/persistence.xml] configura la capa JPA
- [jboss-config.xml] configura el contenedor Ejb3. Este, a su vez, utiliza los archivos [default.persistence.properties, ejb3-interceptors-aop.xml, embedded-jboss-beans.xml, jndi.properties]. Estos últimos se incluyen con Jboss Ejb3 y proporcionan una configuración por defecto que normalmente no se modifica. Al desarrollador solo le interesa el archivo [jboss-config.xml]
Analicemos los dos archivos de configuración:
persistence.xml
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
<persistence-unit name="jpa">
<!-- el proveedor JPA es Hibernate -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<!-- la DataSource JTA gestionada por el entorno Java EE5 -->
<jta-data-source>java:/datasource</jta-data-source>
<properties>
<!-- búsqueda de entidades de la capa JBA -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- registros de Hibernate SQL
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- el tipo de SGBD gestionado -->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />
<!-- recreación de todas las tablas (drop+create) al implementar la unidad de persistencia -->
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
Este archivo se parece a los que ya hemos visto al estudiar las entidades JPA. Configura una capa JPA de Hibernate. Las novedades son las siguientes:
- línea 5: la unidad de persistencia jpa no tiene el atributo transaction-type que siempre habíamos tenido hasta ahora:
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />
A falta de valor, el atributo transaction-type tiene el valor por defecto «JTA» (para Java Transaction API), lo que indica que el gestor de transacciones lo proporciona un contenedor EJB3. Un gestor «JTA» puede hacer más que un gestor «RESOURCE_LOCAL»: puede gestionar transacciones que abarquen varias conexiones. Con JTA, se puede abrir una transacción t1 en una conexión c1 sobre una SGBD 1, una transacción t2 en una conexión c2 con una SGBD 2, y poder considerar (t1, t2) como una única transacción en la que o bien todas las operaciones se completan con éxito (commit) o bien ninguna (rollback).
En este caso, trabajamos con el gestor JTA del contenedor JBoss EJB3.
- Línea 11: declara la fuente de datos que debe utilizar el gestor JTA. Esta se indica en forma de un nombre JNDI (Java Naming and Directory Interface). Esta fuente de datos se define en [jboss-config.xml].
jboss-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
xmlns="urn:jboss:bean-deployer:2.0">
<!-- fábrica de DataSource -->
<bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
<!-- nombre JNDI de la DataSource -->
<property name="jndiName">java:/datasource</property>
<!-- base de datos gestionada -->
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="connectionURL">jdbc:mysql://localhost:3306/jpa</property>
<property name="userName">jpa</property>
<property name="password">jpa</property>
<!-- propiedades del grupo de conexiones -->
<property name="minSize">0</property>
<property name="maxSize">10</property>
<property name="blockingTimeout">1000</property>
<property name="idleTimeout">100000</property>
<!-- gestor de transacciones, en este caso JTA -->
<property name="transactionManager">
<inject bean="TransactionManager" />
</property>
<!-- gestor de caché de Hibernate -->
<property name="cachedConnectionManager">
<inject bean="CachedConnectionManager" />
</property>
<!-- ¿Propiedades de instanciación de JNDI? -->
<property name="initialContextProperties">
<inject bean="InitialContextProperties" />
</property>
</bean>
<!-- se solicita DataSource a una fábrica -->
<bean name="datasource" class="java.lang.Object">
<constructor factoryMethod="getDatasource">
<factory bean="datasourceFactory" />
</constructor>
</bean>
</deployment>
- línea 3: la etiqueta raíz del archivo es <deployment>. Este archivo de implementación tiene como objetivo principal configurar la fuente de datos java:/datasource que se ha declarado en persistence.xml.
- La fuente de datos viene definida por el bean «datasource» de la línea 38. Se observa que la fuente de datos se obtiene (línea 40) de una «fábrica» definida por el bean «datasourceFactory» de la línea 7. Para obtener la fuente de datos de la aplicación, el cliente deberá llamar al método [getDatasource] de factory (línea 39).
- línea 7: la clase factory, que proporciona la fuente de datos, es una clase de JBoss.
- Línea 9: el nombre JNDI de la fuente de datos. Debe ser el mismo nombre que el declarado en la etiqueta <jta-data-source> del archivo persistence.xml. De hecho, la capa JPA utilizará este nombre, JNDI, para solicitar la fuente de datos.
- líneas 12-15: algo más habitual: las características JDBC de la conexión a SGBD
- líneas 18-21: configuración del grupo de conexiones interno del contenedor JBoss EJB3.
- líneas 24-26: el gestor JTA. La clase [TransactionManager] inyectada en la línea 25 se define en el archivo [embedded-jboss-beans.xml].
- Líneas 28-30: la caché de Hibernate, un concepto que aún no hemos abordado. La clase [CachedConnectionManager], inyectada en la línea 29, se define en el archivo [embedded-jboss-beans.xml]. Cabe señalar que la configuración depende ahora de Hibernate, lo que nos planteará problemas cuando queramos migrar a Toplink.
- Líneas 32-34: configuración del servicio JNDI.
Hemos terminado con el archivo de configuración de JBoss EJB3. Es complejo y hay muchas cosas que siguen sin estar claras. Se ha extraído de [ref1]. No obstante, podremos adaptarlo a otro SGBD (líneas 12-15 de jboss-config.xml, línea 24 de persistence.xml). La migración a Toplink no ha sido posible por falta de ejemplos.
3.2.6. Programa de cliente [InitDB]
Vamos a abordar la creación de un primer cliente de la arquitectura descrita anteriormente:
![]() |
El código de [InitDB] es el siguiente:
package tests;
...
public class InitDB {
// capa de servicio
private static IService service;
// constructor
public static void main(String[] args) throws ParseException, NamingException {
// se inicia el contenedor EJB3 JBoss
// se utilizan los archivos de configuración ejb3-interceptors-aop.xml y embedded-jboss-beans.xml
EJB3StandaloneBootstrap.boot(null);
// Creación de los beans específicos de la aplicación
EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
// Implementar todos los EJBs encontrados en la ruta de clases (lento, se analizan todos)
// EJB3StandaloneBootstrap.scanClasspath();
// Se implementan todos los EJB encontrados en la ruta de clases de la aplicación
EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));
// Se inicializa el contexto JNDI. Se procesa el archivo jndi.properties
InitialContext initialContext = new InitialContext();
// instanciación de la capa de servicio
service = (IService) initialContext.lookup("Service/local");
// Se vacía la base de datos
clean();
// Se rellena
fill();
// se comprueba visualmente
dumpPersonnes();
// se detiene el contenedor EJB
EJB3StandaloneBootstrap.shutdown();
}
// visualización del contenido de la tabla
private static void dumpPersonnes() {
System.out.format("[personnes]-------------------------------------------------------------------%n");
for (Personne p : service.getAll()) {
System.out.println(p);
}
}
// rellenar la tabla
public static void fill() throws ParseException {
// creación de personas
Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// que se guardan
service.saveArray(new Personne[] { p1, p2 });
}
// eliminación de elementos de la tabla
public static void clean() {
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
}
- La forma de iniciar el contenedor JBoss EJB3 se ha encontrado en [ref1].
- Línea 13: se inicia el contenedor. [EJB3StandaloneBootstrap] es una clase del contenedor.
- Línea 16: la unidad de despliegue configurada por [jboss-config.xml] se despliega en el contenedor: se configuran el gestor JTA, la fuente de datos, el grupo de conexiones, la caché de Hibernate y el servicio JNDI.
- línea 22: se solicita al contenedor que escanee la carpeta «bin» del proyecto Eclipse para localizar los EJB. El contenedor encontrará y gestionará los EJB de las capas [service] y [dao].
- Línea 25: se inicializa un contexto JNDI. Nos servirá para localizar los EJB.
- Línea 28: se solicita al servicio JNDI el EJB correspondiente a la clase [Service] de la capa [service]. Se puede acceder a un EJB de forma local o a través de la red. En este caso, el nombre «Service/local» del EJB buscado hace referencia a la clase [Service] de la capa [service] para un acceso local.
- Ahora, la aplicación está implementada y se dispone de una referencia a la capa [service]. Nos encontramos en la misma situación que tras la línea 11 del código [InitDB] de la versión Spring que se muestra a continuación. Por lo tanto, el código es idéntico en ambas versiones.
public class InitDB {
// capa de servicio
private static IService service;
// constructor
public static void main(String[] args) throws ParseException {
// configuración de la aplicación
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// capa de servicio
service = (IService) ctx.getBean("service");
// se vacía la base de datos
clean();
// se rellena
fill();
// se comprueba visualmente
dumpPersonnes();
}
...
- línea 36 (JBoss EJB3): se detiene el contenedor EJB3.
La ejecución de [InitDB] ofrece los siguientes resultados:
Se invita al lector a consultar estos registros. En ellos se encuentra información interesante sobre el funcionamiento del contenedor EJB3.
3.2.7. Pruebas unitarias [TestNG]
El código del programa [TestNG] es el siguiente:
package tests;
...
public class TestNG {
// capa de servicio
private IService service = null;
@BeforeClass
public void init() throws NamingException, ParseException {
// registro
log("init");
// se inicia el contenedor EJB3 JBoss
// Se están explotando los archivos de configuración ejb3-interceptors-aop.xml y embedded-jboss-beans.xml
EJB3StandaloneBootstrap.boot(null);
// Creación de los beans específicos de la aplicación
EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
// Implementar todos los EJBs encontrados en la ruta de clases (lento, se analizan todos)
// EJB3StandaloneBootstrap.scanClasspath();
// Se implementan todos los EJB encontrados en la ruta de clases de la aplicación
EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));
// Se inicializa el contexto JNDI. Se procesa el archivo jndi.properties
InitialContext initialContext = new InitialContext();
// instanciación de la capa de servicio
service = (IService) initialContext.lookup("Service/local");
// Se vacía la base de datos
clean();
// Se rellena
fill();
// se comprueba visualmente
dumpPersonnes();
}
@AfterClass
public void terminate() {
// registro
log("terminate");
// Apagado del contenedor EJB
EJB3StandaloneBootstrap.shutdown();
}
@BeforeMethod
public void setUp() throws ParseException {
...
}
...
}
- El método init (líneas 10-37), que sirve para configurar el entorno necesario para las pruebas, retoma el código explicado anteriormente en [InitDB].
- El método «terminate» (líneas 40-45), que se ejecuta al final de las pruebas (presencia de la anotación @AfterClass), detiene el contenedor EJB3 (línea 44).
- Todo lo demás es idéntico a lo que había en la versión Spring.
Las pruebas se superan:

3.2.8. Cambiar a SGBD
![]() |
Para cambiar a SGBD, basta con sustituir el contenido de la carpeta [META-INF] [2] por el de la carpeta SGBD dentro de la carpeta [conf] [1]. Tomemos como ejemplo el servidor SQL:
El archivo [persistence.xml] es el siguiente:
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
<persistence-unit name="jpa">
<!-- el proveedor JPA es Hibernate -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<!-- la DataSource JTA gestionada por el entorno Java EE5 -->
<jta-data-source>java:/datasource</jta-data-source>
<properties>
<!-- búsqueda de entidades de la capa JBA -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- registros de Hibernate SQL
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- el tipo de SGBD gestionado -->
<property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
<!-- recreación de todas las tablas (drop+create) al implementar la unidad de persistencia -->
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
Solo ha cambiado una línea:
- línea 24: el dialecto SQL que debe utilizar Hibernate
El archivo [jboss-config.xml] del servidor SQL es, por su parte, el siguiente:
<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
xmlns="urn:jboss:bean-deployer:2.0">
<!-- fábrica de DataSource -->
<bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
<!-- nombre JNDI de la DataSource -->
<property name="jndiName">java:/datasource</property>
<!-- base de datos gestionada -->
<property name="driverClass">com.microsoft.sqlserver.jdbc.SQLServerDriver</property>
<property name="connectionURL">jdbc:sqlserver://localhost\\SQLEXPRESS:1246;databaseName=jpa</property>
<property name="userName">jpa</property>
<property name="password">jpa</property>
<!-- propiedades del grupo de conexiones -->
...
</bean>
</deployment>
Solo se han modificado las líneas 12-15: en ellas se indican las características de la nueva conexión JDBC.
Se invita al lector a repetir con otros archivos SGBD las pruebas descritas para MySQL5.
3.2.9. Cambiar la implementación JPA
Como se ha indicado anteriormente, no hemos encontrado ningún ejemplo de uso del contenedor JBoss EJB3 con TopLink. A día de hoy (junio de 2007), sigo sin saber si esta configuración es posible.
3.3. Otros ejemplos
Resumamos lo que se ha hecho con la entidad Personne. Hemos creado tres arquitecturas para realizar las mismas pruebas:
1 - una implementación con Spring y Hibernate
![]() |
2 - una implementación con Spring y Toplink
![]() |
3 - una implementación con JBoss EJB3 y Hibernate
![]() |
Los ejemplos del tutorial recogen estas tres arquitecturas junto con otras entidades estudiadas en la primera parte del tutorial:
Categoría - Artículo
![]() |
- en [1]: la versión Spring / Hibernate
- en [2]: la versión Spring / Toplink
- en [3]: la versión Jboss Ejb3 / Hibernate
Persona - Dirección - Actividad
![]() |
- en [1]: la versión Spring / Hibernate
- en [2]: la versión Spring / Toplink
- en [3]: la versión JBoss EJB3 / Hibernate
Estos ejemplos no aportan novedades en cuanto a la arquitectura. Simplemente se sitúan en un contexto en el que hay varias entidades que gestionar con relaciones uno a varios o varios a varios entre ellas, algo de lo que carecían los ejemplos con la entidad Personne.
3.4. Ejemplo 3: Spring / JPA en una aplicación web
3.4.1. Presentación
Retomamos aquí una aplicación presentada en el siguiente documento:
[ref4]: Fundamentos del desarrollo web MVC en Java [http://tahe.developpez.com/java/baseswebmvc/].
Este documento presenta los fundamentos del desarrollo web MVC en Java. Para comprender el ejemplo que sigue, el lector debe conocer estos fundamentos. La aplicación web utilizará el servidor Tomcat. Su instalación y su uso en Eclipse se describen en el apartado 5.3.
La aplicación se había desarrollado con una capa [dao] basada en la herramienta Ibatis / SqlMap [http://ibatis.apache.org/], que se encargaba del puente entre el modelo relacional y el modelo orientado a objetos. Nos limitamos a sustituir Ibatis por JPA. La arquitectura de la aplicación será la siguiente:
![]() |
La aplicación web que vamos a desarrollar permitirá gestionar un grupo de personas mediante cuatro operaciones:
- lista de personas del grupo
- añadir una persona al grupo
- modificación de una persona del grupo
- eliminación de una persona del grupo
Estas cuatro operaciones básicas son las propias de una tabla de base de datos. Las siguientes capturas de pantalla muestran las páginas que la aplicación muestra al usuario.
![]() |
![]() |
![]() |
![]() |
![]() |
3.4.2. El proyecto Eclipse
El proyecto Eclipse de la aplicación es el siguiente:
![]() |
- en [1]: el proyecto web. Se trata de un proyecto de Eclipse de tipo [Dynamic Web Project] [2]. Se encuentra en [4], dentro de la carpeta [3] de los ejemplos del tutorial. Lo importaremos.
![]() |
- en [5]: los archivos fuente y la configuración de las capas [service, dao, jpa]. Conservamos el trabajo realizado en [dao, entites, service] del proyecto Eclipse [hibernate-spring-personnes-metier-dao] estudiado en el apartado 3.1.1. Solo desarrollamos la capa [web], representada aquí por el paquete [web]. Por otra parte, conservamos los archivos de configuración [persistence.xml, spring-config.xml] de este proyecto, con la única diferencia de que vamos a utilizar el Postgres SGBD, lo que se traduce en las siguientes modificaciones en [spring-config.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
...
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
...
<property name="databasePlatform" value="org.hibernate.dialect.PostgreSQLDialect" />
...
</property>
...
</bean>
<!-- la fuente de datos DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.postgresql.Driver" />
<property name="url" value="jdbc:postgresql:jpa" />
<property name="username" value="jpa" />
<property name="password" value="jpa" />
</bean>
....
</beans>
Las líneas 8 y 16-19 se han adaptado a Postgres.
- En [6]: la carpeta [WebContent] contiene las páginas JSP del proyecto, así como las bibliotecas necesarias. Estas últimas se muestran en [8]
- La aplicación se puede utilizar con diversos SGBD. Basta con modificar el archivo [spring-config.xml]. La carpeta [conf] [7] contiene el archivo [spring-config.xml] adaptado a diversos SGBD.
3.4.3. La capa [web]
Nuestra aplicación tiene la siguiente arquitectura multicapa:
![]() |
La capa [web] mostrará pantallas al usuario para que pueda gestionar el grupo de personas:
- lista de personas del grupo
- añadir una persona al grupo
- modificación de una persona del grupo
- eliminación de una persona del grupo
Para ello, se basará en la capa [service], que a su vez recurrirá a la capa [dao]. Ya hemos presentado las pantallas gestionadas por la capa [web] (apartado 3.4.1). Para describir la capa web, vamos a presentar sucesivamente:
- su configuración
- sus vistas
- su controlador
- algunas pruebas
3.4.3.1. Configuración de la aplicación web
Repasemos la arquitectura del proyecto Eclipse:
![]() | ![]() |
- En el paquete [web] se encuentra el controlador de la aplicación web: la clase [Application].
- Las páginas JSP / JSTL de la aplicación se encuentran en [WEB-INF/vues].
- La carpeta [WEB-INF/lib] contiene los archivos de terceros necesarios para la aplicación. Estos se pueden ver en la carpeta [Web App Libraries].
[web.xml]
El archivo [web.xml] es el archivo que utiliza el servidor web para cargar la aplicación. Su contenido es el siguiente:
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>spring-jpa-hibernate-personnes-crud</display-name>
<!-- ServletPersonne -->
<servlet>
<servlet-name>personnes</servlet-name>
<servlet-class>web.Application</servlet-class>
<init-param>
<param-name>urlEdit</param-name>
<param-value>/WEB-INF/vues/edit.jsp</param-value>
</init-param>
<init-param>
<param-name>urlErreurs</param-name>
<param-value>/WEB-INF/vues/erreurs.jsp</param-value>
</init-param>
<init-param>
<param-name>urlList</param-name>
<param-value>/WEB-INF/vues/list.jsp</param-value>
</init-param>
</servlet>
<!-- Asignación ServletPersonne-->
<servlet-mapping>
<servlet-name>personnes</servlet-name>
<url-pattern>/do/*</url-pattern>
</servlet-mapping>
<!-- archivos de inicio -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- Página de error inesperado -->
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/WEB-INF/vues/exception.jsp</location>
</error-page>
</web-app>
- líneas 23-26: las URL [/do/*] serán procesadas por el servlet [personnes]
- líneas 7-8: el servlet [personnes] es una instancia de la clase [Application], una clase que vamos a crear.
- líneas 9-20: definen tres parámetros [urlList, urlEdit, urlErreurs] que identifican las URL de las páginas JSP de las vistas [list, edit, erreurs].
- Líneas 28-30: la aplicación tiene una página de inicio por defecto, [index.jsp], que se encuentra en la raíz de la carpeta de la aplicación web.
- Líneas 32-35: la aplicación tiene una página de error predeterminada que se muestra cuando el servidor web detecta una excepción no gestionada por la aplicación.
- línea 37: la etiqueta <exception-type> indica el tipo de excepción gestionada por la directiva <error-page>; en este caso, el tipo [java.lang.Exception] y sus derivados, es decir, todas las excepciones.
- línea 38: la etiqueta <location> indica la página JSP que se mostrará cuando se produzca una excepción del tipo definido por <exception-type>. La excepción que se ha producido está disponible en esta página en un objeto denominado «exception» si la página contiene la directiva:
<%@ page isErrorPage="true" %>
- (continuación)
- si <exception-type> especifica un tipo T1 y una excepción de tipo T2 no derivada de T1 se remite al servidor web, este envía al cliente una página de excepción propia que, por lo general, resulta poco intuitiva. De ahí la utilidad de la etiqueta <error-page> en el archivo [web.xml].
[index.jsp]
Esta página se muestra si un usuario solicita directamente el contexto de la aplicación sin especificar una URL, c.a.d. Aquí, [/spring-jpa-hibernate-personnes-crud]. Su contenido es el siguiente:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/do/list"/>
[index.jsp] redirige (línea 4) al cliente a la URL [/do/list]. Esta URL muestra la lista de personas del grupo.
3.4.3.2. Las páginas JSP / JSTL de la aplicación
La vista [list.jsp]
Sirve para mostrar la lista de personas:

Su código es el siguiente:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
<c:if test="${erreurs!=null}">
<h3>Les erreurs suivantes se sont produites :</h3>
<ul>
<c:forEach items="${erreurs}" var="erreur">
<li><c:out value="${erreur}"/></li>
</c:forEach>
</ul>
<hr>
</c:if>
<h2>Liste des personnes</h2>
<table border="1">
<tr>
<th>Id</th>
<th>Version</th>
<th>Prénom</th>
<th>Nom</th>
<th>Date de naissance</th>
<th>Marié</th>
<th>Nombre d'enfants</th>
<th></th>
</tr>
<c:forEach var="personne" items="${personnes}">
<tr>
<td><c:out value="${personne.id}"/></td>
<td><c:out value="${personne.version}"/></td>
<td><c:out value="${personne.prenom}"/></td>
<td><c:out value="${personne.nom}"/></td>
<td><dt:format pattern="dd/MM/yyyy">${personne.datenaissance.time}</dt:format></td>
<td><c:out value="${personne.marie}"/></td>
<td><c:out value="${personne.nbenfants}"/></td>
<td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
<td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
</tr>
</c:forEach>
</table>
<br>
<a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
</body>
</html>
- Esta vista recibe dos elementos en su modelo:
- el elemento [personnes] asociado a un objeto de tipo [List] de objetos de tipo [Personne]: una lista de personas.
- el elemento opcional [erreurs] asociado a un objeto de tipo [List] de objetos de tipo [String]: una lista de mensajes de error.
- líneas 31-43: se recorre la lista ${personas} para mostrar una tabla HTML que contiene a las personas del grupo.
- línea 40: la URL a la que apunta el enlace [Modifier] se configura mediante el campo [id] de la persona actual, de modo que el controlador asociado a la URL [/do/edit] sepa qué persona debe modificarse.
- línea 41: se hace lo mismo con el enlace [Supprimer].
- línea 37: para mostrar la fecha de nacimiento de la persona en el formato JJ/MM/AAAA, se utiliza la etiqueta <dt> de la biblioteca de etiquetas [DateTime] del proyecto Apache [Jakarta Taglibs]:

El archivo de descripción de esta biblioteca de etiquetas se define en la línea 3.
- Línea 46: el enlace [Ajout] para añadir una nueva persona tiene como destino la URL [/do/edit], al igual que el enlace [Modifier] de la línea 40. Es el valor -1 del parámetro [id] el que indica que se trata de una adición y no de una modificación.
- Líneas 10-18: si el elemento ${errores} está presente en la plantilla, se muestran los mensajes de error que contiene.
La vista [edit.jsp]
Sirve para mostrar el formulario de alta de una nueva persona o de modificación de una persona ya existente:
![]() |
El código de la vista [edit.jsp] es el siguiente:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="../ressources/standard.jpg">
<h2>Ajout/Modification d'une personne</h2>
<c:if test="${erreurEdit!=''}">
<h3>Echec de la mise à jour :</h3>
L'erreur suivante s'est produite : ${erreurEdit}
<hr>
</c:if>
<form method="post" action="<c:url value="/do/validate"/>">
<table border="1">
<tr>
<td>Id</td>
<td>${id}</td>
</tr>
<tr>
<td>Version</td>
<td>${version}</td>
</tr>
<tr>
<td>Prénom</td>
<td>
<input type="text" value="${prenom}" name="prenom" size="20">
</td>
<td>${erreurPrenom}</td>
</tr>
<tr>
<td>Nom</td>
<td>
<input type="text" value="${nom}" name="nom" size="20">
</td>
<td>${erreurNom}</td>
</tr>
<tr>
<td>Date de naissance (JJ/MM/AAAA)</td>
<td>
<input type="text" value="${datenaissance}" name="datenaissance">
</td>
<td>${erreurDateNaissance}</td>
</tr>
<tr>
<td>Marié</td>
<td>
<c:choose>
<c:when test="${marie}">
<input type="radio" name="marie" value="true" checked>Oui
<input type="radio" name="marie" value="false">Non
</c:when>
<c:otherwise>
<input type="radio" name="marie" value="true">Oui
<input type="radio" name="marie" value="false" checked>Non
</c:otherwise>
</c:choose>
</td>
</tr>
<tr>
<td>Nombre d'enfants</td>
<td>
<input type="text" value="${nbenfants}" name="nbenfants">
</td>
<td>${erreurNbEnfants}</td>
</tr>
</table>
<br>
<input type="hidden" value="${id}" name="id">
<input type="hidden" value="${version}" name="version">
<input type="submit" value="Valider">
<a href="<c:url value="/do/list"/>">Annuler</a>
</form>
</body>
</html>
Esta vista presenta un formulario para añadir una nueva persona o actualizar una ya existente. A partir de ahora, y para simplificar la redacción, utilizaremos únicamente el término [mise à jour]. El botón [Valider] (línea 73) activa el POST del formulario en la URL [/do/validate] (línea 16). Si el POST falla, se vuelve a mostrar la vista [edit.jsp] con el error o los errores que se hayan producido; de lo contrario, se muestra la vista [list.jsp].
- La vista [edit.jsp], que se muestra tanto en un GET como en un POST que falla, recibe los siguientes elementos en su plantilla:
atributo | GET | POST |
identificador de la persona actualizada | ídem | |
su versión | ídem | |
su nombre | nombre introducido | |
su apellido | apellido introducido | |
su fecha de nacimiento | Fecha de nacimiento introducida | |
su estado civil | estado civil introducido | |
Número de hijos | Número de hijos introducido | |
vacío | un mensaje de error que indica que no se ha podido añadir o la modificación en el momento de la ejecución de POST, provocada por el botón [Envoyer]. Vacío si no hay error. | |
vacío | indica un nombre de pila incorrecto; en caso contrario, está vacío | |
vacío | indica un apellido erróneo; si no, está vacío | |
vacío | indica una fecha de nacimiento errónea; si no, se deja en blanco | |
vacío | indica un número de hijos erróneo; si no, se deja en blanco |
- líneas 11-15: si el código POST del formulario da error, aparecerá [erreurEdit!=''] y se mostrará un mensaje de error.
- línea 16: el formulario se enviará a la URL [/do/validate]
- línea 20: se muestra el elemento [id] de la plantilla
- línea 24: se muestra el elemento [version] de la plantilla
- líneas 26-32: introducción del nombre de la persona:
- al mostrar el formulario por primera vez (GET), ${nombre} muestra el valor actual del campo [prenom] del objeto [Personne] actualizado y ${erreurPrenom} está vacío.
- En caso de error tras el campo POST, se vuelve a mostrar el valor introducido ${prenom}, así como el posible mensaje de error ${erreurPrenom}.
- líneas 33-39: introducción del nombre de la persona
- líneas 40-46: introducción de la fecha de nacimiento de la persona
- líneas 47-61: introducción del estado civil de la persona mediante un botón de selección. Se utiliza el valor del campo [marie] del objeto [Personne] para determinar cuál de los dos botones de selección debe marcarse.
- líneas 62-68: introducción del número de hijos de la persona
- línea 71: un campo oculto HTML denominado [id], cuyo valor es el campo [id] de la persona que se está actualizando; -1 si se trata de una alta, y otro valor si se trata de una modificación.
- línea 72: un campo oculto HTML denominado [version] y cuyo valor es el campo [id] de la persona que se está actualizando.
- línea 73: el botón [Valider], de tipo [Submit], del formulario
- línea 74: un enlace que permite volver a la lista de personas. Se ha denominado [Annuler] porque permite salir del formulario sin validarlo.
La vista [exception.jsp]
Sirve para mostrar una página que indica que se ha producido una excepción no gestionada por la aplicación y que se ha transmitido al servidor web.
Por ejemplo, eliminemos una persona que no existe en el grupo:
![]() |
El código de la vista [exception.jsp] es el siguiente:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>
<%
response.setStatus(200);
%>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
<h2>MVC - personnes</h2>
L'exception suivante s'est produite :
<%= exception.getMessage()%>
<br><br>
<a href="<c:url value="/do/list"/>">Retour à la liste</a>
</body>
</html>
- Esta vista recibe una clave en su modelo, el elemento [exception], que es la excepción que ha sido interceptada por el servidor web. Para que el servidor web incluya este elemento en el modelo de la página JSP, es necesario que la página tenga definida la etiqueta de la línea 3.
- Línea 6: se establece en 200 el código de estado HTTP de la respuesta. Se trata del primer encabezado HTTP de la respuesta. El código 200 indica al cliente que su solicitud ha sido atendida. Por lo general, se ha incluido un documento HTML en la respuesta del servidor. Este es el caso aquí. Si no se establece en 200 el código de estado HTTP de la respuesta, tendrá aquí el valor 500, lo que significa que se ha producido un error. De hecho, al haber interceptado el servidor web una excepción no gestionada, considera que esta situación es anómala y la señala mediante el código 500. La reacción ante el código HTTP 500 varía según los navegadores: Firefox muestra el documento HTML que puede acompañar a esta respuesta, mientras que IE ignora dicho documento y muestra su propia página. Por este motivo, hemos sustituido el código 500 por el código 200.
- línea 16: se muestra el texto de la excepción
- línea 18: se ofrece al usuario un enlace para volver a la lista de personas
La vista [erreurs.jsp]
Sirve para mostrar una página que indica los errores de inicialización de la aplicación, c.a.d, y los errores detectados durante la ejecución del método [init] del servlet del controlador. Puede tratarse, por ejemplo, de la ausencia de un parámetro en el archivo [web.xml], tal y como se muestra en el ejemplo siguiente:

El código de la página [erreurs.jsp] es el siguiente:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
<c:forEach var="erreur" items="${erreurs}">
<li>${erreur}</li>
</c:forEach>
</ul>
</body>
</html>
La página recibe en su plantilla un elemento [erreurs], que es un objeto de tipo [ArrayList] de objetos [String]; estos últimos son mensajes de error. Se muestran mediante el bucle de las líneas 13-15.
3.4.3.3. El controlador de la aplicación
El controlador [Application] se define en el paquete [web]:

Estruct tura e inicialización del controlador
El esqueleto del controlador [Application] es el siguiente:
package web;
...
@SuppressWarnings("serial")
public class Application extends HttpServlet {
// parámetros de instancia
private String urlErreurs = null;
private ArrayList erreursInitialisation = new ArrayList<String>();
private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
private Map params = new HashMap<String, String>();
// servicio
private IService service = null;
// inicialización
@SuppressWarnings("unchecked")
public void init() throws ServletException {
// se recuperan los parámetros de inicialización del servlet
ServletConfig config = getServletConfig();
// se procesan los demás parámetros de inicialización
String valeur = null;
for (int i = 0; i < paramètres.length; i++) {
// valor del parámetro
valeur = config.getInitParameter(paramètres[i]);
// ¿Existe el parámetro?
if (valeur == null) {
// se registra el error
erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
} else {
// se almacena el valor del parámetro
params.put(paramètres[i], valeur);
}
}
// la URL de la vista [erreurs] tiene un tratamiento especial
urlErreurs = config.getInitParameter("urlErreurs");
if (urlErreurs == null)
throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
// Configuración de la aplicación
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// capa de servicio
service = (IService) ctx.getBean("service");
// se vacía la base de datos
clean();
// se rellena
try {
fill();
} catch (ParseException e) {
throw new ServletException(e);
}
}
// rellenar tabla
public void fill() throws ParseException {
// creación de personas
Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// que se guardan
service.saveArray(new Personne[] { p1, p2 });
}
// eliminación de elementos de la tabla
public void clean() {
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
// GET
@SuppressWarnings("unchecked")
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
...
}
// visualización de la lista de personas
private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// modificación/adición de una persona
private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// eliminación de una persona
private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// Validación de la modificación o la incorporación de una persona
public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// visualización del formulario prellenado
private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
...
}
// envío
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// se pasa el control a GET
doGet(request, response);
}
}
- líneas 21-34: se recuperan los parámetros esperados del archivo [web.xml].
- líneas 37-39: el parámetro [urlErreurs] debe estar presente obligatoriamente, ya que indica la URL de la vista [erreurs] capaz de mostrar los posibles errores de inicialización. Si no existe, se interrumpe la aplicación ejecutando una [ServletException] (línea 39). Esta excepción se transmitirá al servidor web y será gestionada por la etiqueta <error-page> del archivo [web.xml]. Por lo tanto, se muestra la vista [exception.jsp]:

El enlace [Retour à la liste] anterior no funciona. Al utilizarlo se obtiene la misma respuesta mientras la aplicación no se haya modificado y vuelto a cargar. Resulta útil para otros tipos de excepciones, como ya hemos visto.
- Líneas 40-43: utilizan el archivo de configuración de Spring para obtener una referencia a la capa [service]. Tras la inicialización del controlador, sus métodos disponen de una referencia [service] en la capa [service] (línea 15) que utilizarán para ejecutar las acciones solicitadas por el usuario. Estas serán interceptadas por el método [doGet], que las remitirá a un método específico del controlador:
Url | Método HTTP | método del controlador |
GET | doListPersonnes | |
GET | doEditPersonne | |
POST | doValidatePersonne | |
GET | doDeletePersonne |
El método [doGet]
El objetivo de este método es dirigir el procesamiento de las acciones solicitadas por el usuario hacia el método adecuado. Su código es el siguiente:
// GET
@SuppressWarnings("unchecked")
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// se comprueba cómo ha ido la inicialización del servlet
if (erreursInitialisation.size() != 0) {
// se pasa el control a la página de errores
request.setAttribute("erreurs", erreursInitialisation);
getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
// fin
return;
}
// se recupera el método de envío de la solicitud
String méthode = request.getMethod().toLowerCase();
// se recupera la acción que se debe ejecutar
String action = request.getPathInfo();
// ¿acción?
if (action == null) {
action = "/list";
}
// ejecución de la acción
if (méthode.equals("get") && action.equals("/list")) {
// lista de personas
doListPersonnes(request, response);
return;
}
if (méthode.equals("get") && action.equals("/delete")) {
// eliminación de una persona
doDeletePersonne(request, response);
return;
}
if (méthode.equals("get") && action.equals("/edit")) {
// visualización del formulario de alta/modificación de una persona
doEditPersonne(request, response);
return;
}
if (méthode.equals("post") && action.equals("/validate")) {
// validación del formulario de alta/modificación de una persona
doValidatePersonne(request, response);
return;
}
// otros casos
doListPersonnes(request, response);
}
- líneas 7-13: se comprueba que la lista de errores de inicialización esté vacía. Si no es así, se muestra la vista [erreurs(erreurs)], que indicará el error o los errores.
- línea 15: se recupera el método [get] o [post] que el cliente ha utilizado para realizar su solicitud.
- línea 17: se recupera el valor del parámetro [action] de la consulta.
- líneas 23-27: procesamiento de la consulta [GET /do/list], que solicita la lista de personas.
- líneas 28-32: procesamiento de la solicitud [GET /do/delete], que solicita la eliminación de una persona.
- líneas 33-37: procesamiento de la solicitud [GET /do/edit], que solicita el formulario de actualización de una persona.
- líneas 38-42: procesamiento de la solicitud [POST /do/validate], que solicita la validación de la persona actualizada.
- línea 44: si la acción solicitada no es ninguna de las cinco anteriores, se procede como si se tratara de [GET /do/list].
El método [doListPersonnes]
Este método procesa la solicitud [GET /do/list], que solicita la lista de personas:

Su código es el siguiente:
// Visualización de la lista de personas
private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// la plantilla de la vista [list]
request.setAttribute("personnes", service.getAll());
// Visualización de la vista [list]
getServletContext().getRequestDispatcher((String) params.get("urlList")).forward(request, response);
}
- línea 4: se solicita a la capa [service] la lista de personas del grupo y se introduce dicha lista en la plantilla bajo la clave «personas».
- línea 6: se muestra la vista [list.jsp] descrita en el apartado 3.4.3.2.
El método [doDeletePersonne]
Este método procesa la consulta [GET /do/delete?id=XX], que solicita la eliminación de la persona con id=XX. La URL [/do/delete?id=XX] es la de los enlaces [Supprimer] de la vista [list.jsp]:

cuyo código es el siguiente:
...
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
...
<c:forEach var="personne" items="${personnes}">
<tr>
...
<td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
<td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
</tr>
</c:forEach>
</table>
<br>
<a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
</body>
</html>
En la línea 12, se ve la URL [/do/delete?id=XX] del enlace [Supprimer]. El método [doDeletePersonne], que debe procesar esta URL, debe eliminar a la persona con id=XX y, a continuación, mostrar la nueva lista de personas del grupo. Su código es el siguiente:
// eliminación de una persona
private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// se recupera el ID de la persona
int id = Integer.parseInt(request.getParameter("id"));
// se elimina a la persona
service.deleteOne(id);
// redirección a la lista de personas
response.sendRedirect("list");
}
- línea 4: la URL procesada tiene el formato [/do/delete?id=XX]. Se recupera el valor [XX] del parámetro [id].
- línea 6: se solicita a la capa [service] la eliminación de la persona con el ID obtenido. No realizamos ninguna comprobación. Si la persona que se pretende eliminar no existe, la capa [dao] lanza una excepción que la capa [service] deja pasar. Tampoco la gestionamos aquí, en el controlador. Por lo tanto, se propagará hasta el servidor web, que, según su configuración, mostrará la página [exception.jsp], descrita en el apartado 3.4.3.2:

- línea 9: si se ha producido la eliminación (sin excepciones), se solicita al cliente que se redirija a la URL relativa [list]. Dado que la que se acaba de procesar es [/do/delete], la URL de redirección será [/do/list]. Por lo tanto, el navegador se verá obligado a realizar una [GET /do/list], lo que provocará que se muestre la lista de personas.
El método [doEditPersonne]
Este método procesa la solicitud [GET /do/edit?id=XX], que solicita el formulario de actualización de la persona con id=XX. La URL [/do/edit?id=XX] es la de los enlaces [Modifier] y la del enlace [Ajout] de la vista [list.jsp]:

cuyo código es el siguiente:
...
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
...
<c:forEach var="personne" items="${personnes}">
<tr>
...
<td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
<td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
</tr>
</c:forEach>
</table>
<br>
<a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
</body>
</html>
En la línea 11, se ve la URL [/do/edit?id=XX] del enlace [Modifier] y, en la línea 17, la URL [/do/edit?id=-1] del enlace [Ajout]. El método [doEditPersonne] debe mostrar el formulario de edición de la persona con id=XX o, si se trata de una nueva entrada, mostrar un formulario vacío.
![]() |
- En el método [1] anterior, el formulario de alta, y en el método [2], el formulario de modificación.
El código del método [doEditPersonne] es el siguiente:
// modificación o alta de una persona
private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// se recupera el ID de la persona
int id = Integer.parseInt(request.getParameter("id"));
// ¿Añadir o modificar?
Personne personne = null;
if (id != -1) {
// modificación: se recupera la persona que se va a modificar
personne = service.getOne(id);
request.setAttribute("id", personne.getId());
request.setAttribute("version", personne.getVersion());
} else {
// adición: se crea una persona vacía
personne = new Personne();
request.setAttribute("id", -1);
request.setAttribute("version", -1);
}
// se añade el objeto [Personne] a la sesión del usuario
request.getSession().setAttribute("personne", personne);
// y en la plantilla de la vista [edit]
request.setAttribute("erreurEdit", "");
request.setAttribute("prenom", personne.getPrenom());
request.setAttribute("nom", personne.getNom());
Date dateNaissance = personne.getDatenaissance();
if (dateNaissance != null) {
request.setAttribute("datenaissance", new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance));
} else {
request.setAttribute("datenaissance", "");
}
request.setAttribute("marie", personne.isMarie());
request.setAttribute("nbenfants", personne.getNbenfants());
// Visualización de la vista [edit]
getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
}
- el GET tiene como destino una URL del tipo [/do/edit?id=XX]. En la línea 4, recuperamos el valor de [id]. A continuación, hay dos casos:
- el id es distinto de -1. En ese caso, se trata de una modificación y hay que mostrar un formulario prellenado con los datos de la persona que se va a modificar. En la línea 9, se solicita esta persona a la capa [service].
- Si id es igual a -1, se trata de una alta y hay que mostrar un formulario vacío. Para ello, se crea una persona vacía en la línea 14.
- En ambos casos, se inicializan los elementos [id, version] de la plantilla de la página [edit.jsp] descrita en el apartado 3.4.3.2.
- El objeto [Personne] obtenido se coloca en la plantilla de la página [edit.jsp]. Esta plantilla incluye los siguientes elementos: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, datenaissance, erreurDateNaissance, marie, nbenfants, erreurNbEnfants]. Estos elementos se inicializan en las líneas 19-31, a excepción de aquellos cuyo valor es la cadena vacía [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. Se sabe que, en caso de que no estén presentes en la plantilla, la biblioteca JSTL mostrará una cadena vacía como su valor. Aunque el elemento [erreurEdit] también tiene como valor una cadena vacía, se inicializa de todos modos porque se realiza una comprobación de su valor en la página [edit.jsp].
- Una vez que el modelo está listo, el control pasa a la página [edit.jsp], línea 33, que generará la vista [edit].
El método [doValidatePersonne]
Este método procesa la solicitud [POST /do/validate] que valida el formulario de actualización. Esta solicitud POST se activa mediante el botón [Valider]:

Recordemos los campos de entrada del formulario HTML de la vista anterior:
<form method="post" action="<c:url value="/do/validate"/>">
...
<input type="text" value="${nom}" name="nom" size="20">
...
<input type="text" value="${datenaissance}" name="datenaissance">
...
<c:choose>
<c:when test="${marie}">
<input type="radio" name="marie" value="true" checked>Oui
<input type="radio" name="marie" value="false">Non
</c:when>
<c:otherwise>
<input type="radio" name="marie" value="true">Oui
<input type="radio" name="marie" value="false" checked>Non
</c:otherwise>
</c:choose>
...
<input type="text" value="${nbenfants}" name="nbenfants">
...
<input type="hidden" value="${id}" name="id">
<input type="hidden" value="${version}" name="version">
<input type="submit" value="Valider">
<a href="<c:url value="/do/list"/>">Annuler</a>
</form>
La consulta POST contiene los parámetros [prenom, nom, datenaissance, marie, nbenfants, id] y se envía a la URL [/do/validate] (línea 1). Se procesa mediante el siguiente método [doValidatePersonne]:
// validación de la modificación o alta de una persona
public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// se recuperan los elementos enviados
boolean formulaireErroné = false;
boolean erreur;
// el nombre
String prenom = request.getParameter("prenom").trim();
// ¿Es válido el nombre?
if (prenom.length() == 0) {
// se anota el error
request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
formulaireErroné = true;
}
// el apellido
String nom = request.getParameter("nom").trim();
// ¿Es válido el nombre?
if (nom.length() == 0) {
// se anota el error
request.setAttribute("erreurNom", "Le nom est obligatoire");
formulaireErroné = true;
}
// fecha de nacimiento
Date datenaissance = null;
try {
datenaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request.getParameter("datenaissance").trim());
} catch (ParseException e) {
// se señala el error
request.setAttribute("erreurDateNaissance", "Date incorrecte");
formulaireErroné = true;
}
// estado civil
boolean marie = Boolean.parseBoolean(request.getParameter("marie").trim());
// Número de hijos
int nbenfants = 0;
erreur = false;
try {
nbenfants = Integer.parseInt(request.getParameter("nbenfants").trim());
if (nbenfants < 0) {
erreur = true;
}
} catch (NumberFormatException ex) {
// se señala el error
erreur = true;
}
// ¿Número de hijos incorrecto?
if (erreur) {
// se señala el error
request.setAttribute("erreurNbEnfants", "Nombre d'enfants incorrect");
formulaireErroné = true;
}
// identificador de la persona
int id = Integer.parseInt(request.getParameter("id"));
// ¿Hay algún error en el formulario?
if (formulaireErroné) {
// Se vuelve a mostrar el formulario con los mensajes de error
showFormulaire(request, response, "");
// fin
return;
}
// El formulario es correcto: se actualiza la persona que se ha guardado en la sesión
// con la información enviada por el cliente
Personne personne = (Personne)request.getSession().getAttribute("personne");
personne.setDatenaissance(datenaissance);
personne.setMarie(marie);
personne.setNbenfants(nbenfants);
personne.setNom(nom);
personne.setPrenom(prenom);
// persistencia
try {
if (id == -1) {
// creación
service.saveOne(personne);
} else {
// actualización
service.updateOne(personne);
}
} catch (DaoException ex) {
// se vuelve a mostrar el formulario con el mensaje del error producido
showFormulaire(request, response, ex.getMessage());
// fin
return;
}
// se redirige a la lista de personas
response.sendRedirect("list");
}
// Visualización del formulario prellenado
private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
// Se prepara la plantilla de la vista [edit]
request.setAttribute("erreurEdit", erreurEdit);
request.setAttribute("id", request.getParameter("id"));
request.setAttribute("version", request.getParameter("version"));
request.setAttribute("prenom", request.getParameter("prenom").trim());
request.setAttribute("nom", request.getParameter("nom").trim());
request.setAttribute("datenaissance", request.getParameter("datenaissance").trim());
request.setAttribute("marie", request.getParameter("marie"));
request.setAttribute("nbenfants", request.getParameter("nbenfants").trim());
// Visualización de la vista [edit]
getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
}
- líneas 7-13: se recupera el parámetro [prenom] de la solicitud POST y se comprueba su validez. Si resulta incorrecto, el elemento [erreurPrenom] se inicializa con un mensaje de error y se incluye en los atributos de la consulta.
- líneas 15-21: se procede de forma similar con el parámetro [nom]
- líneas 23-30: se procede de forma similar con el parámetro [datenaissance]
- línea 32: se recupera el parámetro [marie]. No se comprueba su validez porque, a priori, procede del valor de un botón de opción. Dicho esto, nada impide que un programa genere un [POST /.../do/validate] acompañado de un parámetro [marie] inventado. Por lo tanto, deberíamos comprobar la validez de este parámetro. En este caso, nos basamos en nuestra gestión de excepciones, que provoca que se muestre la página [exception.jsp] si el controlador no las gestiona por sí mismo. Así pues, si la conversión del parámetro [marie] a valor booleano falla en la línea 32, se generará una excepción que dará lugar al envío de la página [exception.jsp] al cliente. Este funcionamiento nos conviene.
- líneas 34-50: se recupera el parámetro [nbenfants] y se comprueba su valor.
- línea 52: se recupera el parámetro [id] sin comprobar su valor
- líneas 54-59: si el formulario contiene errores, se vuelve a mostrar con los mensajes de error generados anteriormente
- líneas 62-67: si es válido, se crea un nuevo objeto [Personne] con los elementos del formulario
- líneas 69-82: se guarda el usuario. El proceso de guardado puede fallar. En un entorno multiusuario, es posible que el usuario que se va a modificar haya sido eliminado o que ya lo haya modificado otra persona. En ese caso, la capa [dao] lanzará una excepción que se gestiona aquí.
- línea 84: si no se ha producido ninguna excepción, se redirige al cliente a la URL [/do/list] para mostrarle el nuevo estado del grupo.
- línea 79: si se ha producido una excepción al guardar, se vuelve a solicitar la visualización del formulario inicial pasándole el mensaje de error de la excepción (tercer parámetro).
El método [showFormulaire] (líneas 88-97) genera la plantilla necesaria para la página [edit.jsp] con los valores introducidos (request.getParameter(" ... ")). Recordemos que los mensajes de error ya han sido insertados en la plantilla mediante el método [doValidatePersonne]. La página [edit.jsp] se muestra en la línea 99.
3.4.4. Las pruebas de la aplicación web
En el apartado 3.4.1 se presentaron varias pruebas. Invitamos al lector a volver a realizarlas. A continuación mostramos otras capturas de pantalla que ilustran los casos de conflictos de acceso a los datos en un entorno multiusuario:
[Firefox] será el navegador del usuario U1. Este solicita la URL [http://localhost:8080/spring-jpa-hibernate-personnes-crud/do/list]:

[IE7] será el navegador del usuario U2. Este solicita la misma URL:

El usuario U1 accede a la edición de la persona [p2]:

El usuario U2 hace lo mismo:

El usuario U1 realiza modificaciones y las valida:
![]() |
El usuario U2 hace lo mismo:
![]() |
El usuario U2 vuelve a la lista de personas mediante el enlace [Retour à la liste] del formulario:

Encuentra a la persona [Lemarchand] tal y como la modificó U1 (casada, 2 hijos). El número de versión de p2 ha cambiado. Ahora U2 elimina [p2]:
![]() |
U1 sigue teniendo su propia lista y quiere modificar [p2] de nuevo:
![]() |
U1 utiliza el enlace [Retour à la liste] para ver de qué se trata:

Descubre que, efectivamente, [p2] ya no forma parte de la lista...
3.4.5. Versión 2
Modificamos ligeramente la versión anterior para utilizar los archivos de las capas [service, dao, jpa] en lugar de sus códigos fuente:
![]() |
- en [1]: el nuevo proyecto de Eclipse. Cabe destacar la desaparición de los paquetes [service, dao, entites]. Estos se han encapsulado en el archivo [service-dao-jpa-personne.jar] [2], ubicado en [WEB-INF/lib].
- La carpeta del proyecto se encuentra en [4]. La importaremos.
No hay nada más que hacer. Cuando se inicia la nueva aplicación web y se solicita la lista de personas, se recibe la siguiente respuesta:
![]() |
Hibernate no encuentra la entidad [Personne]. Para resolver este problema, hay que declarar explícitamente en [persistence.xml] las entidades gestionadas:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<class>entites.Personne</class>
</persistence-unit>
</persistence>
- línea 7: se declara la entidad Personne.
Una vez hecho esto, la excepción desaparece:
![]() |
3.4.6. Cambiar la implementación JPA
![]() |
- por [1]: el nuevo proyecto de Eclipse
- en [2]: las bibliotecas Toplink han sustituido a las bibliotecas Hibernate
- La carpeta del proyecto es [4]. La importaremos.
Cambiar la implementación JPA solo implica algunos cambios en el archivo [spring-config.xml]. No cambia nada más. Los cambios introducidos en el archivo [spring-config.xml] se han explicado en el apartado 3.1.9:
<?xml version="1.0" encoding="UTF-8"?>
<!-- JVM debe ejecutarse con el argumento -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar
(à remplacer par le chemin exact de spring-agent.jar)-->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
...
<property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
...
</bean>
...
</beans>
Solo hay que modificar unas pocas líneas para pasar de Hibernate a Toplink:
- línea 11: la implementación JPA ahora la realiza Toplink
- línea 13: la propiedad [databasePlatform] tiene un valor diferente al de Hibernate: el nombre de una clase propia de Toplink. En el apartado 2.1.15.2 se explica dónde encontrar este nombre.
Eso es todo. Cabe destacar la facilidad con la que se puede cambiar de SGBD o de la implementación JPA con Spring. Sin embargo, aún no hemos terminado del todo. Al ejecutar la aplicación, se produce una excepción:
![]() |
Se trata de un problema ya descrito en el apartado 3.1.9. Se resuelve ejecutando JVM con un agente de Spring. Para ello, se modifica la configuración de ejecución de Tomcat:
![]() |
- por [1]: se ha elegido la opción [Run / Run...] para modificar la configuración de Tomcat
- a [2]: hemos seleccionado la pestaña [Arguments]
- en [3]: se ha añadido el parámetro -javaagent tal y como se describe en el apartado 3.1.9.
Una vez hecho esto, se puede solicitar la lista de personas:

3.5. Otros ejemplos
Nos hubiera gustado mostrar un ejemplo web en el que el contenedor Spring se sustituyera por el contenedor JBoss EJB3 analizado en el apartado 3.2:
![]() |
- en [1]: el proyecto de Eclipse
- en [3]: su ubicación en la carpeta de ejemplos. Lo importaremos.
Hemos retomado la configuración [jboss-config.xml, persistence.xml] descrita en el apartado 3.2 y, a continuación, hemos modificado el método [init] del controlador [Application.java] de la siguiente manera:
// init
@SuppressWarnings("unchecked")
public void init() throws ServletException {
try {
// se recuperan los parámetros de inicialización del servlet
ServletConfig config = getServletConfig();
// se procesan los demás parámetros de inicialización
String valeur = null;
for (int i = 0; i < paramètres.length; i++) {
// valor del parámetro
valeur = config.getInitParameter(paramètres[i]);
// ¿Existe el parámetro?
if (valeur == null) {
// se registra el error
erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
} else {
// se almacena el valor del parámetro
params.put(paramètres[i], valeur);
}
}
// la URL de la vista [erreurs] tiene un tratamiento especial
urlErreurs = config.getInitParameter("urlErreurs");
if (urlErreurs == null)
throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
// Configuración de la aplicación
// se inicia el contenedor EJB3 JBoss
// se utilizan los archivos de configuración ejb3-interceptors-aop.xml y embedded-jboss-beans.xml
EJB3StandaloneBootstrap.boot(null);
// Creación de los beans específicos de la aplicación
EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
// Se implementan todos los EJB encontrados en la ruta de clases de la aplicación
//EJB3StandaloneBootstrap.scanClasspath("WEB-INF/classes".replace("/", File.separator));
EJB3StandaloneBootstrap.scanClasspath();
// Se inicializa el contexto JNDI. Se procesa el archivo jndi.properties
InitialContext initialContext = new InitialContext();
// instanciación de la capa de servicio
service = (IService) initialContext.lookup("Service/local");
// Se vacía la base de datos
clean();
// Se rellena
fill();
} catch (Exception e) {
throw new ServletException(e);
}
}
- líneas 28-38: se inicia el contenedor Ejb3. Este sustituye al contenedor Spring.
- línea 41: se solicita una referencia a la capa [service] de la aplicación.
A priori, estas son las únicas modificaciones que hay que realizar. Al ejecutarlo, aparece el siguiente error:
![]() |
No he sido capaz de entender dónde estaba exactamente el problema. La excepción notificada por Tomcat parece indicar que se solicitó el objeto denominado «TransactionManager» al servicio JNDI y que este no lo reconocía. Dejo a los lectores la tarea de encontrar una solución a este problema. Si se encuentra una solución, se incorporará al documento.





























































