Skip to content

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

我们将通过一个案例研究来结束对 Struts 的学习。为了更贴近实际,本案例将比之前研究的案例复杂得多。对于初学者来说,在着手本案例研究之前,最好先通过个人项目深入掌握 Struts 2 的基础知识。

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

[metier]、[dao]、[jpa/hibernate]这三个层将以JAR压缩包的形式提供,我们将详细介绍其功能。 各层的集成将由 Spring 负责。[web] 层将由 Struts 2 实现。

18.1. 问题

我们计划开发一个Web应用程序,用于为某市镇“幼儿之家”雇佣的保育员制作工资单。

Image

本案例研究详见文档:

《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. 数据库

我们将使用以下数据库:

Image

 
  • 在 [1] 中,该数据库包含三个表:
  • [employes]:记录托儿所员工的表
  • [cotisations]:记录社会保险缴费率的表
  • [indemnites]:用于记录计算员工工资所需信息的表

表 [employes]

  • [2] 为员工表,[3] 为其字段说明

该表的内容可能如下:

Image

表 [cotisations]

  • 在 [4] 中,是缴费表;而在 [5] 中,是其字段的含义

该表的内容可能如下:

Image

表 [indemnites]

  • 在 [6] 中,是津贴表;而在 [7] 中,是其字段的含义

该表的内容可能如下:

Image

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

#
# `cotisations` 表的结构:
#

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;

#
# `indemnites` 表的结构:
#

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;

#
# `employes` 表的结构:
#

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 层)进行操作。因此,我们需要了解它们:

实体 [Employe] 代表表 [Employes] 中的一个行

Image

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] 中某一行对象的表达式:

Image

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] 中某一行对应的表达式:

Image

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应用程序将允许我们根据三项信息计算员工的工资:

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

以下是薪资计算的屏幕截图:

Image

下面介绍家庭保姆月薪的计算方法。此方法并非实际使用的计算方式。我们以玛丽·朱维纳尔女士为例,她在当月工作了20天,共计150小时。

计算中考虑了以下要素:

[TOTALHEURES]: total des heures travaillées dans le mois

[TOTALJOURS]: total des jours travaillés dans le mois
[TOTALHEURES]=150
[TOTALJOURS]= 20
幼儿园助理的
由以下公式给出

[SALAIREBASE]=([TOTALHEURES]*[BASEHEURE])*(1+[INDEMNITESCP]/100)
[SALAIREBASE]=(150*[2.1])*(1+0.15)= 362,25
需从该工资中扣除一定数额的社会保险费
需从该

Contribution sociale généralisée et contribution au remboursement de la dette sociale : [SALAIREBASE]*[CSGRDS/100]

Contribution sociale généralisée déductible : [SALAIREBASE]*[CSGD/100]

Sécurité sociale, veuvage, vieillesse : [SALAIREBASE]*[SECU/100]

Retraite Complémentaire + AGPF + Assurance Chômage : [SALAIREBASE]*[RETRAITE/100]
CSGRDS : 12,64
CSGD : 22,28
Sécurité sociale : 34,02
Retraite : 28,55
社会保险费总额:

[COTISATIONSSOCIALES]=[SALAIREBASE]*(CSGRDS+CSGD+SECU+RETRAITE)/100
[COTISATIONSSOCIALES]=97,48
此外,保育员有权
每工作一天,可获得
生活津贴以及
。因此,她将获得
如下:

[Indemnités]=[TOTALJOURS]*(ENTRETIENJOUR+REPASJOUR)
[INDEMNITES]=104
最终,应支付给保育员的净工资如下:

[SALAIREBASE]-[COTISATIONSSOCIALES]+[INDEMNITÉS]
[salaire NET]=368,77

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] 获取。