Skip to content

18. 案例研究:Struts 2 / Tiles / Spring / Hibernate / MySQL

我们将通过一个案例研究来结束对 Struts 的介绍。为了更贴近实际,我们将探讨的示例将比之前介绍的那些复杂得多。对于初学者来说,在着手这个案例研究之前,最好先通过个人项目巩固对 Struts 2 基础知识的理解。

该应用程序将采用分层架构:

[业务]、[DAO] 和 [JPA/Hibernate] 层将以 JAR 归档的形式提供给我们,我们将详细介绍其功能。Spring 将负责这些层的集成。而 [Web] 层将使用 Struts 2 进行实现。

18.1. 问题

我们计划开发一个网络应用程序,用于为受雇于某市“幼儿中心”的保育员生成工资单。

Image

本案例研究详见以下文档:

《Java EE 5 入门》一书,可访问网址 [http://tahe.developpez.com/java/javaee]

在本文档中,该案例研究采用以下多层架构实现:

[Web] 层采用 JSF(Java Server Faces)框架实现。我们将采用相同的架构,使用 Struts 2 实现 [Web] 层。为了展示分层架构的优势,我们将使用 JSF 版本中包含 [业务、DAO、JPA] 层的 JAR 归档文件,并将其连接到 [Web / Struts 2] 层:

我们将介绍[业务、DAO、JPA]层的以下组件:

  • [Web]层与[业务]层进行交互。我们将展示这一交互接口。
  • [JPA]层用于访问数据库。我们将对此进行介绍。
  • [JPA] 层将数据库表中的行转换为 JPA 实体,这些实体被应用程序的所有层所使用。我们将介绍它们。
  • [业务、DAO、JPA] 层由 Spring 进行实例化。我们将展示执行此实例化和集成配置的配置文件。

18.2. 数据库

我们将使用以下 MySQL 数据库 [dbpam_hibernate]:

Image

 
  • 在 [1] 中,该数据库包含三个表:
  • [employees]:记录日托中心员工的表
  • [contributions]:用于存储社会保险缴费率的表
  • [indemnites]:用于存储计算员工薪酬所需信息的表

[employees] 表

  • 在 [2] 中是 employees 表,在 [3] 中是其字段的含义

该表的内容可能如下所示:

Image

表 [contributions]

  • 在 [4] 中,是贡献表;而在 [5] 中,则是该表各字段的含义

该表格的内容可能如下所示:

Image

表 [indemnites]

  • 在 [6] 中,即津贴表,以及在 [7] 中,其字段的含义

该表格的内容可能如下:

Image

将数据库结构导出到 SQL 文件后,结果如下:

#
# Structure for the `cotisations` table :
#

CREATE TABLE `cotisations` (
  `ID` bigint(20) NOT NULL auto_increment,
  `SECU` double NOT NULL,
  `RETRAITE` double NOT NULL,
  `CSGD` double NOT NULL,
  `CSGRDS` double NOT NULL,
  `VERSION` int(11) NOT NULL,
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;

#
# Structure for the `indemnites` table :
#

CREATE TABLE `indemnites` (
  `ID` bigint(20) NOT NULL auto_increment,
  `ENTRETIEN_JOUR` double NOT NULL,
  `REPAS_JOUR` double NOT NULL,
  `INDICE` int(11) NOT NULL,
  `INDEMNITES_CP` double NOT NULL,
  `BASE_HEURE` double NOT NULL,
  `VERSION` int(11) NOT NULL,
  PRIMARY KEY  (`ID`),
  UNIQUE KEY `INDICE` (`INDICE`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;

#
# Structure for the `employes` table :
#

CREATE TABLE `employes` (
  `ID` bigint(20) NOT NULL auto_increment,
  `PRENOM` varchar(20) NOT NULL,
  `SS` varchar(15) NOT NULL,
  `ADRESSE` varchar(50) NOT NULL,
  `CP` varchar(5) NOT NULL,
  `VILLE` varchar(30) NOT NULL,
  `NOM` varchar(30) NOT NULL,
  `VERSION` int(11) NOT NULL,
  `INDEMNITE_ID` bigint(20) NOT NULL,
  PRIMARY KEY  (`ID`),
  UNIQUE KEY `SS` (`SS`),
  KEY `FK_EMPLOYES_INDEMNITE_ID` (`INDEMNITE_ID`),
  CONSTRAINT `FK_EMPLOYES_INDEMNITE_ID` FOREIGN KEY (`INDEMNITE_ID`) REFERENCES `indemnites` (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=latin1;

18.3. JPA 实体

在以下架构中

[Jpa] 层充当了 [dao] 层处理的对象与 Jdbc 驱动程序处理的数据库表中的行之间的桥梁。从数据库表中读取的行会被转换为称为 JPA 实体的对象。反之,在写入过程中,JPA 实体会被转换为表行。这些实体由所有层处理,尤其是 Web 层。因此,我们需要了解它们:

[Employee] 实体代表 [Employees] 表中的一行

ID:主键,类型为自动递增VERSION:记录版本号FIRST_NAME:员工名LAST_NAME:员工姓ADDRESS:员工地址ZIP:员工邮政编码CITY:员工所在城市INDEMNITY_ID:指向INDEMNITIES表(ID)的外键

Image

[Employee] 实体如下:


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()
    +"]";
  }
 
  // getters and setters
   ...  
}

我们将忽略针对 [Jpa] 层的 @ 注解。该类的各个字段对应 [EMPLOYEES] 表的各个列。indemnities 字段(第 29 行)反映了 [EMPLOYEES] 表与 [INDEMNITIES] 表之间存在外键关系这一事实。当我们操作一名员工时,也会同时操作其相关赔偿金。

[Indemnite] 实体是 [INDEMNITIES] 表中某一行数据的对象表示:

ID:自增主键VERSION:记录版本号BASE_HEURE:每小时待命值班的费用(单位:欧元)DAILY_MAINTENANCE:每日津贴(单位:欧元)MEAL_DAY:每日护理餐费津贴(单位:欧元)PAID_LEAVE_ALLOWANCES:带薪休假津贴。此为应用于基本工资的百分比。

Image

[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()
    + "]";
  }
 
  // getters and setters
....  
}

该类的各个字段反映了 [INDEMNITIES] 表的各个列。

[Cotisation] 实体是 [COTISATIONS] 表中某一行在对象层面的表示:

ID:主键,类型为自动递增VERSION:记录版本号SECU:社会保险缴费率(百分比)socialPENSION:养老金缴费率CSGD:一般社会保险缴费率Generalized Deductible:一般社会保险免赔额CSGRDS:社会债务偿还缴费率

Image

[Contribution] 实体的结构如下:


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()+"]";
  }
 
  // getters and setters
  ...
}

该类的各个字段对应 [INDEMNITES] 表中的不同列。

18.4. 基于...的方法来计算保育员的薪资

我们将编写一个 Web 应用程序,该应用程序允许我们根据三项信息计算员工的薪资:

  • 员工的指数
  • 工作天数
  • 工作小时数

以下是薪资计算的截图:

Image

接下来我们将介绍计算保育员月薪的方法。此方法并非用于实际应用。作为示例,我们将以玛丽·朱维纳尔女士的薪资为例,她在该薪资周期内工作了20天,共计150小时。

计算时考虑了以下因素:

[TOTALHOURS]:当月总工作小时数

[TOTALDAYS]:当月总工作日数
[TOTALHOURS]=150
[TOTALDAYS] = 20
由以下公式计算
[BASESALARY]=([TOTALHOURS]*[HOURLYRATE])*(1+[CPALLOWANCE]/100)
[BASESALARY]=(150*[2.1])*(1+0.15)= 362.25
需从该基本工资中扣除一定数额的社会保险费
必须从该基本工资中扣除

一般社会保险费及偿还社会债务的缴费:[BASESALARY]*[CSGRDS/100]

应扣缴的一般社会保险费:[BASESALARY]*[CSGD/100]

社会保障、遗属抚恤金、养老金:[基本工资]*[社会保障/100]

补充养老金 + AGPF + 失业保险:[基本工资]*[养老金/100]
CSGRDS:12.64
CSGD:22.28
社会保障:34.02
养老金:28.55
社会保障缴费总额:
[SOCIALCONTRIBUTIONS] = [BASESALARY] * (CSGRDS + CSGD + SECU + RETIREMENT) / 100
[SOCIALCONTRIBUTIONS]=97.48
此外,保育员有权
每日
以及一笔餐费补贴
。因此,她获得的
[津贴] = [总天数] * (每日生活费 + 每日餐费)
[津贴总额]=104
最终,支付给保育员的净薪资如下:
[基本工资] - [社会保险费] + [津贴]
[净薪]=368.77

18.5. [业务]层接口

让我们来看看我们正在构建的应用程序的架构:

[Web / Struts 2] 层与 [业务] 层的接口进行通信。具体如下:


package metier;
 
import java.util.List;
import jpa.Employe;
 
public interface IMetier {
  // get your payslip
  FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
  // list of employees
  List<Employe> findAllEmployes();
}
  • 第 8 行:用于计算员工工资的方法
  • 第 10 行:用于填充员工下拉列表的方法

calculatePaystub 方法返回以下 [Paystub] 类的实例:


package metier;
 
import java.io.Serializable;
import jpa.Cotisation;
import jpa.Employe;
 
public class FeuilleSalaire implements Serializable {
  // private fields
 
  private Employe employe;
  private Cotisation cotisation;
  private ElementsSalaire elementsSalaire;
 
  // manufacturers
  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 and setters
  ...
}

工资单包含以下信息:

  • 第10行:关于正在计算薪资的员工的信息
  • 第11行:各项缴费率
  • 第12行:工资构成

[ElementsSalaire] 类如下:


package metier;
 
import java.io.Serializable;
 
public class ElementsSalaire implements Serializable{
 
  // private fields
  private double salaireBase;
  private double cotisationsSociales;
  private double indemnitesEntretien;
  private double indemnitesRepas;
  private double salaireNet;
 
  // manufacturers
  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 and setters
  ...
}
  • 第 8-12 行:薪资构成

18.6. Spring 配置文件

[业务、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">
 
  <!-- application layers -->
 
<!-- business -->
  <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" />
 
  <!-- configuration 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>
 
  <!-- the DBCP data source -->
  <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>
 
  <!-- transaction manager -->
  <tx:annotation-driven transaction-manager="txManager" />
  <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
  </bean>
 
  <!-- translation of exceptions -->
  <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
 
  <!-- persistence -->
  <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
 
</beans>

我们不会尝试解释此配置。它是 [业务、DAO、JPA] 层实例化和集成的必要条件。因此,依赖于这些层的 Web 应用程序必须采用此配置。请注意,第 32– –37 行配置了数据库的 JDBC 属性。希望更改数据库的读者必须修改这些行。

有关此配置的更多信息,请参阅文档《Java EE 5 入门》,该文档可通过以下网址获取:[http://tahe.developpez.com/java/javaee]。