19. 案例研究 – 第1版
19.1. 模拟的 [业务] 层
让我们回顾一下我们正在构建的应用程序的架构:
![]() |
我们拥有[业务、DAO、JPA]层的归档文件,并已列出了[Web]层需要了解的这些层中的要素。现在,我们可以使用Struts框架编写[Web]层了。
为了简化开发过程中的应用程序测试,我们将创建一个模拟的业务层,该层将实现 [business] 层的接口。架构将如下所示:
![]() |
我们将基于模拟的[业务]层开发[Web]层。由于架构中不再包含数据库,测试将变得更加简单。 得益于 Spring 框架和接口的使用,在后续阶段将模拟的 [业务] 层替换为实际的 [业务、DAO、JPA] 架构时,不会对 [Web / Struts2] 层的代码产生任何影响。我们即将开发的 [Web / Struts2] 层可以原样使用。
我们将使用的模拟[业务]层如下:
package metier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jpa.Cotisation;
import jpa.Employe;
import jpa.Indemnite;
public class MetierSimule implements IMetier {
// list of employees
private Map<String, Employe> hashEmployes = new HashMap<String, Employe>();
private List<Employe> listEmployes;
// get your payslip
public FeuilleSalaire calculerFeuilleSalaire(String SS,
double nbHeuresTravaillées, int nbJoursTravaillés) {
// we retrieve employee n° SS
Employe e = hashEmployes.get(SS);
// a fictitious payslip is returned
return new FeuilleSalaire(e, new Cotisation(3.49, 6.15, 9.39, 7.88), new ElementsSalaire(100, 100, 100, 100, 100));
}
// list of employees
public List<Employe> findAllEmployes() {
if (listEmployes == null) {
// create a list of two employees
listEmployes = new ArrayList<Employe>();
listEmployes.add(new Employe("254104940426058", "Jouveinal", "Marie", "5 rue des oiseaux", "St Corentin", "49203", new Indemnite(2, 2.1, 2.1, 3.1, 15)));
listEmployes.add(new Employe("260124402111742", "Laverti", "Justine", "La br�lerie", "St Marcel", "49014", new Indemnite(1, 1.93, 2, 3, 12)));
// employee dictionary
for (Employe e : listEmployes) {
hashEmployes.put(e.getSS(), e);
}
}
// we return the list of employees
return listEmployes;
}
}
- 第 11 行:[MetierSimule] 类实现了 [IMetier] 接口,而该接口由实际的 [business] 层实现。
- 第 14 行:一个以 INSEE 编号为索引的员工字典
- 第 15 行:员工列表
- 第 27–39 行:实现 [IMetier] 接口的 findAllEmployees 方法。
- 第 30–33 行:创建包含两名员工的列表
- 第 34–36 行:创建以 INSEE 编号为索引的员工字典
- 第 18–24 行:实现 [IMetier] 接口中的 `calculerSalaire` 方法。此处返回一份虚构的工资单。
19.2. NetBeans 项目
NetBeans 项目结构如下:
![]() |
- 在 [1] 中:
- [applicationContext.xml] 是 Spring 配置文件
- [tiles.xml] 是名为 Tiles 的框架的配置文件
- [web.xml] 是 Web 应用程序的配置文件
- 在 [2] 中:应用程序的不同视图
- 在 [3] 中:
- [messages.properties]:消息文件
- [struts.xml]:Struts 的配置文件
![]() |
- 在 [4] 中:应用程序的源代码。Struts 动作位于 [web.actions] 包中。
- 在 [5] 中:模拟的 [business] 层
- 在 [6] 中:所使用的压缩包。这里是各种工具的压缩包:Spring、Tiles、Struts 2、Struts 2/Spring 集成插件以及 Struts 2/Tiles 集成插件。
- 在 [7] 中:实际的 [业务、DAO、JPA] 层的归档文件。它使我们能够访问 JPA 实体、[IMetier] 接口以及 [PayrollSheet] 和 [PayrollItems] 类。所有这些元素确实都被我们的 [SimulatedBusiness] 类所使用。
19.3. 项目配置
该项目通过以下文件进行配置:
- [web.xml],用于配置 Web 应用程序
- [struts.xml],用于配置 Struts 框架
- [applicationContext.xml],用于配置 Spring 框架
- [tiles.xml],用于配置 Tiles 框架
19.3.1. Web 应用程序配置
[web.xml] 文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="pam_struts_01" 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>Pam</display-name>
<!-- Tiles -->
<context-param>
<param-name> org.apache.tiles.impl.BasicTilesContainer.DEFINITIONS_CONFIG </param-name>
<param-value>/WEB-INF/tiles.xml</param-value>
</context-param>
<listener>
<listener-class>org.apache.struts2.tiles.StrutsTilesListener</listener-class>
</listener>
<!-- Struts 2 -->
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Spring -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
- 第 13–20 行:配置 Struts 2 过滤器——已见
- 第 22–24 行:配置 Spring 监听器——之前已见过
- 第 9–11 行:配置 Tiles 监听器。当 Web 应用程序启动时,将实例化 [org.apache.struts2.tiles.StrutsTilesListener] 类。随后它将使用其配置文件。该配置由第 5–8 行定义。因此,Tiles 配置文件即为 [WEB-INF/tiles.xml] 文件。
最终,当 Struts 应用程序启动时,将实例化三个类:
- 一个用于 Struts 2 过滤器。这是处理 MVC 中“C”(控制器)的组件。
- 另一个用于 Spring 监听器。Spring 将使用 [applicationContext.xml] 文件来实例化应用程序的 [业务、DAO、JPA] 层。与前面的示例一样,Spring 还将实例化一个 [Config] 类,该类将包含应用程序范围的数据。最后,Spring 会将对这个单一 [Config] 实例的引用注入到每个需要它的 Struts 动作中。
- 再看一个 Tiles 监听器。该框架将负责视图管理。我们稍后会回到这一点。
19.3.2. Struts 框架配置
Struts 框架通过以下 [struts.xml] 文件进行配置:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<!-- internationalization -->
<constant name="struts.custom.i18n.resources" value="messages" />
<!-- spring integration -->
<constant name="struts.objectFactory.spring.autoWire" value="name" />
<!-- struts /Tiles actions -->
<package name="default" namespace="/" extends="tiles-default">
<!-- default action -->
<default-action-ref name="index" />
<action name="index">
<result type="redirectAction">
<param name="actionName">Formulaire</param>
<param name="namespace">/</param>
</result>
</action>
<!-- action Form -->
<action name="Formulaire" class="web.actions.Formulaire" method="input">
<result name="success" type="tiles">saisie</result>
<result name="exception" type="tiles">exception</result>
</action>
<!-- action FaireSimulation -->
<action name="FaireSimulation" class="web.actions.Formulaire" method="calculSalaire">
<result name="success" type="tiles">simulation</result>
<result name="exception" type="tiles">exception</result>
<result name="input" type="tiles">saisie</result>
</action>
<!-- action EnregistrerSimulation -->
<action name="EnregistrerSimulation" class="web.actions.Enregistrer" method="execute">
<result name="error" type="tiles">erreur</result>
<result name="simulations" type="tiles">simulations</result>
</action>
<!-- action RetourFormulaire -->
<action name="RetourFormulaire" >
<result type="redirectAction">
<param name="actionName">Formulaire</param>
<param name="namespace">/</param>
</result>
</action>
<!-- action VoirSimulations -->
<action name="VoirSimulations" class="web.actions.Voir">
<result name="success" type="tiles">simulations</result>
</action>
<!-- action RetirerSimulation -->
<action name="SupprimerSimulation" class="web.actions.Supprimer" method="execute">
<result name="erreur" type="tiles">erreur</result>
<result name="simulations" type="tiles">simulations</result>
</action>
<!-- action TerminerSession -->
<action name="TerminerSession" class="web.actions.Terminer" method="execute">
<result name="success" type="redirectAction">
<param name="actionName">Formulaire</param>
<param name="namespace">/</param>
</result>
</action>
</package>
</struts>
我们将随着讲解逐步探讨各种 Struts 动作。目前,请注意以下几点:
- 第 8 行:定义消息文件
- 第 10 行:定义了 Spring Bean 如何注入到 Struts 动作中。注入基于 Bean 的名称。需要由 Spring 初始化的 Struts 动作字段必须与待注入的 Bean 名称相同。
- 第 25 行:定义了 [Form] 动作的“success”导航键对应的视图。我们可以看到,<result> 元素有一个我们不熟悉的 type='tiles' 属性。 我们熟悉的是 redirect 类型,它允许将客户端重定向到一个视图。在此,tiles 类型的视图由 Tiles 框架管理。tiles 类型在 [struts2-tiles-plugin-2.2.3.1.jar] 归档文件中的 [struts-plugin.xml] 文件中定义:
<struts>
<package name="tiles-default" extends="struts-default">
<result-types>
<result-type name="tiles" class="org.apache.struts2.views.tiles.TilesResult"/>
</result-types>
</package>
</struts>
- 第 3–5 行:Tiles 结果类型的定义。
- 第 2 行:该类型定义在 [tiles-default] 包中,该包继承自 [struts-default] 包。
- 第 14 行:定义了 [default] 包,该包将包含应用程序的所有操作。为了利用 Tiles 视图类型的定义,该包继承自 [tiles-default]。
19.3.3. Spring 框架配置
Spring 框架通过以下 [WEB-INF/applicationContext.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">
<!-- application layers -->
<!-- web -->
<bean id="config" class="web.Config" init-method="init">
<property name="metier" ref="metier"/>
</bean>
<!-- business -->
<bean id="metier" class="metier.MetierSimule"/>
</beans>
- 第 13 行:由 [business.SimulatedBusiness] 类实例化的模拟 [business] 层
- 第 9–11 行:配置一个名为 config 的 Bean。与之前学习的示例一样,该 Bean 将用于封装应用程序范围的信息。与该 Bean 关联的类是以下 [Config] 类:
package web;
import java.util.List;
import jpa.Employe;
import metier.IMetier;
public class Config {
// business layer initialized by Spring
private IMetier metier;
// list of employees
private List<Employe> employes;
// errors
private Exception initException;
// manufacturer
public Config() {
}
// spring method for object initialization
public void init() {
// we ask for the list of employees
try {
employes = metier.findAllEmployes();
} catch (Exception ex) {
initException = ex;
}
}
// getters and setters
...
}
让我们回到配置 Bean 的配置:
<bean id="config" class="web.Config" init-method="init">
<property name="metier" ref="metier"/>
</bean>
<!-- métier -->
<bean id="metier" class="metier.Metier">
...
</bean>
在第 2 行,我们可以看到第 5 行的业务 Bean 被注入(ref)到 [Config] 对象中名为 business(name)的字段中。该业务 Bean 是对 [business] 层的引用:
![]() |
为了与 [business] 层交互,[web] 层中的所有 Struts 动作都需要对其进行引用。可以说,对 [business] 层的引用属于应用程序范围的数据。 所有用户的所有请求都需要它。这就是为什么我们将这个引用放在 [Config] 对象中。此外,在第 1 行,config Bean 的配置中包含一个 init-method 属性。该属性指定了 Bean 实例化后要执行的 Bean 方法。这里,我们指定在 [web.Config] 类实例化后,必须执行其 init 方法。该方法如下:
// business layer initialized by Spring
private IMetier metier;
// list of employees
private List<Employe> employes;
// errors
private Exception initException;
// manufacturer
public Config() {
}
// spring method for object initialization
public void init() {
// we ask for the list of employees
try {
employes = metier.findAllEmployes();
} catch (Exception ex) {
initException = ex;
}
}
当 init 方法被执行时,该类的业务字段已由 Spring 实例化。因此,init 方法可以访问业务层接口 [IMetier](第 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 行:该方法检索员工列表
我们可以看到,init 方法从 [business] 层请求员工列表。该列表存储在第 4 行的字段中。如果发生异常,则将其存储在第 6 行的字段中。
综上所述,单个 [Config] 对象包含:
- 对 [business] 层的引用
- 员工列表
19.4. 生成 Tiles 视图
正如我们在 Struts 配置文件中所见,视图将由 Tiles 框架生成。我们将仅解释编写本应用程序所必需的内容。
Tiles 允许您从主页面生成视图。此页面(此处称为 [MasterPage.jsp])将由以下 JSP 片段组合而成:
这些 JSP 片段在 NetBeans 项目中定义如下:

Tiles 框架允许我们定义哪些片段将被插入到主页面中。
主页面 [MasterPage.jsp] 如下所示:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link href="styles.css" rel="stylesheet" type="text/css"/>
<title>
<tiles:insertAttribute name="titre" ignore="true" />
</title>
<s:head/>
</head>
<body background="<s:url value="/ressources/standard.jpg"/>">
<tiles:insertAttribute name="entete" />
<hr/>
<tiles:insertAttribute name="saisie" />
<tiles:insertAttribute name="simulation" />
<tiles:insertAttribute name="exception" />
<tiles:insertAttribute name="erreur" />
<tiles:insertAttribute name="simulations" />
</body>
</html>
主页面是 JSP 片段的容器。在此,它由六个片段组成,即第 17 行至第 23 行中的内容。生成时,主页面中可能包含 0 到 6 个片段。此生成过程由 [WEB-INF/tiles.xml] 文件控制,该文件定义了 Tiles 视图:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE tiles-definitions PUBLIC "-//Apache Software Foundation//DTD Tiles Configuration 2.0//EN"
"http://tiles.apache.org/dtds/tiles-config_2_0.dtd">
<tiles-definitions>
<!-- the master page -->
<definition name="masterPage" template="/MasterPage.jsp">
<put-attribute name="entete" value="/Entete.jsp"/>
<put-attribute name="titre" value="Pam"/>
<put-attribute name="saisie" value=""/>
<put-attribute name="simulation" value=""/>
<put-attribute name="simulations" value=""/>
<put-attribute name="exception" value=""/>
<put-attribute name="erreur" value=""/>
</definition>
<!-- input view -->
<definition name="saisie" extends="masterPage">
<put-attribute name="saisie" value="/Saisie.jsp"/>
</definition>
<!-- simulation view -->
<definition name="simulation" extends="saisie">
<put-attribute name="simulation" value="/Simulation.jsp"/>
</definition>
<!-- the simulations view -->
<definition name="simulations" extends="masterPage">
<put-attribute name="simulations" value="/Simulations.jsp"/>
</definition>
<!-- the exceptional view -->
<definition name="exception" extends="masterPage">
<put-attribute name="exception" value="/Exception.jsp"/>
</definition>
<!-- error view -->
<definition name="erreur" extends="masterPage">
<put-attribute name="erreur" value="/Erreur.jsp"/>
</definition>
</tiles-definitions>
- 上述文件定义了六个名为 masterPage(第 9 行)、input(第 20 行)、simulation(第 25 行)、simulations(第 30 行)、exception(第 35 行)和 error(第 40 行)的 Tiles 视图。
- 第 9–17 行:定义了一个名为 masterPage(name)的视图,并将其与主页面 [MasterPage.jsp](template)关联。 我们看到该 JSP 页面定义了六个子视图。与主页面关联的 Tiles 视图必须指定与这六个子视图各自关联的 JSP 片段。我们可以看到,某些子视图被赋予了空字符串作为其值。这些子视图将不会被包含在主页面 [MasterPage.jsp] 中。因此,名为 masterPage 的 Tiles 视图仅由子片段 [Entete.jsp] 组成。
- 第 20–22 行:定义了一个名为 `saisie` 的视图,该视图继承了之前定义的 `masterPage` 视图。这意味着它继承了 `masterPage` 视图中的所有定义。其定义等同于以下内容:
<definition name="saisie" template="/MasterPage.jsp">
<put-attribute name="entete" value="/Entete.jsp"/>
<put-attribute name="titre" value="Pam"/>
<put-attribute name="saisie" value=""/>
<put-attribute name="simulation" value=""/>
<put-attribute name="simulations" value=""/>
<put-attribute name="exception" value=""/>
<put-attribute name="erreur" value=""/>
<put-attribute name="saisie" value="/Saisie.jsp"/>
</definition>
我们可以看到,它与 JSP 页面 [MasterPage.jsp] 相关联,因此必须定义该页面的六个子视图。我们可以看到第 9 行的定义覆盖了第 4 行的定义。因此,名为“saisie”的 Tiles 视图由 JSP 片段 [Entete.jsp, Saisie.jsp] 组成
若继续沿此思路推导,我们将得到下表:
Tiles视图 | JSP 页面 |
19.5. 消息文件
该应用程序已实现国际化。消息位于 [messages.properties] 和 [Formulaire.properties] 文件中。
[messages.properties] 文件内容如下:
Pam.titre=Calcul du salaire des assistantes maternelles
Pam.Erreurs.titre=Les erreurs suivantes se sont produites :
Pam.Erreurs.classe=Exception
Pam.Erreurs.message=Message
Pam.Erreur.libelle=L''erreur suivante s''est produite
Pam.Saisie.Heures.libell\u00e9=Heures travaill\u00e9es
Pam.Saisie.Jours.libell\u00e9=Jours travaill\u00e9s
Pam.Saisie.employ\u00e9=Employ\u00e9
Pam.BtnSalaire.libell\u00e9=Salaire
Pam.BtnEffacer.libell\u00e9=Effacer
Simulation.Infos.employe=Informations Employ\u00e9
Simulation.Employe.nom=Nom
Simulation.Employe.prenom=Pr\u00e9nom
Simulation.Employe.adresse=Adresse
Simulation.Employe.indice=Indice
Simulation.Employe.ville=Ville
Simulation.Employe.codePostal=Code Postal
Simulation.Infos.cotisations=Cotisations Sociales
Simulation.Cotisations.csgrds=CsgRds
Simulation.Cotisations.csgrds=Csgd
Simulation.Cotisations.retraite=Retraite
Simulation.Cotisations.secu=S\u00e9cu
Form.Infos.indemnites=Indemnit\u00e9s
Simulation.Indemnites.salaireHoraire=Salaire horaire
Simulation.Indemnites.entretienJour=Entretien/Jour
Simulation.Indemnites.repasJour=Repas/Jour
Simulation.Indemnites.cong\u00e9sPay\u00e9s=Cong\u00e9s pay\u00e9s
Simulation.Infos.Salaire=Salaire
Simulation.Salaire.salaireBase=Salaire de base
Simulation.Salaire.cotisationsSociales=Cotisations sociales
Simulation.Salaire.entretien=Indemnit\u00e9s d''entretien
Simulation.Salaire.repas=Indemnit\u00e9s de repas
Simulation.salaireNet=Salaire net
# formats
Format.heure = {0,time}
Format.nombre = {0,number,#0.0##}
Format.pourcent = {0,number,##0.00' %'}
Format.monnaie={0,number,##0.00' \u20ac'}
# liste des simulations
Pam.Simulations.titre=Liste des simulations
Pam.Simulations.num=Num\u00e9ro
Pam.Simulations.nom=Nom
Pam.Simulations.prenom=Pr\u00e9nom
Pam.Simulations.heures=Heures
Pam.Simulations.jours=Jours
Pam.Simulations.salairebase=Salaire de base
Pam.Simulations.indemnites=Indemnites
Pam.Simulations.cotisationsociales=Cotisations
Pam.Simulations.salairenet=Salaire
Pam.SimulationsVides.titre=La liste des simulations est vide
# menu
Menu.FaireSimulation=Faire la simulation
Menu.EffacerSimulation=Effacer la simulation
Menu.VoirSimulations=Voir les simulations
Menu.RetourFormulaire=Retour au formulaire de navigation
Menu.EnregistrerSimulation=Enregistrer la simulation
Menu.TerminerSession=Terminer la session
# msg d'erreur
Erreur.sessionexpiree=La session a expir\u00e9
Erreur.numSimulation=N\u00b0 de simulation incorrect
# erreur de conversion
xwork.default.invalid.fieldvalue=Valeur invalide pour le champ "{0}".
[Form.properties] 文件内容如下:
# pour que les doubles soient au format local
double.format={0,number,#0.00##}
# msg d'erreur
joursTravaill\u00e9s.error=Tapez un nombre entier compris entre 1 et 31
heuresTravaill\u00e9es.error=Tapez un nombre r\u00e9el entre 0 et 300
19.6. 样式表
Tiles 视图使用以下样式表 [styles.css]:
.libelle{
background-color: #ccffff;
font-family: 'Times New Roman',Times,serif;
font-size: 14px;
font-weight: bold;;
padding-right: 5px;
padding-left: 5px;
padding-bottom: 5px;
padding-top: 5px;
}
.info{
background-color: #99cc00;;
padding-right: 5px;
padding-left: 5px;
padding-bottom: 5px;
padding-top: 5px;
}
.titreInfos{
background-color: #ffcc00
}
19.7. 初始视图
为了探索该应用程序,我们将根据用户的各种操作来分析它。对于每项操作,我们将查看执行该操作的 Struts 操作以及作为响应返回的 Tiles 视图。
在 [struts.xml] 中,我们有以下操作:
<!-- action par défaut -->
<default-action-ref name="index" />
<action name="index">
<result type="redirectAction">
<param name="actionName">Formulaire!input</param>
<param name="namespace">/</param>
</result>
</action>
<!-- action Formulaire -->
<action name="Formulaire" class="web.actions.Formulaire">
<result name="success" type="tiles">saisie</result>
<result name="exception" type="tiles">exception</result>
<result name="input" type="tiles">saisie</result>
<result name="simulation" type="tiles">simulation</result>
</action>
- 第 2-8 行:应用程序的默认操作是 [Form!input]。
[Form] 类的定义如下:
package web.actions;
...
public class Formulaire extends ActionSupport implements Preparable, SessionAware {
// configuration initialized by Spring
private Config config;
// list of employees
private List<Employe> employes;
// error list
private List<Erreur> erreurs;
// payslip
private FeuilleSalaire feuilleSalaire;
// foreclosures
private String comboEmployesValue;
private Double heuresTravaillees;
private Integer joursTravailles;
// session
private Map<String, Object> session;
// menu
private Menu menu;
@Override
public void prepare() throws Exception {
...
}
@Override
public String input() {
....
}
// wage calculation
public String calculSalaire() {
...
}
}
@Override
public void validate() {
...
}
@Override
public void setSession(Map<String, Object> map) {
session = map;
}
// getters and setters
...
}
- 第 4 行:[Form] 动作实现了 Preparable 接口。该接口仅包含一个方法,即第 24 行中的 prepare 方法。该方法会在动作中的其他方法执行之前被调用一次。它通常用于初始化动作的模型。
操作模型定义在第 6–21 行:
- 第 7 行:如前所述,config 字段由 Spring 初始化。它提供了对应用范围数据的访问:
- 指向 [business] 层的引用
- 员工列表的引用
- 对 [Config] 对象实例化过程中可能发生的异常的引用
- 第 9 行:员工列表。这将填充 [Saisie.jsp] 片段中的员工下拉列表。
- 第 11 行:错误列表。这将填充 [Error.jsp] 片段。
- 第 21 行:[Entete.jsp] 片段的菜单选项列表
![]() |
在 [1] 中,显示菜单中的链接由 [Form] 操作的 menu 字段控制。
prepare 方法在 input 方法之前执行。具体如下:
@Override
public void prepare() throws Exception {
// configuration error?
Exception initException = config.getInitException();
if (initException != null) {
erreurs = new ArrayList<Erreur>();
Throwable th = initException;
while (th != null) {
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
th = th.getCause();
}
} else {
employes = config.getEmployes();
}
}
- 第 4 行:我们从 Spring 实例化的 [Config] 对象中获取异常
- 第 5 行:如果在 [Config] 对象的实例化过程中发生了异常,则在第 11 行初始化错误列表。[Error] 类定义如下:
package web.entities;
import java.io.Serializable;
public class Erreur implements Serializable{
public Erreur() {
}
// fields
private String classe;
private String message;
// manufacturer
public Erreur(String classe, String message){
this.setClasse(classe);
this.message=message;
}
// getters and setters
...
}
该类用于存储异常堆栈:
- 第 11 行:异常类
- 第 12 行:异常消息
让我们回到 prepare 方法:
- 第 13 行:来自 [Config] 对象的员工列表被存储在操作的 employees 字段中。
执行完 prepare 方法后,接下来将执行 input 方法。具体如下:
@Override
public String input() {
if (erreurs == null) {
// menu
menu = new Menu(true, false, false, true, false, true);
return SUCCESS;
} else {
// menu
menu = new Menu(false, false, false, false, false, false);
return "exception";
}
}
该输入方法仅用于设置要显示的菜单选项列表。[Menu] 类定义如下:
package web.entities;
import java.io.Serializable;
public class Menu implements Serializable {
// menu items
private boolean faireSimulation;
private boolean effacerSimulation;
private boolean enregistrerSimulation;
private boolean voirSimulations;
private boolean retourFormulaire;
private boolean terminerSession;
public Menu() {
}
public Menu(boolean faireSimulation, boolean effacerSimulation, boolean enregistrerSimulation, boolean voirSimulations, boolean retourFormulaire, boolean terminerSession) {
this.faireSimulation = faireSimulation;
this.effacerSimulation = effacerSimulation;
this.enregistrerSimulation = enregistrerSimulation;
this.voirSimulations = voirSimulations;
this.retourFormulaire = retourFormulaire;
this.terminerSession = terminerSession;
}
// getters and setters
...
}
- 第 8–13 行:菜单中共有 6 个可能的链接
- 第18–25行:该类的构造函数允许您指定应显示哪些链接以及不应显示哪些链接。
菜单链接显示在 [Entete.jsp] 中,这是一个存在于所有 Tiles 视图中的 JSP 片段。每个操作都将有一个菜单字段,用于控制 [Entete.jsp] 中菜单的显示。
让我们回到输入方法:
@Override
public String input() {
if (erreurs == null) {
// menu
menu = new Menu(true, false, false, true, false, true);
return SUCCESS;
} else {
// menu
menu = new Menu(false, false, false, false, false, false);
return "exception";
}
}
- 第3-6行:如果错误列表为空,则显示菜单 [运行仿真、查看仿真、结束会话],并返回输入键。
- 第 9-10 行:如果错误列表不为空,则菜单将为空,并返回异常键。
让我们回到 [struts.xml] 中 [Form] 动作的配置:
<!-- action Formulaire -->
<action name="Formulaire" class="web.actions.Formulaire">
<result name="success" type="tiles">saisie</result>
<result name="exception" type="tiles">exception</result>
<result name="input" type="tiles">saisie</result>
<result name="simulation" type="tiles">simulation</result>
</action>
- 第 5 行:input 键显示名为 input 的 Tiles 视图
- 第 4 行:exception 键显示名为 exception 的 Tiles 视图
让我们从名为“input”的 Tiles 视图开始。它由 JSP 片段 [Entete.jsp] 和 [Saisie.jsp] 组成。
[Entete.jsp] 片段内容如下:

其代码如下:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<table>
<tr>
<td><h1><s:text name="Pam.titre"/></h1></td>
<td>
<s:if test="menu.faireSimulation">
|<a href="javascript:doSimulation()"><s:text name="Menu.FaireSimulation"/></a><br/>
</s:if>
<s:if test="menu.effacerSimulation">
|<a href="<s:url action="Formulaire!input"/>"><s:text name="Menu.EffacerSimulation"/></a><br/>
</s:if>
<s:if test="menu.voirSimulations">
|<a href="<s:url action="VoirSimulations"/>"><s:text name="Menu.VoirSimulations"/></a><br/>
</s:if>
<s:if test="menu.retourFormulaire">
|<a href="<s:url action="RetourFormulaire"/>"><s:text name="Menu.RetourFormulaire"/></a><br/>
</s:if>
<s:if test="menu.enregistrerSimulation">
|<a href="<s:url action="EnregistrerSimulation"/>"><s:text name="Menu.EnregistrerSimulation"/></a><br/>
</s:if>
<s:if test="menu.terminerSession">
|<a href="<s:url action="TerminerSession"/>"><s:text name="Menu.TerminerSession"/></a><br/>
</s:if>
</td>
</tr>
</table>
- 第 8-25 行:显示六个菜单链接 [运行模拟(第 8-10 行)、清除模拟(第 11-13 行)、查看模拟(第 14–16 行)、返回表单(第 17–19 行)、保存模拟(第 20–22 行)、结束会话(第 23–25 行)]。
- 第 8、11、14、17、20、23 行:链接的显示由当前操作的菜单字段控制。
请注意,[Entete.jsp] 片段显示了一个 HTML 表格(第 4–28 行),但并非完整的 HTML 页面。请记住,应用程序的所有视图都嵌入在以下母版页 [MasterPage.jsp] 中:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link href="styles.css" rel="stylesheet" type="text/css"/>
<title>
<tiles:insertAttribute name="titre" ignore="true" />
</title>
<s:head/>
</head>
<body background="<s:url value="/ressources/standard.jpg"/>">
<tiles:insertAttribute name="entete" />
<hr/>
<tiles:insertAttribute name="saisie" />
<tiles:insertAttribute name="simulation" />
<tiles:insertAttribute name="exception" />
<tiles:insertAttribute name="erreur" />
<tiles:insertAttribute name="simulations" />
</body>
</html>
[Entete.jsp] 片段被插入到第 17 行,位于一个常规的 HTML 页面中。
[Saisie.jsp] 片段插入在第 19 行。以下是生成的视图:

[Saisie.jsp] 片段的代码如下:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<script language="javascript" type="text/javascript">
function doSimulation(){
// mail the form
document.forms['Saisie'].elements['action'].name='action:Formulaire!calculSalaire'
document.forms['Saisie'].submit();
}
</script>
<!-- data entry -->
<s:form name="Saisie" id="Saisie">
<s:select name="comboEmployesValue" list="employes" listKey="SS" listValue="prenom+' ' +nom" key="Pam.Saisie.employé"/>
<s:textfield name="heuresTravaillees" key="Pam.Saisie.Heures.libellé" value="%{#parameters['heuresTravaillees']!=null ? #parameters['heuresTravaillees'] : heuresTravaillees==null ? '' : getText('double.format',{heuresTravaillees})}"/>
<s:textfield name="joursTravailles" key="Pam.Saisie.Jours.libellé" value="%{#parameters['joursTravailles']!=null ? #parameters['joursTravailles'] : joursTravailles==null ? '' : joursTravailles}"/>
<input type="hidden" name="action"/>
</s:form>
- 第 17 行:表单没有 action 属性。默认情况下,action='Form'。
- 第 18 行:显示员工下拉列表。下拉列表的内容(list 属性)由当前操作的 employees 字段提供。选项的 value 属性将是员工的社保号(listKey 属性)。每个选项显示的标签将是员工的姓名(listValue 属性)。 在组合框中选中的员工的社会保障号码将提交至 [Form].comboEmployeesValue 字段(name 属性)。
- 第 19 行:工作小时数输入字段。显示值(value 属性)取自 [Form] 操作中的 heuresTravaillees 字段,格式如下(Form.properties):
double.format={0,number,#0.00##}
该值将发布到 [Form].hoursWorked 字段(名称属性)。
- 第 20 行:用于输入工作天数的字段。显示值(value 属性)是 [Form] 操作中 daysWorked 字段的值。
该值将提交至 [Form].daysWorked 字段(name 属性)。
最终,在启动时若无错误,显示的“磁贴”视图如下:

让我们回到 [Form] 操作的配置:
<!-- action Formulaire -->
<action name="Formulaire" class="web.actions.Formulaire">
<result name="success" type="tiles">saisie</result>
<result name="exception" type="tiles">exception</result>
<result name="input" type="tiles">saisie</result>
<result name="simulation" type="tiles">simulation</result>
</action>
我们已经看到,[Form].input 操作也可以在第 4 行触发 exception 键。在这种情况下,名为 exception 的 Tiles 视图会被显示出来。该视图由 [Header.jsp] 和 [Exception.jsp] 片段组成。我们已经介绍了 [Header.jsp] 片段。[Exception.jsp] 片段如下:

这是应用程序第 2 版的启动页面,此时数据库管理系统 (DBMS) 尚未启动。[Error.jsp] 片段的 JSP 代码如下:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<h2><s:text name="Pam.Erreurs.titre"/></h2>
<table>
<tr class="titreInfos">
<th><s:text name="Pam.Erreurs.classe"/></th>
<th><s:text name="Pam.Erreurs.message"/></th>
</tr>
<s:iterator value="erreurs">
<tr>
<td class="libelle"><s:property value="classe"/></td>
<td class="info"><s:property value="message"/></td>
</tr>
</s:iterator>
</table>
- 第 10–14 行:一个遍历 [Form] 操作中错误的 List<Error> 集合的迭代器。请注意,发生错误时,那里会存储一个异常堆栈。
19.8. 运行模拟
初始视图显示后,您可以通过 [运行模拟] 链接计算薪资。
19.8.1. 验证输入内容
请参考以下操作流程:
![]() |
- 在 [1] 中,一个错误的输入
- 在 [2] 中,发送的响应。
让我们来看看 [struts.xml] 中 [Form] 动作的配置:
<!-- action Formulaire -->
<action name="Formulaire" class="web.actions.Formulaire">
<result name="success" type="tiles">saisie</result>
<result name="exception" type="tiles">exception</result>
<result name="input" type="tiles">saisie</result>
<result name="simulation" type="tiles">simulation</result>
</action>
我们知道,在发生验证错误时,验证拦截器会返回 input 键。因此,系统会返回 Tiles 的输入视图。验证过程确保带有错误的字段会显示相应的错误信息。
[Form] 操作的验证由以下 [Form-validation.xml] 文件处理:
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
<validators>
<field name="heuresTravaillees" >
<field-validator type="required" short-circuit="true">
<message key="heuresTravaillées.error"/>
</field-validator>
<field-validator type="conversion" short-circuit="true">
<message key="heuresTravaillées.error"/>
</field-validator>
<field-validator type="double" short-circuit="true">
<param name="minInclusive">0</param>
<param name="maxInclusive">300</param>
<message key="heuresTravaillées.error"/>
</field-validator>
</field>
<field name="joursTravailles" >
<field-validator type="required" short-circuit="true">
<message key="joursTravaillés.error"/>
</field-validator>
<field-validator type="conversion" short-circuit="true">
<message key="joursTravaillés.error"/>
</field-validator>
<field-validator type="int" short-circuit="true">
<param name="min">0</param>
<param name="max">31</param>
<message key="joursTravaillés.error"/>
</field-validator>
</field>
</validators>
- 第 6–20 行验证 hoursWorked 字段是否为 [0,300] 范围内的实数。
- 第 22–36 行验证 `joursTravaillés` 字段是否为 [0,31] 范围内的整数。
让我们回到 [struts.xml] 中 [Form] 动作的配置:
<!-- action Formulaire -->
<action name="Formulaire" class="web.actions.Formulaire">
...
<result name="input" type="tiles">saisie</result>
</action>
我们知道,如果发生验证错误,验证拦截器会返回“input”键。因此,将返回“Input” Tiles视图。
回顾一下,该视图由 [Header.jsp] 和 [Input.jsp] 两个片段组成,其中 [Header.jsp] 包含标题和选项列表,而 [Input.jsp] 包含输入表单。当发生输入错误时,验证过程会确保错误字段显示错误信息,并展示其错误的值。 [Header.jsp] 片段在验证过程中不发挥任何作用。让我们看看它的代码:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<table>
<tr>
<td><h1><s:text name="Pam.titre"/></h1></td>
<td>
<s:if test="menu.faireSimulation">
|<a href="javascript:doSimulation()"><s:text name="Menu.FaireSimulation"/></a><br/>
</s:if>
<s:if test="menu.effacerSimulation">
|<a href="<s:url action="Formulaire!input"/>"><s:text name="Menu.EffacerSimulation"/></a><br/>
</s:if>
<s:if test="menu.voirSimulations">
|<a href="<s:url action="VoirSimulations"/>"><s:text name="Menu.VoirSimulations"/></a><br/>
</s:if>
<s:if test="menu.retourFormulaire">
|<a href="<s:url action="RetourFormulaire"/>"><s:text name="Menu.RetourFormulaire"/></a><br/>
</s:if>
<s:if test="menu.enregistrerSimulation">
|<a href="<s:url action="EnregistrerSimulation"/>"><s:text name="Menu.EnregistrerSimulation"/></a><br/>
</s:if>
<s:if test="menu.terminerSession">
|<a href="<s:url action="TerminerSession"/>"><s:text name="Menu.TerminerSession"/></a><br/>
</s:if>
</td>
</tr>
</table>
这六个链接由模板的菜单字段配置(第 8、11、14、17、20、23 行)。当发生错误时,该模板不会被操作更新,导致页面没有菜单。为了解决此问题,[Form] 类提供了以下 validate 方法:
package web.actions;
import com.opensymphony.xwork2.ActionSupport;
...
public class Formulaire extends ActionSupport implements Preparable, SessionAware {
...
// menu
private Menu menu;
@Override
public void prepare() throws Exception {
...
}
@Override
public String input() {
...
}
// wage calculation
public String calculSalaire() {
...
}
@Override
public void validate() {
// mistakes?
if (!getFieldErrors().isEmpty()) {
// menu
menu = new Menu(true, false, false, true, false, true);
}
}
// getters and setters
...
}
- 第 27 行:我们知道,当此代码存在时,验证过程会执行 validate 方法。我们借此机会更新第 4 行中的菜单,该菜单属于 [Entete.jsp] 片段模板的一部分。
- 第 29–32 行:如果存在验证错误,则将菜单设置为重新显示 Tiles 输入视图。如果不存在验证错误,则不执行任何操作。随后,`calculSalaire` 方法负责创建待显示的视图模型。
19.8.2. 薪资计算
让我们回到页眉中的 JSP 代码:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<table>
<tr>
<td><h1><s:text name="Pam.titre"/></h1></td>
<td>
<s:if test="menu.faireSimulation">
|<a href="javascript:doSimulation()"><s:text name="Menu.FaireSimulation"/></a><br/>
</s:if>
...
</td>
</tr>
</table>
- 第 9 行:当用户点击 [运行模拟] 链接时,将执行 JavaScript 函数 doSimulation。该函数在 [Saisie.jsp] 片段中定义:
<script language="javascript" type="text/javascript">
function doSimulation(){
// mail the form
document.forms['Saisie'].elements['action'].name='action:Formulaire!calculSalaire'
document.forms['Saisie'].submit();
}
</script>
<!-- data entry -->
<s:form name="Saisie" id="Saisie">
<s:select name="comboEmployesValue" list="employes" listKey="SS" listValue="prenom+' ' +nom" key="Pam.Saisie.employé"/>
<s:textfield name="heuresTravaillees" key="Pam.Saisie.Heures.libellé" value="%{#parameters['heuresTravaillees']!=null ? #parameters['heuresTravaillees'] : heuresTravaillees==null ? '' : getText('double.format',{heuresTravaillees})}"/>
<s:textfield name="joursTravailles" key="Pam.Saisie.Jours.libellé" value="%{#parameters['joursTravailles']!=null ? #parameters['joursTravailles'] : joursTravailles==null ? '' : joursTravailles}"/>
<input type="hidden" name="action"/>
</s:form>
- 第 14 行:一个名为 action 的隐藏字段将被提交至 [Form] 操作。该字段允许我们指定表单提交时要执行的操作和方法。正如我们在前几个示例中看到的,这些可以通过名为 action:Action!method 的参数来指定。该参数的具体值并不重要,关键在于它必须存在。
- 第 2–6 行:当用户点击 [Header.jsp] 片段中的 [Run Simulation] 链接时执行的 JavaScript 函数。
- 第 4 行:我们修改隐藏的 action 字段的 name 属性。确保其符合 Struts 预期的 action:Action!method 格式。
- 第 5 行:提交名为“Saisie de la ligne 5”的表单。因此,将提交以下参数字符串:
SS1:下拉框中选定员工的INSEE编号
heuresTravaillees:工作小时数
daysWorked:工作日数
action:Form!calculateSalary:上述元素将传递至 [Form] 操作,随后该操作的 calculateSalary 方法将被执行。
[Form].calculateSalary 方法如下:
// wage calculation
public String calculSalaire() {
try {
// salary calculation
feuilleSalaire = config.getMetier().calculerFeuilleSalaire(comboEmployesValue, heuresTravaillees, joursTravailles);
// put the simulation in the session
session.put("simulation", new Simulation(0, "" + heuresTravaillees, "" + joursTravailles, feuilleSalaire));
// menu
menu = new Menu(true, true, true, true, false, true);
// finish
return "simulation";
} catch (Throwable th) {
...
}
}
- 第 5 行:向 [business] 层请求薪资计算
- 第 7 行:向用户的会话中添加了一个 Simulation 对象。这是因为后续请求可能需要它。[Simulation] 类如下所示:
package web.entities;
import java.io.Serializable;
import metier.FeuilleSalaire;
public class Simulation implements Serializable{
public Simulation() {
}
// simulation fields
private Integer num;
private FeuilleSalaire feuilleSalaire;
private String heuresTravaillées;
private String joursTravaillés;
// manufacturer
public Simulation(Integer num,String heuresTravaillées, String joursTravaillés, FeuilleSalaire feuilleSalaire){
this.setNum(num);
this.setFeuilleSalaire(feuilleSalaire);
this.setHeuresTravaillées(heuresTravaillées);
this.setJoursTravaillés(joursTravaillés);
}
public double getIndemnites(){
return feuilleSalaire.getElementsSalaire().getIndemnitesEntretien()+ feuilleSalaire.getElementsSalaire().getIndemnitesRepas();
}
// getters and setters
...
}
- 第 12 行:模拟编号。每次保存新的模拟时,该编号会递增。
- 第 13 行:员工的工资单
- 第14行:工作小时数
- 第15行:工作天数
- 第 25 行:getIndemnites 方法返回该员工的总补偿金额
我们将看到,[Simulation] 类是 [Simulations.jsp] 片段的模型,该片段用于显示所有已执行的模拟。
回到 [Form].calculateSalary 方法:
// wage calculation
public String calculSalaire() {
try {
// salary calculation
feuilleSalaire = config.getMetier().calculerFeuilleSalaire(comboEmployesValue, heuresTravaillees, joursTravailles);
// put the simulation in the session
session.put("simulation", new Simulation(0, "" + heuresTravaillees, "" + joursTravailles, feuilleSalaire));
// menu
menu = new Menu(true, true, true, true, false, true);
// finish
return "simulation";
} catch (Throwable th) {
...
}
- 第 9 行:更新菜单
- 第 11 行:返回“模拟”导航键。
返回 [表单] 操作配置:
<!-- action Formulaire -->
<action name="Formulaire" class="web.actions.Formulaire">
...
<result name="simulation" type="tiles">simulation</result>
</action>
第 4 行表明,“simulation”导航键会显示名为“simulation”的 Tiles 视图。该视图由以下 JSP 片段组成:[Header, Input, Simulation]。
渲染后的视图如下:
![]() |
- 在 [1] 中,片段 [Header.jsp]
- 在 [2] 中,片段 [Input.jsp]
- 在 [3] 处,片段 [Simulation.jsp]。请注意,显示的工资单是由 [business] 层渲染的虚构工资单。
前两个片段已经介绍过了。[Simulation.jsp] 片段如下:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<hr/>
<!-- information Employees -->
<span class="titreInfos">
<s:text name="Simulation.Infos.employe"/>
</span>
<br/><br/>
<table>
<!-- line 1 -->
<tr>
<th class="libelle">
<s:text name="Simulation.Employe.nom"/>
</th>
<th class="libelle">
<s:text name="Simulation.Employe.prenom"/>
</th>
<th class="libelle">
<s:text name="Simulation.Employe.adresse"/>
</th>
</tr>
<!-- line 2 -->
<tr>
<td class="info">
<s:property value="feuilleSalaire.employe.nom"/>
</td>
<td class="info">
<s:property value="feuilleSalaire.employe.prenom"/>
</td>
<td class="info">
<s:property value="feuilleSalaire.employe.adresse"/>
</td>
</table>
<table>
<!-- line 1 -->
<tr>
<th class="libelle"><s:text name="Simulation.Employe.ville"/></th>
<th class="libelle">
<s:text name="Simulation.Employe.codePostal"/>
</th>
<th class="libelle">
<s:text name="Simulation.Employe.indice"/>
</th>
</tr>
<!-- line 2 -->
<tr>
<td class="info">
<s:property value="feuilleSalaire.employe.ville"/>
</td>
<td class="info">
<s:property value="feuilleSalaire.employe.codePostal"/>
</td>
<td class="info">
<s:property value="feuilleSalaire.employe.indemnite.indice"/>
</td>
</table>
<!-- information Contributions -->
<br/>
<span class="titreInfos">
<s:text name="Simulation.Infos.cotisations"/>
</span>
<br/><br/>
<table>
<!-- line 1 -->
<tr>
<th class="libelle">
<s:text name="Simulation.Cotisations.csgrds"/>
</th>
<th class="libelle">
<s:text name="Simulation.Cotisations.csgrds"/>
</th>
<th class="libelle">
<s:text name="Simulation.Cotisations.retraite"/>
</th>
<th class="libelle">
<s:text name="Simulation.Cotisations.secu"/>
</th>
</tr>
<!-- line 2 -->
<tr>
<td class="info">
<s:text name="Format.pourcent">
<s:param value="feuilleSalaire.cotisation.csgrds"/>
</s:text>
</td>
<td class="info">
<s:text name="Format.pourcent">
<s:param value="feuilleSalaire.cotisation.csgd"/>
</s:text>
</td>
<td class="info">
<s:text name="Format.pourcent">
<s:param value="feuilleSalaire.cotisation.retraite"/>
</s:text>
</td>
<td class="info">
<s:text name="Format.pourcent">
<s:param value="feuilleSalaire.cotisation.secu"/>
</s:text>
</td>
</table>
<!-- information Indemnities -->
<br/>
<span class="titreInfos">
<s:text name="Form.Infos.indemnites"/>
</span>
<br/><br/>
<table>
<!-- line 1 -->
<tr>
<th class="libelle">
<s:text name="Simulation.Indemnites.salaireHoraire"/>
</th>
<th class="libelle">
<s:text name="Simulation.Indemnites.entretienJour"/>
</th>
<th class="libelle">
<s:text name="Simulation.Indemnites.repasJour"/>
</th>
<th class="libelle">
<s:text name="Simulation.Indemnites.congésPayés"/>
</th>
</tr>
<!-- line 2 -->
<tr>
<td class="info">
<s:text name="Format.monnaie">
<s:param value="feuilleSalaire.employe.indemnite.baseHeure"/>
</s:text>
</td>
</td>
<td class="info">
<s:text name="Format.monnaie">
<s:param value="feuilleSalaire.employe.indemnite.entretienJour"/>
</s:text>
</td>
<td class="info">
<s:text name="Format.monnaie">
<s:param value="feuilleSalaire.employe.indemnite.repasJour"/>
</s:text>
</td>
<td class="info">
<s:text name="Format.monnaie">
<s:param value="feuilleSalaire.employe.indemnite.indemnitesCP"/>
</s:text>
</td>
</tr>
</table>
<!-- salary information -->
<br/>
<span class="titreInfos">
<s:text name="Simulation.Infos.Salaire"/>
</span>
<br/><br/>
<table>
<!-- line 1 -->
<tr>
<th class="libelle">
<s:text name="Simulation.Salaire.salaireBase"/>
</th>
<th class="libelle">
<s:text name="Simulation.Salaire.cotisationsSociales"/>
</th>
<th class="libelle">
<s:text name="Simulation.Salaire.entretien"/>
</th>
<th class="libelle">
<s:text name="Simulation.Salaire.repas"/>
</th>
</tr>
<!-- line 2 -->
<tr>
<td class="info">
<s:text name="Format.monnaie">
<s:param value="feuilleSalaire.elementsSalaire.salaireBase"/>
</s:text>
</td>
<td class="info">
<s:text name="Format.monnaie">
<s:param value="feuilleSalaire.elementsSalaire.cotisationsSociales"/>
</s:text>
</td>
<td class="info">
<s:text name="Format.monnaie">
<s:param value="feuilleSalaire.elementsSalaire.indemnitesEntretien"/>
</s:text>
</td>
<td class="info">
<s:text name="Format.monnaie">
<s:param value="feuilleSalaire.elementsSalaire.indemnitesRepas"/>
</s:text>
</td>
</tr>
</table>
<!-- Net salary-->
<br/>
<table>
<tr>
<td class="libelle">
<s:text name="Simulation.salaireNet"/>
<td></td>
<td class="info">
<s:text name="Format.monnaie">
<s:param value="feuilleSalaire.elementsSalaire.salaireNet"/>
</s:text>
</td>
</tr>
</table>
代码虽然长……但功能很简单。这段代码展示了 [Form].paystub 字段的各种属性,该字段代表员工的工资单。
回到 [Form].calculatePay 方法:
// wage calculation
public String calculSalaire() {
try {
...
return "simulation";
} catch (Throwable th) {
erreurs = new ArrayList<Erreur>();
while (th != null) {
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
th = th.getCause();
}
// menu
menu = new Menu(false, false, false, false, true, true);
return "exception";
}
}
薪资计算可能会出错。例如,如果与数据库管理系统(DBMS)的连接失败,就会出现这种情况。在这种情况下,我们会处理发生的异常。我们在学习 [Form] 方法时已经遇到过这种情况。
- 第 7–11 行:我们根据异常堆栈创建一个 Error 对象列表
- 第13行:设置菜单
- 第14行:设置异常键。
该异常键将显示 Tiles 异常视图:
<!-- action Formulaire -->
<action name="Formulaire" class="web.actions.Formulaire">
<result name="exception" type="tiles">exception</result>
...
</action>
此 Tiles 视图已显示。其外观如下:

19.9. 保存模拟
运行模拟后,用户可能希望将其保存到会话中。
![]() |
![]() |
- 在[1]中,模拟被保存
- 在[2]中,响应界面显示已执行的模拟列表,并将新模拟添加到其中
[保存模拟]链接位于[Entete.jsp]片段中:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<table>
<tr>
<td><h1><s:text name="Pam.titre"/></h1></td>
<td>
...
<s:if test="menu.enregistrerSimulation">
|<a href="<s:url action="EnregistrerSimulation"/>"><s:text name="Menu.EnregistrerSimulation"/></a><br/>
</s:if>
...
</td>
</tr>
</table>
我们可以看到,点击链接会触发 [SaveSimulation] 操作。该操作在 [struts.xml] 文件中的配置如下:
<!-- action EnregistrerSimulation -->
<action name="EnregistrerSimulation" class="web.actions.Enregistrer" method="execute">
<result name="error" type="tiles">erreur</result>
<result name="simulations" type="tiles">simulations</result>
</action>
- 第 1 行:[SaveSimulation] 操作与 [Save] 类及其 execute 方法相关联。
[Save] 类如下所示:
package web.actions;
...
public class Enregistrer extends ActionSupport implements SessionAware {
// session
private Map<String, Object> session;
// menu
private Menu menu;
@Override
public void setSession(Map<String, Object> session) {
this.session = session;
}
// action execution
public String execute() {
// retrieve the last simulation in the session
Simulation simulation = (Simulation) session.get("simulation");
if (simulation == null) {
return ERROR;
}
...
}
// getters and setters
...
}
- 第 4 行:由于该 Action 需要访问会话,因此它实现了 SessionAware 接口。
- 第 7 行:会话
- 第 9 行:菜单
当 [Save] 操作被实例化时,会调用其 execute 方法。请注意,该方法的职责是将最新的模拟结果放入会话中。该模拟结果将被添加到已完成的模拟列表中,该列表同样存储在会话中。
- 第 19 行:我们检索会话中存储的最后一次模拟。
- 第 20–22 行:如果未找到,则会话很可能已过期。实际上,会话仅持续一定时间,该时间可在配置应用程序的 [web.xml] 文件中设置。
- 第 21 行:我们返回错误键。
回到 [SaveSimulation] 操作的配置:
<!-- action EnregistrerSimulation -->
<action name="EnregistrerSimulation" class="web.actions.Enregistrer" method="execute">
<result name="error" type="tiles">erreur</result>
<result name="simulations" type="tiles">simulations</result>
</action>
我们可以看到,“error”键(第3行)触发了名为“error”的 视图的显示。该视图由片段[Entete.jsp]和[Erreur.jsp]组成,外观如下:

[Erreur.jsp] 片段内容如下:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<h2><s:text name="Pam.Erreur.libelle"/></h2>
<h4><s:text name="Erreur.sessionexpiree"/></h4>
回到 [Save].execute 方法:
// action execution
public String execute() {
// retrieve the last simulation in the session
Simulation simulation = (Simulation) session.get("simulation");
if (simulation == null) {
return ERROR;
}
// retrieve the number of the last simulation
Integer numDerniereSimulation = (Integer) session.get("numDerniereSimulation");
if (numDerniereSimulation == null) {
numDerniereSimulation = 0;
}
// increment it
numDerniereSimulation++;
// we give it the new number in the session
session.put("numDerniereSimulation", numDerniereSimulation);
// retrieve the list of simulations
List<Simulation> simulations = (List<Simulation>) session.get("simulations");
if (simulations == null) {
simulations = new ArrayList<Simulation>();
session.put("simulations", simulations);
}
// we add the current simulation
simulation.setNum(numDerniereSimulation);
simulations.add(simulation);
// the list of simulations is displayed
menu = new Menu(false, false, false, false, true, true);
return "simulations";
}
- 第 9–16 行:各种模拟从 1 开始编号。最后分配的编号存储在会话中,键名为 numDerniereSimulation。第 9–16 行的代码检索该键,并递增与其关联的值。
- 第 18–22 行:模拟列表存储在会话中,键名为 simulations。第 18–22 行会检索该列表(若存在)或创建该列表(若不存在)。
- 第 24–25 行:获取模拟列表后,将当前模拟添加到其中(第 25 行)。此前,已为当前模拟分配了一个编号(第 24 行)。
- 第 27 行:设置待显示的菜单
- 第 28 行:返回“simulations”导航键。
返回 [struts.xml] 中 [EnregistrerSimulation] 操作的配置:
<!-- action EnregistrerSimulation -->
<action name="EnregistrerSimulation" class="web.actions.Enregistrer" method="execute">
<result name="error" type="tiles">erreur</result>
<result name="simulations" type="tiles">simulations</result>
</action>
第 4 行:"simulations" 键会触发显示名为 "simulations" 的 Tiles 视图。该视图由 [Entete.jsp] 和 [Simulations.jsp] 片段组成。显示的视图如下:
![]() |
- 在 [1] 处,是 [Entete.jsp] 片段,我们现在已经很熟悉了。
- 在 [2] 处,是片段 [Simulations.jsp]
[Simulations.jsp]片段如下:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!-- empty simulation list -->
<s:if test="#session['simulations']==null || #session['simulations'].size()==0">
<h2><s:text name="Pam.SimulationsVides.titre"/></h2>
</s:if>
<!-- non-empty simulation list -->
<s:if test="#session['simulations'].size()!=0">
<h2><s:text name="Pam.Simulations.titre"/></h2>
<table>
<tr class="titreInfos">
<th><s:text name="Pam.Simulations.num"/></th>
<th><s:text name="Pam.Simulations.nom"/></th>
<th><s:text name="Pam.Simulations.prenom"/></th>
<th><s:text name="Pam.Simulations.heures"/></th>
<th><s:text name="Pam.Simulations.jours"/></th>
<th><s:text name="Pam.Simulations.salairebase"/></th>
<th><s:text name="Pam.Simulations.indemnites"/></th>
<th><s:text name="Pam.Simulations.cotisationsociales"/></th>
<th><s:text name="Pam.Simulations.salairenet"/></th>
</tr>
<s:iterator value="#session['simulations']">
<s:url action="SupprimerSimulation" var="url">
<s:param name="id" value="num"/>
</s:url>
<tr>
<td class="libelle"><s:property value="num"/></td>
<td class="info"><s:property value="feuilleSalaire.employe.nom"/></td>
<td class="info"><s:property value="feuilleSalaire.employe.prenom"/></td>
<td class="info"><s:property value="heuresTravaillées"/></td>
<td class="info"><s:property value="joursTravaillés"/></td>
<td class="info">
<s:text name="Format.monnaie">
<s:param value="feuilleSalaire.elementsSalaire.salaireBase"/>
</s:text>
</td>
<td class="info">
<s:text name="Format.monnaie">
<s:param value="indemnites"/>
</s:text>
</td>
<td class="info">
<s:text name="Format.monnaie">
<s:param value="feuilleSalaire.elementsSalaire.cotisationsSociales"/>
</s:text>
</td>
<td class="info">
<s:text name="Format.monnaie">
<s:param value="feuilleSalaire.elementsSalaire.salaireNet"/>
</s:text>
</td>
<td class="info"><a href="<s:property value="#url"/>">Retirer</a></td>
</tr>
</s:iterator>
</table>
</s:if>
- 第 5-7 行:如果会话中没有模拟,则显示以下视图:

- 第13-21行:显示表格列标题
![]()
- 第 23-55 行:遍历会话中找到的模拟列表
- 第 24-26 行:创建一个名为 url(id 属性)的 URL。该 URL 生成的 HTML 链接如下:
<a href="<a href="view-source:http://localhost:8084/pam/SupprimerSimulation.action?id=1">/pam/SupprimerSimulation.action?id=1</a>">Retirer</a>
我们可以看到,该链接指向 [DeleteSimulation] 操作,并带有一个 id 参数,该参数代表要从列表中删除的模拟的编号。
- 第 28–54 行:在遍历模拟列表的每次迭代中,都会显示当前模拟的属性。

19.10. 删除模拟
用户可能希望从模拟列表中删除某个模拟:
![]() |
![]() |
- 在[1]中,已删除模拟#1
- 在[2]中,已移除模拟#1
[删除] 链接位于我们之前已经查看过的 [Simulations.jsp] 片段中:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!-- empty simulation list -->
<s:if test="#session['simulations']==null || #session['simulations'].size()==0">
<h2><s:text name="Pam.SimulationsVides.titre"/></h2>
</s:if>
<!-- non-empty simulation list -->
<s:if test="#session['simulations'].size()!=0">
<h2><s:text name="Pam.Simulations.titre"/></h2>
<table>
<tr class="titreInfos">
...
</tr>
<s:iterator value="#session['simulations']">
<s:url action="SupprimerSimulation" var="url">
<s:param name="id" value="num"/>
</s:url>
<tr>
...
<td class="info">
<s:text name="Format.monnaie">
<s:param value="feuilleSalaire.elementsSalaire.salaireNet"/>
</s:text>
</td>
<td class="info"><a href="<s:property value="#url"/>">Retirer</a></td>
</tr>
</s:iterator>
</table>
</s:if>
- 第 16-18 行:生成 HTML 链接
<a href="<a href="view-source:http://localhost:8084/pam-01/SupprimerSimulation.action?id=2">/pam-01/SupprimerSimulation.action?id=</a>num">Retirer</a>
其中 num 是要删除的模拟编号。
[DeleteSimulation] 操作在 [struts.xml] 文件中的定义如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<!-- internationalization -->
<constant name="struts.custom.i18n.resources" value="messages" />
<!-- spring integration -->
<constant name="struts.objectFactory.spring.autoWire" value="name" />
<!-- struts /Tiles actions -->
<package name="default" namespace="/" extends="tiles-default">
...
<!-- action RetirerSimulation -->
<action name="SupprimerSimulation" class="web.actions.Supprimer">
<result name="erreur" type="tiles">erreur</result>
<result name="simulations" type="tiles">simulations</result>
</action>
...
</package>
<!-- Add packages here -->
</struts>
- 第 16 行:[SupprimerSimulation] 操作与 [Supprimer] 类相关联。由于未指定具体方法,因此将执行其 execute 方法。[Supprimer] 类的定义如下:
package web.actions;
...
public class Supprimer extends ActionSupport implements SessionAware {
// session
private Map<String, Object> session;
// id of simulation to be deleted
private String id;
// menu
private Menu menu;
@Override
public void setSession(Map<String, Object> session) {
this.session = session;
}
// action execution
public String execute() {
// simulations are retrieved from the
List<Simulation> simulations = (List<Simulation>) session.get("simulations");
if (simulations == null) {
// abnormal case - session must have expired
menu = new Menu(false, false, false, false, true, true);
return "erreur";
}
// id test
int num = 0;
boolean erreur = false;
try {
num = Integer.parseInt(id);
erreur = num <= 0;
} catch (NumberFormatException ex) {
// abnormal
erreur = true;
}
// mistake?
if (erreur) {
menu = new Menu(false, false, false, false, true, true);
return "erreur";
}
// search for the simulation to be deleted
for (int i = 0; i < simulations.size(); i++) {
if (num == simulations.get(i).getNum()) {
simulations.remove(i);
break;
}
}
// the list of simulations is displayed
menu = new Menu(false, false, false, false, true, true);
return "simulations";
}
// getters and setters
...
}
- 第 4 行:[Delete] 操作实现了 [SessionAware] 接口以访问会话。
- 第 7 行:会话
- 第 9 行:待删除的模拟编号。请注意,我们是通过 HTML URL 来实例化 [Delete] 类的:
<a href="<a href="view-source:http://localhost:8084/pam-01/SupprimerSimulation.action?id=2">/pam-01/SupprimerSimulation.action?id=</a>num">Retirer</a>
其中 num 是要删除的模拟的编号。该编号将存储在第 9 行的 id 字段中。
- 第 11 行:响应请求时将显示的视图菜单
- 第 19 行:用于生成请求响应的 `execute` 方法。
- 第 21 行:我们检索该会话中已执行的模拟列表
- 第 22-26 行:若无法从会话中获取此列表,通常意味着会话已过期。我们之前曾遇到过这种情况。此时返回 error 键,该键将显示 Tiles 错误视图:
<!-- action RetirerSimulation -->
<action name="SupprimerSimulation" class="web.actions.Supprimer">
<result name="erreur" type="tiles">erreur</result>
...
</action>
第 19.9 节介绍了 Tiles 错误视图。
- 第 28–36 行:我们验证第 9 行中的 id 字符串确实是一个大于 0 的整数。
- 第 38–40 行:如果不是这种情况,则再次返回 error 键,这将显示 Tiles 错误视图
- 第 43–48 行:在模拟列表中搜索要删除的模拟。如果找到,则将其删除。
- 第 50 行:更新“Tiles 模拟”视图的菜单。
- 第 51 行:返回 `simulations` 键。这将显示“Tiles 模拟”视图:
<!-- action RetirerSimulation -->
<action name="SupprimerSimulation" class="web.actions.Supprimer">
...
<result name="simulations" type="tiles">simulations</result>
</action>
“模拟”磁贴视图已在第 19.9 节中介绍。
19.11. 返回表单
在“模拟磁贴”视图中,用户可以返回表单:
![]() |
![]() |
- 在 [1] 中,点击链接返回表单
- 在 [2] 中,会出现一个空表单
[返回模拟表单] 链接在 [Entete.jsp] 片段中定义:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<table>
<tr>
<td><h1><s:text name="Pam.titre"/></h1></td>
<td>
...
<s:if test="menu.retourFormulaire">
|<a href="<s:url action="RetourFormulaire"/>"><s:text name="Menu.RetourFormulaire"/></a><br/>
</s:if>
...
</td>
</tr>
</table>
- 第 10 行:该链接指向 [ReturnForm] 操作。该操作在 [struts.xml] 文件中定义如下:
<!-- action RetourFormulaire -->
<action name="RetourFormulaire" >
<result type="redirectAction">
<param name="actionName">Formulaire!input</param>
<param name="namespace">/</param>
</result>
</action>
我们可以看到,此操作未与任何类相关联。它只是将客户端浏览器重定向到 [/Form!input] 操作。因此,我们处于与第 19.7 节中所述显示初始视图时相同的情况。于是,我们返回此初始视图 [2]。
19.12. 查看模拟列表
用户可以在“仿真磁贴”或“输入”视图中请求查看仿真结果:
![]() |
![]() |
- 在 [1] 中,点击 [查看模拟] 链接
- 在 [2] 中,将显示模拟列表
[查看模拟]链接在[Entete.jsp]片段中定义:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<table>
<tr>
<td><h1><s:text name="Pam.titre"/></h1></td>
<td>
...
<s:if test="menu.voirSimulations">
|<a href="<s:url action="VoirSimulations"/>"><s:text name="Menu.VoirSimulations"/></a><br/>
</s:if>
...
</td>
</tr>
</table>
- 第 10 行:链接 [View Simulations] 调用了操作 [ViewSimulations]。该操作在 [struts.xml] 文件中定义如下:
<!-- action VoirSimulations -->
<action name="VoirSimulations" class="web.actions.Voir">
<result name="success" type="tiles">simulations</result>
</action>
[ViewSimulations] 操作与 [View] 类相关联,但未指定具体方法。因此,将执行 [View].execute 方法。 [View] 类的定义如下:
package web.actions;
import com.opensymphony.xwork2.ActionSupport;
import web.entities.Menu;
public class Voir extends ActionSupport{
// menu
private Menu menu=new Menu(false,false,false,false,true,true);
// getters and setters
public Menu getMenu() {
return menu;
}
public void setMenu(Menu menu) {
this.menu = menu;
}
}
[View] 操作只做一件事:为 Tiles 模拟视图设置菜单(第 8 行)。它没有 execute 方法。因此,将执行父类 [ActionSupport] 中的该方法。我们知道该方法除了返回成功标志外,不会执行任何操作。
回到 [struts.xml] 中的该操作:
<!-- action VoirSimulations -->
<action name="VoirSimulations" class="web.actions.Voir">
<result name="success" type="tiles">simulations</result>
</action>
在第 3 行,我们可以看到“success”键会触发 Tiles 模拟视图的显示。这一点已在第 157 页中进行过说明。
19.13. 清除当前模拟
在“Tiles”模拟视图中,用户可以请求清除当前模拟:
![]() |
![]() |
- 在 [1] 中,当前模拟被清除
- 在 [2] 中,输入表单被清空
[清除模拟]链接在[Entete.jsp]片段中定义:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<table>
<tr>
<td><h1><s:text name="Pam.titre"/></h1></td>
<td>
...
<s:if test="menu.effacerSimulation">
|<a href="<s:url action="Formulaire!input"/>"><s:text name="Menu.EffacerSimulation"/></a><br/>
</s:if>
...
</td>
</tr>
</table>
在第 10 行,我们可以看到 [清除模拟] 链接触发了 [Form!input] 操作。我们知道,此操作会导致显示初始视图 [2]。
19.14. 结束当前会话
在任何 Tiles 视图中,用户均可请求结束会话:
![]() |
![]() |
![]() |
- 在 [1] 中,我们从模拟视图开始并结束会话
- 在[2]中,输入表单为空。我们请求查看模拟结果。
- 在[3]中,模拟列表现在为空。
[结束会话]链接在[Entete.jsp]片段中定义:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<table>
<tr>
<td><h1><s:text name="Pam.titre"/></h1></td>
<td>
...
<s:if test="menu.terminerSession">
|<a href="<s:url action="TerminerSession"/>"><s:text name="Menu.TerminerSession"/></a><br/>
</s:if>
</td>
</tr>
</table>
在第 10 行,我们可以看到 [End Session] 链接触发了 [EndSession] 操作。该操作在 [struts.xml] 文件中的定义如下:
<action name="TerminerSession" class="web.actions.Terminer">
<result name="success" type="redirectAction">
<param name="actionName">Formulaire!input</param>
<param name="namespace">/</param>
</result>
</action>
- 第 1 行:我们可以看到 [Terminer] 类将被实例化,并调用其 execute 方法。
- 第 2–5 行:在 [Terminer].execute 方法执行后,用户将被重定向到初始输入视图。这解释了屏幕 2。
[Terminer] 类定义如下:
package web.actions;
import com.opensymphony.xwork2.ActionSupport;
import java.util.Map;
import org.apache.struts2.interceptor.SessionAware;
public class Terminer extends ActionSupport implements SessionAware {
// session
private Map<String, Object> session;
@Override
public String execute() {
// quit current session
session.clear();
return SUCCESS;
}
@Override
public void setSession(Map<String, Object> session) {
this.session = session;
}
}
[Finish] 操作的作用是清除当前会话的属性。
- 第 7 行:[Terminate] 操作实现了 [SessionAware] 接口以访问会话。
- 第 10 行:会话字典
- 第 13 行:调用了 execute 方法
- 第 15 行:它清空了会话字典。因此,当前会话中的模拟列表将消失。这解释了屏幕 #3 的情况。
- 第 16 行:返回键值 "success",正如我们所见,这将显示 Tiles 视图并显示值为 [2]。
19.15. 结论
我们已对案例研究第 1 版进行了全面注释,该版本基于模拟的 [业务] 层运行:
![]() |
剩下的工作就是将真实的业务层与[Web]层“连接”起来。






















