18. Caso práctico: Struts 2 / Tiles / Spring / Hibernate / MySQL
Vamos a terminar nuestro aprendizaje de Struts con un caso práctico. Para que sea realista, el ejemplo que vamos a estudiar será bastante más complejo que los anteriores. Para los principiantes, sin duda es mejor profundizar en los fundamentos de Struts 2 con aplicaciones personales antes de abordar este caso práctico.
La aplicación utilizará una arquitectura por capas:
![]() |
El conjunto de capas [metier], [dao], [jpa/hibernate] se nos proporcionará en forma de un archivo jar cuyas funcionalidades detallaremos. La integración de las capas correrá a cargo de Spring. La capa [web] se implementará mediante Struts 2.
![]() |
18.1. El problema
Nos proponemos escribir una aplicación web que permita elaborar la nómina de las cuidadoras infantiles empleadas por la «Casa de la Primera Infancia» de un municipio.

Este caso práctico se presenta en el documento:
Introducción a Java EE 5, disponible en la URL [http://tahe.developpez.com/java/javaee]
En este documento, el caso práctico se implementa con la siguiente arquitectura multicapa:
![]() |
La capa [web] se implementa utilizando el marco JSF (Java Server Faces). Vamos a adoptar esta misma arquitectura al implementar la capa [web] con Struts 2. Para mostrar las ventajas de las arquitecturas por capas, utilizaremos el archivo jar de las capas [metier, dao, jpa] de la versión JSF y lo conectaremos a una capa [web / struts2]:
![]() |
Presentaremos los siguientes elementos de las capas [metier, dao, jpa]:
- la capa [web] se dirige a la interfaz de la capa [métier]. Presentaremos esta interfaz.
- La capa [jpa] accede a una base de datos. La presentaremos.
- La capa [jpa] transforma las filas de las tablas de la base de datos en entidades JPA manipuladas por todas las capas de la aplicación. Las presentaremos.
- Las capas [metier, dao, jpa] son instanciadas por Spring. Presentaremos el archivo de configuración que realiza esta instanciación e integración.
18.2. La base de datos
Utilizaremos la siguiente base de datos MySQL [dbpam_hibernate]:

- En [1], la base de datos tiene tres tablas:
- [employes]: una tabla que registra a las empleadas de una guardería
- [cotisations]: una tabla que registra las tasas de cotización a la seguridad social
- [indemnites]: una tabla que registra la información necesaria para calcular la nómina de las empleadas
Tabla [employes]
![]() |
- en [2], la tabla de empleados, y en [3], el significado de sus campos
El contenido de la tabla podría ser el siguiente:
![]()
Tabla [cotisations]
![]() |
- en [4], la tabla de cotizaciones y en [5], el significado de sus campos
El contenido de la tabla podría ser el siguiente:
![]()
Tabla [indemnites]
![]() |
- en [6], la tabla de indemnizaciones y en [7], el significado de sus campos
El contenido de la tabla podría ser el siguiente:
![]()
La exportación de la estructura de la base de datos a un archivo SQL da el siguiente resultado:
18.3. Las entidades JPA
En la siguiente arquitectura
![]() |
La capa [Jpa] actúa como puente entre los objetos manipulados por la capa [dao] y las filas de las tablas de la base de datos manipuladas por el controlador Jdbc. Las filas de las tablas leídas en la base de datos se transforman en objetos denominados entidades Jpa. A la inversa, en la escritura, las entidades JPA se transforman en filas de las tablas. Estas entidades son manipuladas por todas las capas y, en particular, por la capa web. Por lo tanto, debemos conocerlas:
La entidad [Employe] representa una fila de la tabla [Employes]

ID: clave primaria de tipo autoincremento; VERSION: número de versión del registro; PRENOM: nombre de pila de la empleada; NOM: su apellido; ADRESSE: su dirección; CP: su código postal; VILLE: su ciudad; INDEMNITE_ID: clave externa en INDEMNITES(ID)
La entidad [Employe] es la siguiente:
package jpa;
...
@Entity
@Table(name="EMPLOYES")
public class Employe implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
@Column(name="VERSION",nullable=false)
private int version;
@Column(name="SS", nullable=false, unique=true, length=15)
private String SS;
@Column(name="NOM", nullable=false, length=30)
private String nom;
@Column(name="PRENOM", nullable=false, length=20)
private String prenom;
@Column(name="ADRESSE", nullable=false, length=50)
private String adresse;
@Column(name="VILLE", nullable=false, length=30)
private String ville;
@Column(name="CP", nullable=false, length=5)
private String codePostal;
@ManyToOne
@JoinColumn(name="INDEMNITE_ID",nullable=false)
private Indemnite indemnite;
public Employe() {
}
public Employe(String SS, String nom, String prenom, String adresse, String ville, String codePostal, Indemnite indemnite){
setSS(SS);
setNom(nom);
setPrenom(prenom);
setAdresse(adresse);
setVille(ville);
setCodePostal(codePostal);
setIndemnite(indemnite);
}
@Override
public String toString() {
return "jpa.Employe[id=" + getId()
+ ",version="+getVersion()
+",SS="+getSS()
+ ",nom="+getNom()
+ ",prenom="+getPrenom()
+ ",adresse="+getAdresse()
+",ville="+getVille()
+",code postal="+getCodePostal()
+",indice="+getIndemnite().getIndice()
+"]";
}
// getter y setter
...
}
Se ignorarán las anotaciones @ destinadas a la capa [Jpa]. Los diferentes campos de la clase reflejan las diferentes columnas de la tabla [EMPLOYES]. El campo indemnites (línea 29) refleja el hecho de que la tabla [EMPLOYES] tiene una clave externa en la tabla [INDEMNITES]. Cuando se manipula un empleado, también se manipulan sus indemnizaciones.
La entidad [Indemnite] es la expresión objeto de una línea de la tabla [INDEMNITES]:

IDclave primaria de tipo autoincrementoVERSIÓNn° de versión del registro BASE_HEURE: coste en euros de una hora de guardia ENTRETIEN_JOUR: indemnización en euros por día de guardia REPAS_JOUR: indemnización por comida en euros por día de guardia INDEMNITES_CP: indemnizaciones por vacaciones pagadas. Se trata de un porcentaje que se aplica al salario base.
La entidad [Indemnite] es la siguiente:
package jpa;
...
@Entity
@Table(name="INDEMNITES")
public class Indemnite implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
@Column(name="VERSION",nullable=false)
private int version;
@Column(name="INDICE", nullable=false,unique=true)
private int indice;
@Column(name="BASE_HEURE",nullable=false)
private double baseHeure;
@Column(name="ENTRETIEN_JOUR",nullable=false)
private double entretienJour;
@Column(name="REPAS_JOUR",nullable=false)
private double repasJour;
@Column(name="INDEMNITES_CP",nullable=false)
private double indemnitesCP;
public Indemnite() {
}
public Indemnite(int indice, double baseHeure, double entretienJour, double repasJour, double indemnitesCP){
setIndice(indice);
setBaseHeure(baseHeure);
setEntretienJour(entretienJour);
setRepasJour(repasJour);
setIndemnitesCP(indemnitesCP);
}
@Override
public String toString() {
return "jpa.Indemnite[id=" + getId()
+ ",version="+getVersion()
+",indice="+getIndice()
+",base heure="+getBaseHeure()
+",entretien jour"+getEntretienJour()
+",repas jour="+getRepasJour()
+",indemnités CP="+getIndemnitesCP()
+ "]";
}
// getter y setter
....
}
Los distintos campos de la clase reflejan las distintas columnas de la tabla [INDEMNITES].
La entidad [Cotisation] es la expresión objeto de una línea de la tabla [COTISATIONS]:

IDclave primaria de tipo autoincrementoVERSIÓNnúmero de versión del registroSECUporcentaje (porcentaje) de cotización a la Seguridad Social; RETRAITE: tasa de cotización para la jubilación; CSGD: tasa de cotización para la contribución social generalizada deducible; CSGRD: tasa de cotización para la contribución social generalizada y la contribución al reembolso de la deuda social
La entidad [Cotisation] es la siguiente:
package jpa;
...
@Entity
@Table(name="COTISATIONS")
public class Cotisation implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
@Column(name="VERSION",nullable=false)
private int version;
@Column(name="CSGRDS",nullable=false)
private double csgrds;
@Column(name="CSGD",nullable=false)
private double csgd;
@Column(name="SECU",nullable=false)
private double secu;
@Column(name="RETRAITE",nullable=false)
private double retraite;
public Cotisation() {
}
public Cotisation(double csgrds, double csgd, double secu, double retraite){
setCsgrds(csgrds);
setCsgd(csgd);
setSecu(secu);
setRetraite(retraite);
}
@Override
public String toString() {
return "jpa.Cotisation[id=" + getId() + ",version=" + getVersion()+",csgrds="+getCsgrds()+"" +
",csgd="+getCsgd()+",secu="+getSecu()+",retraite="+getRetraite()+"]";
}
// getter y setter
...
}
Los distintos campos de la clase reflejan las distintas columnas de la tabla [INDEMNITES].
18.4. Método de cálculo del salario de una cuidadora infantil
La aplicación web que vamos a escribir nos permitirá calcular el salario de una empleada a partir de tres datos:
- el índice de la empleada
- el número de días trabajados
- el número de horas trabajadas
Aquí tienes una captura de pantalla del cálculo de un salario:

A continuación, presentamos el método de cálculo del salario mensual de una cuidadora infantil. No pretende ser el que se utiliza en la realidad. Tomamos como ejemplo el salario de la Sra. Marie Jouveinal, que trabajó 150 horas durante 20 días en el mes a pagar.
Se tienen en cuenta los siguientes elementos: | | |
El salario base de la asistente de guardería viene dado por la fórmula siguiente: | | |
Se deben deducir una serie de cotizaciones sociales deben deducirse de este salario base: | | |
Total de cotizaciones sociales: | | |
Además, la cuidadora tiene derecho, por cada día trabajado, a una indemnización de manutención, así como a una indemnización por comidas. En este concepto, recibe las indemnizaciones siguientes: | | |
En definitiva, el salario neto a pagar a la cuidadora es el siguiente: | |
18.5. La interfaz de la capa [métier]
Volvamos a la arquitectura de la aplicación que estamos construyendo:
![]() |
La capa [web / struts 2] se comunica con la interfaz de la capa [métier]. Esta es la siguiente:
package metier;
import java.util.List;
import jpa.Employe;
public interface IMetier {
// obtener la nómina
FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
// lista de empleados
List<Employe> findAllEmployes();
}
- línea 8: el método que nos permitirá calcular el salario de un empleado
- línea 10: el método que nos permitirá rellenar el combo de empleados
El método calculerFeuillesalaire devuelve una instancia de la clase [FeuilleSalaire] siguiente:
package metier;
import java.io.Serializable;
import jpa.Cotisation;
import jpa.Employe;
public class FeuilleSalaire implements Serializable {
// campos privados
private Employe employe;
private Cotisation cotisation;
private ElementsSalaire elementsSalaire;
// constructores
public FeuilleSalaire() {
}
public FeuilleSalaire(Employe employe, Cotisation cotisation,
ElementsSalaire elementsSalaire) {
setEmploye(employe);
setCotisation(cotisation);
setElementsSalaire(elementsSalaire);
}
// toString
@Override
public String toString() {
return "[" + employe + "," + cotisation + ","
+ elementsSalaire + "]";
}
// getters y setters
...
}
La hoja de nómina contiene la siguiente información:
- línea 10: información sobre el empleado cuyo salario se calcula
- línea 11: las diferentes tasas de cotización
- línea 12: elementos del salario
La clase [ElementsSalaire] es la siguiente:
package metier;
import java.io.Serializable;
public class ElementsSalaire implements Serializable{
// campos privados
private double salaireBase;
private double cotisationsSociales;
private double indemnitesEntretien;
private double indemnitesRepas;
private double salaireNet;
// constructores
public ElementsSalaire() {
}
public ElementsSalaire(double salaireBase, double cotisationsSociales,
double indemnitesEntretien, double indemnitesRepas,
double salaireNet) {
setSalaireBase(salaireBase);
setCotisationsSociales(cotisationsSociales);
setIndemnitesEntretien(indemnitesEntretien);
setIndemnitesRepas(indemnitesRepas);
setSalaireNet(salaireNet);
}
// toString
@Override
public String toString() {
return "[salaire base=" + salaireBase + ",cotisations sociales=" + cotisationsSociales + ",indemnités d'entretien="
+ indemnitesEntretien + ",indemnités de repas=" + indemnitesRepas + ",salaire net="
+ salaireNet + "]";
}
// getters y setters
...
}
- líneas 8-12: los componentes del salario
18.6. El archivo de configuración de Spring
La integración de las capas [métier, dao, jpa] se realiza mediante el siguiente archivo de configuración de Spring:
<?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 -->
<!-- dominio de negocio -->
<bean id="metier" class="metier.Metier">
<property name="employeDao" ref="employeDao"/>
<property name="cotisationDao" ref="cotisationDao"/>
</bean>
<!-- DAO -->
<bean id="employeDao" class="dao.EmployeDao" />
<bean id="indemniteDao" class="dao.IndemniteDao" />
<bean id="cotisationDao" class="dao.CotisationDao" />
<!-- configuración 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="databasePlatform" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
</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/dbpam_hibernate" />
<property name="username" value="root" />
<property name="password" value="" />
</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>
No vamos a intentar explicar esta configuración. Es necesaria para la instanciación y la integración de las capas [métier, dao, jpa]. Por lo tanto, nuestra aplicación web, que se basará en estas capas, deberá adoptar esta configuración. Cabe señalar que las líneas 32-37 configuran las características Jdbc de la base de datos. El lector que desee cambiar de base de datos deberá modificar estas líneas.
Para obtener más información sobre esta configuración, consulte el documento Introducción a Java EE 5, disponible en la URL [http://tahe.developpez.com/java/javaee].








