18. 案例研究:Struts 2 / Tiles / Spring / Hibernate / MySQL
我们将通过一个案例研究来结束对 Struts 的学习。为了更贴近实际,本案例将比之前研究的案例复杂得多。对于初学者来说,在着手本案例研究之前,最好先通过个人项目深入掌握 Struts 2 的基础知识。
该应用程序将采用分层架构:
![]() |
[metier]、[dao]、[jpa/hibernate]这三个层将以JAR压缩包的形式提供,我们将详细介绍其功能。 各层的集成将由 Spring 负责。[web] 层将由 Struts 2 实现。
![]() |
18.1. 问题
我们计划开发一个Web应用程序,用于为某市镇“幼儿之家”雇佣的保育员制作工资单。

本案例研究详见文档:
《Java入门》EE 5,可通过以下网址获取:[http://tahe.developpez.com/java/javaee]
在该文档中,该案例采用以下多层架构进行实现:
![]() |
[web] 层是利用 JSF 框架(Java Server Faces)实现的。我们将采用相同的架构,使用 Struts 2 来实现 [web] 层。 为了展示分层架构的优势,我们将使用 JSF 版本的 [metier, dao, jpa] 层 jar 包,并将其连接到 [web / struts2] 层:
![]() |
我们将介绍[metier, dao, jpa]层的以下内容:
- [web] 层面向 [métier] 层的接口。我们将介绍该接口。
- [jpa] 层访问数据库。我们将对此进行介绍。
- [jpa] 层将数据库表中的行转换为 JPA 实体,这些实体由应用程序的所有层进行操作。我们将介绍它们。
- [metier, dao, jpa] 层由 Spring 进行实例化。我们将介绍实现该实例化和集成的配置文件。
18.2. 数据库
我们将使用以下数据库:

- 在 [1] 中,该数据库包含三个表:
- [employes]:记录托儿所员工的表
- [cotisations]:记录社会保险缴费率的表
- [indemnites]:用于记录计算员工工资所需信息的表
表 [employes]
![]() |
- [2] 为员工表,[3] 为其字段说明
该表的内容可能如下:
![]()
表 [cotisations]
![]() |
- 在 [4] 中,是缴费表;而在 [5] 中,是其字段的含义
该表的内容可能如下:
![]()
表 [indemnites]
![]() |
- 在 [6] 中,是津贴表;而在 [7] 中,是其字段的含义
该表的内容可能如下:
![]()
将数据库结构导出到文件 SQL 后,结果如下:
18.3. JPA 实体
在以下架构中
![]() |
[Jpa]层在[dao]层处理的对象与Jdbc驱动程序处理的数据库表行之间起着桥梁作用。从数据库中读取的表行被转换为称为Jpa实体的对象。 反之,在写入操作中,JPA 实体会被转换为表中的行。这些实体由所有层(尤其是 Web 层)进行操作。因此,我们需要了解它们:
实体 [Employe] 代表表 [Employes] 中的一个行

ID:自增型主键VERSION:记录的版本号PRENOM:员工的名字NOM:员工的姓氏ADRESSE:员工的地址CP:邮政编码VILLE:所在城市INDEMNITE_ID:指向 INDEMNITES(ID)
实体 [Employe] 如下:
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()
+"]";
}
// 获取器和设置器
...
}
我们将忽略针对 [Jpa] 层级的 @ 注释。该类的各个字段对应于 [EMPLOYES] 表中的不同列。 indemnites 字段(第 29 行)反映了 [EMPLOYES] 表对 [INDEMNITES] 表具有外键。当操作某位员工时,其津贴信息也会被同步操作。
实体 [Indemnite] 是表 [INDEMNITES] 中某一行对象的表达式:

ID 自增型主键VERSIONn° 记录的版本号BASE_HEURE 每小时值班的欧元成本ENTRETIEN_JOUR 每天值班的欧元津贴REPAS_JOUR 每天值班的欧元餐费津贴INDEMNITES_CP 带薪休假津贴。这是一个应用于基本工资的百分比。
实体 [Indemnite] 如下:
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()
+ "]";
}
// 获取器和设置器
....
}
该类的各个字段对应于表 [INDEMNITES] 的不同列。
实体 [Cotisation] 是表 [COTISATIONS] 中某一行对应的表达式:

ID自增型主键VERSION记录版本号SECU (百分比)社会保险缴费率RETRAITE养老保险缴费率CSGD可抵扣的通用社会缴费率CSGRD通用社会缴费率及社会债务偿还缴费率
实体 [Cotisation] 如下:
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()+"]";
}
// 获取器和设置器
...
}
该类别的各个字段对应于表 [INDEMNITES] 的不同列。
18.4. 保育员工资的计算方式
我们将编写的Web应用程序将允许我们根据三项信息计算员工的工资:
- 员工的指数
- 工作天数
- 工作小时数
以下是薪资计算的屏幕截图:

下面介绍家庭保姆月薪的计算方法。此方法并非实际使用的计算方式。我们以玛丽·朱维纳尔女士为例,她在当月工作了20天,共计150小时。
计算中考虑了以下要素: | | |
幼儿园助理的 由以下公式给出 : | | |
需从该工资中扣除一定数额的社会保险费 需从该 : | | |
社会保险费总额: | | |
此外,保育员有权 每工作一天,可获得 生活津贴以及 。因此,她将获得 如下: | | |
最终,应支付给保育员的净工资如下: | |
18.5. [métier] 层的接口
让我们回顾一下我们正在构建的应用程序架构:
![]() |
[web / struts 2] 层与 [métier] 层的接口进行通信。该接口如下:
package metier;
import java.util.List;
import jpa.Employe;
public interface IMetier {
// 获取工资单
FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
// 员工列表
List<Employe> findAllEmployes();
}
- 第 8 行:用于计算员工工资的方法
- 第 10 行:用于填充员工下拉列表的方法
方法 calculerFeuillesalaire 返回以下 [FeuilleSalaire] 类的实例:
package metier;
import java.io.Serializable;
import jpa.Cotisation;
import jpa.Employe;
public class FeuilleSalaire implements Serializable {
// 私有字段
private Employe employe;
private Cotisation cotisation;
private ElementsSalaire elementsSalaire;
// 构造函数
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 + "]";
}
// 获取器和设置器
...
}
工资单包含以下信息:
- 第 10 行:待计算工资的员工信息
- 第 11 行:各种缴费率
- 第 12 行:工资构成要素
类 [ElementsSalaire] 如下所示:
package metier;
import java.io.Serializable;
public class ElementsSalaire implements Serializable{
// 私有字段
private double salaireBase;
private double cotisationsSociales;
private double indemnitesEntretien;
private double indemnitesRepas;
private double salaireNet;
// 构造函数
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 + "]";
}
// getter 和 setter
...
}
- 第8-12行:工资构成
18.6. Spring配置文件
[métier, dao, jpa] 层的集成由以下 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">
<!-- 应用层 -->
<!-- 业务 -->
<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" />
<!-- 配置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>
<!-- 数据源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>
<!-- 事务管理器 -->
<tx:annotation-driven transaction-manager="txManager" />
<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<!-- 异常转换 -->
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<!-- 持久化 -->
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
</beans>
我们不会在此解释该配置。它是实例化和集成[métier, dao, jpa]层所必需的。 因此,将基于这些层的Web应用程序必须采用此配置。请注意,第32至37行配置了数据库的JDBC属性。若读者希望更换数据库,则需修改这些行。
有关此配置的更多信息,请参阅文档《Java EE 5 入门》,该文档可通过网址 [http://tahe.developpez.com/java/javaee] 获取。








