12. 第 7 版 - PAM 多视图/多页面 Web 应用程序
此处我们回归到最初的架构,即模拟 [métier] 层的情况。现在我们知道,该层可以轻松地被真实的 [métier] 层所取代。模拟的 [métier] 层有助于测试。
![]() |
JSF 应用程序属于 MVC 类型(模型-视图-控制器):
- Servlet [Faces Servlet] 是 JSF 提供的通用控制器。该控制器由应用程序特有的事件处理程序进行扩展。 此前遇到的事件处理程序是作为 JSF 页面模型的类中的方法
- JSF 页面将响应发送给客户端浏览器。这些是应用程序的视图。
- JSF 页面包含动态元素,这些元素被称为页面的模型。 需要指出的是,对于某些作者而言,模型涵盖了应用程序所操作的实体,例如类 FeuilleSalaire 或 Employe。为了区分这两种模型,我们可以分别称之为应用程序模型和页面模型 JSF。
在 JSF 架构中,从页面 JSFi 跳转至页面 JSFj 可能会遇到问题。
- 页面 JSFi 已显示。从该页面出发,用户通过某个事件 [1] 触发了 POST
- 在 JSF 中,该 POST 通常由页面 JSFi 的 Mi 模型中的 C 方法处理。可以说,该 C 方法是一个次级控制器。
- 如果该方法执行完毕后需要显示页面 JSFj,控制器 C 必须:
- 更新页面 JSFj 的 Mj 模型 [2c]
- 将导航键 [2a] 返回给主控制器,该键将用于显示页面 JSFj
步骤 1 要求页面 JSFi 的 Mi 模板引用页面 JSFj 的 Mj 模板。这使得情况稍显复杂,因为 Mi 模板之间会相互依赖。实际上,负责更新 Mj 模板的 Mi 模板的 C 管理器必须了解 Mj 模板。 如果需要修改Mj模板,就必须相应地修改Mi模板的C管理器。
有一种情况可以避免模型之间的相互依赖:即存在一个唯一的M模型,用于所有JSF页面。这种架构仅适用于只有少数视图的应用程序,但使用起来非常简单。这就是我们目前采用的架构。
在此背景下,应用程序的架构如下:
![]() |
12.1. 应用程序的视图
向用户展示的视图包括:
-
视图 [VueSaisies],用于展示模拟表单
-
视图 [VueSimulation],用于显示模拟的详细结果:


- 视图 [VueSimulations],显示客户进行的模拟列表

- 视图 [VueSimulationsVides],用于显示客户没有或已无模拟记录:

- 视图 [VueErreur] 显示存在一个或多个错误:

12.2. [web] 层的 NetBeans 项目
该版本的 NetBeans 项目将对应以下 Maven 项目:
![]() |
- 在 [1] 中,配置文件
- 在 [2] 中,页面文件;在 JSF 中,
- 在 [3] 中,视图的样式表和背景图像,
- 在 [4] 中,[web] 层的类,
- 在 [5] 中,应用程序的底层,
- 在 [6] 中,应用程序国际化的消息文件,
- 在 [7] 中,是项目的依赖项。
下面我们将逐一介绍其中的一些内容。
12.2.1. 配置文件
文件 [web.xml] 和 [faces-config.xml] 与上一个项目中的完全相同,但 [web.xml] 中有一个细节不同:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
...
<welcome-file-list>
<welcome-file>faces/saisie.xhtml</welcome-file>
</welcome-file-list>
...
</web-app>
- 第4-6行:首页是页面[saisie.xhtml]
12.2.2. 样式表
文件 [styles.css] 内容如下:
.simulationsHeader {
text-align: center;
font-style: italic;
color: Snow;
background: Teal;
}
.simuNum {
height: 25px;
text-align: center;
background: MediumTurquoise;
}
.simuNom {
text-align: left;
background: PowderBlue;
}
.simuPrenom {
width: 6em;
text-align: left;
color: Black;
background: MediumTurquoise;
}
.simuHT {
width: 3em;
text-align: center;
color: Black;
background: PowderBlue;
}
.simuJT {
width: 3em;
text-align: center;
color: Black;
background: MediumTurquoise;
}
.simuSalaireBase {
width: 9em;
text-align: center;
color: Black;
background: PowderBlue;
}
.simuIndemnites {
width: 3em;
text-align: center;
color: Black;
background: MediumTurquoise;
}
.simuCotisationsSociales {
width: 6em;
text-align: center;
background: PowderBlue;
}
.simuSalaireNet {
width: 10em;
text-align: center;
background: MediumTurquoise;
}
.erreursHeaders {
background: Teal;
background-color: #ff6633;
color: Snow;
font-style: italic;
text-align: center
}
.erreurClasse {
background: MediumTurquoise;
background-color: #ffcc66;
height: 25px;
text-align: center
}
.erreurMessage {
background: PowderBlue;
background-color: #ffcc99;
text-align: left
}
以下是使用这些样式的 JSF 代码示例:
模拟视图
<h:dataTable value="#{form.simulations}" var="simulation"
headerClass="simulationsHeaders" columnClasses="simuNum,simuNom,simuPrenom,simuHT,simuJT,simuSalaireBase,simuIndemnites,simuCotisationsSociales,simuSalaireNet">
生成的结果如下:

错误视图
<h:dataTable value="#{form.erreurs}" var="erreur"
headerClass="erreursHeaders" columnClasses="erreurClasse,erreurMessage">

12.2.3. 消息文件
消息文件 [messages_fr.properties] 内容如下:
form.titre=Simulateur de calcul de paie
form.comboEmployes.libell\u00e9=Employ\u00e9
form.heuresTravaill\u00e9es.libell\u00e9=Heures travaill\u00e9es
form.joursTravaill\u00e9s.libell\u00e9=Jours travaill\u00e9s
form.heuresTravaill\u00e9es.required=Indiquez le nombre d'heures travaill\u00e9es
form.heuresTravaill\u00e9es.validation=Donn\u00e9e incorrecte
form.joursTravaill\u00e9s.required=Indiquez le nombre de jours travaill\u00e9s
form.joursTravaill\u00e9s.validation=Donn\u00e9e incorrecte
form.btnSalaire.libell\u00e9=Salaire
form.btnRaz.libell\u00e9=Raz
exception.header=L'exception suivante s'est produite
exception.httpCode=Code HTTP de l'erreur
exception.message=Message de l'exception
exception.requestUri=Url demand\u00e9e lors de l'erreur
exception.servletName=Nom de la servlet demand\u00e9e lorsque l'erreur s'est produite
form.infos.employ\u00e9=Informations Employ\u00e9
form.employe.nom=Nom
form.employe.pr\u00e9nom=Pr\u00e9nom
form.employe.adresse=Adresse
form.employe.ville=Ville
form.employe.codePostal=Code postal
form.employe.indice=Indice
form.infos.cotisations=Informations Cotisations sociales
form.cotisations.csgrds=CSGRDS
form.cotisations.csgd=CSGD
form.cotisations.retraite=Retraite
form.cotisations.secu=S\u00e9curit\u00e9 sociale
form.infos.indemnites=Informations Indemnit\u00e9s
form.indemnites.salaireHoraire=Salaire horaire
form.indemnites.entretienJour=Entretien / Jour
form.indemnites.repasJour=Repas / Jour
form.indemnites.cong\u00e9sPay\u00e9s=Cong\u00e9s pay\u00e9s
form.infos.salaire=Informations Salaire
form.salaire.base=Salaire de base
form.salaire.cotisationsSociales=Cotisations sociales
form.salaire.entretien=Indemnit\u00e9s d'entretien
form.salaire.repas=Indemnit\u00e9s de repas
form.salaire.net=Salaire net
form.menu.faireSimulation=| Faire la simulation
form.menu.effacerSimulation=| Effacer la simulation
form.menu.enregistrerSimulation=| Enregistrer la simulation
form.menu.retourSimulateur=| Retour au simulateur
form.menu.voirSimulations=| Voir les simulations
form.menu.terminerSession=| Terminer la session
simulations.headers.nom=Nom
simulations.headers.nom=Nom
simulations.headers.prenom=Pr\u00e9nom
simulations.headers.heuresTravaillees=Heures travaill\u00e9es
simulations.headers.joursTravailles=Jours Travaill\u00e9s
simulations.headers.salaireBase=Salaire de base
simulations.headers.indemnites=Indemnit\u00e9s
simulations.headers.cotisationsSociales=Cotisations sociales
simulations.headers.salaireNet=SalaireNet
simulations.headers.numero=N\u00b0
erreur.titre=Une erreur s'est produite.
erreur.exceptions=Cha\u00eene des exceptions
exception.type=Type de l'exception
exception.message=Message associ\u00e9
12.2.4. [métier] 层
模拟的图层 [métier] 已按以下方式修改:
package metier;
...
public class Metier implements ImetierLocal, Serializable {
// 员工列表
private Map<String,Employe> hashEmployes=new HashMap<String,Employe>();
private List<Employe> listEmployes;
// 获取工资单
public FeuilleSalaire calculerFeuilleSalaire(String SS,
double nbHeuresTravaillées, int nbJoursTravaillés) {
// 检索员工
Employe e=hashEmployes.get(SS);
// 若员工不存在则抛出异常
if(e==null){
throw new PamException(String.format("L'employé de n° SS [%s] n'existe pas",SS),1);
}
// 生成虚拟工资单
return new FeuilleSalaire(e,new Cotisation(3.49,6.15,9.39,7.88),e.getIndemnite(),new ElementsSalaire(100,100,100,100,100));
}
// 员工列表
public List<Employe> findAllEmployes() {
if(listEmployes==null){
// 创建包含三名员工的列表
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)));
// 员工字典
for(Employe e:listEmployes){
hashEmployes.put(e.getSS(),e);
}
// 向员工名录中添加一名不存在的员工
listEmployes.add(new Employe("X","Y","Z","La brûlerie","St Marcel","49014",new Indemnite(1,1.93,2,3,12)));
}
// 返回员工列表
return listEmployes;
}
}
- 第 8 行:员工列表
- 第 7 行:以员工编号为索引的字典形式的同一列表 SS
- 第24-39行:返回员工列表的方法findAllEmployes。该方法创建一个硬编码列表,并通过第8行的字段employés进行引用。
- 第27-33行:创建包含两名员工的列表和字典
- 第35行:将一名员工添加到员工列表(第8行)中,但未添加到字典hashEmployes(第7行)中。 此操作旨在使其出现在员工下拉列表中,但随后不被方法 calculerFeuilleSalaire(第 14 行)识别,从而触发该方法抛出异常(第 17 行)。
- 第 11-21 行:方法 calculerFeuilleSalaire
- 第14行:通过员工编号SS在字典hashEmployes中搜索该员工。若未找到,则抛出异常(第16-18行)。 因此,对于编号为 SS X 的员工,系统会在第 35 行将其添加到员工列表中,但未添加到字典 hashEmployes 中,从而引发异常。
- 第20行,创建并返回一份虚拟的工资单。
12.3. 应用程序的Bean
将包含三个作用域不同的 Bean:
![]() |
12.3.1. Bean ApplicationData
Bean ApplicationData 的作用域为 application:
package web.beans.application;
import java.io.Serializable;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import metier.IMetierLocal;
import metier.Metier;
@Named
@ApplicationScoped
public class ApplicationData implements Serializable {
// 业务层
private IMetierLocal metier = new Metier();
// 日志记录器
private boolean logsEnabled = true;
private final Logger logger = Logger.getLogger("pam");
public ApplicationData() {
}
@PostConstruct
public void init() {
// 日志
if (isLogsEnabled()) {
logger.info("ApplicationData");
}
}
// 获取器和设置器
...
}
- 第 11 行:@Named 注解将该类设为受管 Bean。 需要注意的是,与前一个项目不同,这里未使用 @ManagedBean 注解。原因是该类的引用必须通过 @Inject 注解注入到另一个类中,而 @Inject 注解仅能注入带有 @Named 注解的类。
- 第 12 行:@ApplicationScoped 注解将该类设为应用程序作用域对象。需注意,该注解对应的类是 [javax.enterprise.context.ApplicationScoped](第 6 行),而非前一个项目中 Bean 所用的 [javax.faces.bean.ApplicationScoped]。
Bean ApplicationData 具有两个作用:
- 第 16 行:维护对 [métier] 层的引用,
- 第 18-19 行:定义一个日志器,其他 Bean 可通过它向 Glassfish 控制台写入日志。
12.3.2. Bean SessionData
SessionData Bean 的作用域为会话:
package web.beans.session;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.enterprise.context.SessionScoped;
import javax.inject.Inject;
import javax.inject.Named;
import web.beans.application.ApplicationData;
import web.entities.Simulation;
@Named
@SessionScoped
public class SessionData implements Serializable {
// 应用程序
@Inject
private ApplicationData applicationData;
// 模拟
private List<Simulation> simulations = new ArrayList<Simulation>();
private int numDerniereSimulation = 0;
private Simulation simulation;
// 菜单
private boolean menuFaireSimulationIsRendered = true;
private boolean menuEffacerSimulationIsRendered = true;
private boolean menuEnregistrerSimulationIsRendered;
private boolean menuVoirSimulationsIsRendered;
private boolean menuRetourSimulateurIsRendered;
private boolean menuTerminerSessionIsRendered = true;
// 区域设置
private String locale="fr_FR";
// 构造函数
public SessionData() {
}
@PostConstruct
public void init() {
// 日志
if (applicationData.isLogsEnabled()) {
applicationData.getLogger().info("SessionData");
}
}
// 菜单管理
public void setMenu(boolean menuFaireSimulationIsRendered, boolean menuEnregistrerSimulationIsRendered, boolean menuEffacerSimulationIsRendered, boolean menuVoirSimulationsIsRendered, boolean menuRetourSimulateurIsRendered, boolean menuTerminerSessionIsRendered) {
this.setMenuFaireSimulationIsRendered(menuFaireSimulationIsRendered);
this.setMenuEnregistrerSimulationIsRendered(menuEnregistrerSimulationIsRendered);
this.setMenuVoirSimulationsIsRendered(menuVoirSimulationsIsRendered);
this.setMenuEffacerSimulationIsRendered(menuEffacerSimulationIsRendered);
this.setMenuRetourSimulateurIsRendered(menuRetourSimulateurIsRendered);
this.setMenuTerminerSessionIsRendered(menuTerminerSessionIsRendered);
}
// 获取器和设置器
...
}
- 第 13 行:类 SessionData 是一个受管 Bean(@Named),可注入到其他受管 Bean 中,
- 第 14 行:其作用域为会话 (@SessionScoped),
- 第 18-19 行:向其注入了对 Bean ApplicationData 的引用 (@Inject),
- 第 21-32 行:应用程序中需要在不同会话间保持的数据。
- 第 21 行:用户执行的模拟列表,
- 第22行:最近一次保存的模拟编号,
- 第23行:最近一次执行的模拟,
- 第25-30行:菜单选项,
- 第32行:应用程序的区域设置。
第39-44行,在类(@PostConstruct)实例化后执行init方法。此处仅将其用于记录执行痕迹。 由于该类的作用域为会话,因此应能验证该方法对每个用户仅执行一次。第42行,该方法使用了在类ApplicationData中定义的日志器。正因如此,我们需要注入对该Bean的引用(第18-19行)。
12.3.3. Form Bean
Form Bean 的作用域为 requête:
package web.beans.request;
import java.util.ArrayList;
import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.HttpServletRequest;
import jpa.Employe;
import metier.FeuilleSalaire;
import web.beans.application.ApplicationData;
import web.beans.session.*;
import web.entities.Erreur;
import web.entities.Simulation;
@Named
@RequestScoped
public class Form {
public Form() {
}
// 其他 Bean
@Inject
private ApplicationData applicationData;
@Inject
private SessionData sessionData;
// 视图模型
private String comboEmployesValue = "";
private String heuresTravaillées = "";
private String joursTravaillés = "";
private Integer numSimulationToDelete;
private List<Erreur> erreurs = new ArrayList<Erreur>();
private FeuilleSalaire feuilleSalaire;
// 员工列表
public List<Employe> getEmployes(){
return applicationData.getMetier().findAllEmployes();
}
// 菜单操作
public String faireSimulation() {
...
}
public String enregistrerSimulation() {
...
}
public String effacerSimulation() {
...
}
public String voirSimulations() {
...
}
public String retourSimulateur() {
...
}
public String terminerSession() {
...
}
public String retirerSimulation() {
...
}
private void razFormulaire() {
...
}
// getter 和 setter
...
}
- 第 17 行,该类是一个受管 Bean (@Named),
- 第 18 行,作用域为请求 (@RequestScoped),
- 第 24-25 行:注入对应用程序作用域 Bean ApplicationData 的引用,
- 第26-27行:注入会话作用域Bean SessionData的引用。
12.4. 应用程序的页面
![]() |
12.4.1. [layout.xhtml]
页面 [layout.xhtml] 负责所有视图的版面设置:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<f:view locale="#{sessionData.locale}">
<h:head>
<title><h:outputText value="#{msg['form.titre']}"/></title>
<h:outputStylesheet library="css" name="styles.css"/>
</h:head>
<script type="text/javascript">
function raz(){
// 修改提交的值
document.forms['formulaire'].elements['formulaire:comboEmployes'].value="0";
document.forms['formulaire'].elements['formulaire:heuresTravaillees'].value="0";
document.forms['formulaire'].elements['formulaire:joursTravailles'].value="0";
}
</script>
<h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
<h:form id="formulaire">
<!-- 页眉 -->
<ui:include src="entete.xhtml" />
<!-- 内容 -->
<ui:insert name="part1" >
Gestion des assistantes maternelles
</ui:insert>
<ui:insert name="part2"/>
</h:form>
</h:body>
</f:view>
</html>
每个视图由以下元素组成:
- 由片段 [entete.xhtml] 显示的页眉(第 24 行),
- 一个由名为 part1(第 26-28 行)和 part2(第 29 行)的两个片段组成的主体,
- 第 8 行:locale 属性负责页面的国际化。此处仅有一个:fr_FR,
- 第14-19行:一段JavaScript代码。
页面 [layout.xhtml] 的显示效果如下:
![]() |
- 在 [1] 中,标题为 [entete.xhtml],
- 在 [2] 中,片段 part1。
12.4.2. 标题 [entete.xhtml]
页面 [entete.xhtml] 的代码如下:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition>
<!-- 页眉 -->
<h:panelGrid columns="2">
<h:panelGroup>
<h2><h:outputText value="#{msg['form.titre']}"/></h2>
</h:panelGroup>
<h:panelGroup>
<h:panelGrid columns="1">
<h:commandLink id="cmdFaireSimulation" value="#{msg['form.menu.faireSimulation']}" action="#{form.faireSimulation}" rendered="#{sessionData.menuFaireSimulationIsRendered}"/>
<h:commandLink id="cmdEffacerSimulation" onclick="raz()" value="#{msg['form.menu.effacerSimulation']}" action="#{form.effacerSimulation}" rendered="#{sessionData.menuEffacerSimulationIsRendered}"/>
<h:commandLink id="cmdEnregistrerSimulation" immediate="true" value="#{msg['form.menu.enregistrerSimulation']}" action="#{form.enregistrerSimulation}" rendered="#{sessionData.menuEnregistrerSimulationIsRendered}"/>
<h:commandLink id="cmdVoirSimulations" immediate="true" value="#{msg['form.menu.voirSimulations']}" action="#{form.voirSimulations}" rendered="#{sessionData.menuVoirSimulationsIsRendered}"/>
<h:commandLink id="cmdRetourSimulateur" immediate="true" value="#{msg['form.menu.retourSimulateur']}" action="#{form.retourSimulateur}" rendered="#{sessionData.menuRetourSimulateurIsRendered}"/>
<h:commandLink id="cmdTerminerSession" immediate="true" value="#{msg['form.menu.terminerSession']}" action="#{form.terminerSession}" rendered="#{sessionData.menuTerminerSessionIsRendered}"/>
</h:panelGrid>
</h:panelGroup>
</h:panelGrid>
<hr/>
</ui:composition>
</html>
- 第 12 行:应用程序标题,
- 第 16-21 行:六个链接,对应用户可执行的六项操作。这些链接由 SessionData Bean 中的布尔值控制(rendered 属性),
- 第 17 行:点击 [Effacer la simulation] 链接将触发 Javascript 函数 raz 的执行。该函数在模型 [layout.xhtml] 中已定义,
- 第18行:immediate=true属性导致在执行方法[Form].enregistrerSimulation之前,不会验证数据的有效性。这是预期的行为。 用户可能希望保存一分钟前进行的最后一次模拟,即使此后在表单中输入(但尚未验证)的数据无效。 操作 [Voir les simulations]、[Effacer la simulation]、[Retour au simulateur] 和 [Terminer la session] 亦遵循此规则。
12.5. 应用程序的使用场景
12.5.1. 显示首页
主页是以下页面 [saisie.xhtml]:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="layout.xhtml">
<ui:define name="part1">
<ui:include src="saisie2.xhtml"/>
</ui:define>
</ui:composition>
</html>
- 第 8 行:显示在页面 [layout.xhtml] 内,
- 第 9 行:位于名为 part1 的片段位置。在此片段中,显示页面 [saisie2.xhtml]:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<h:panelGrid columns="3">
<h:outputText value="#{msg['form.comboEmployes.libellé']}"/>
<h:outputText value="#{msg['form.heuresTravaillées.libellé']}"/>
<h:outputText value="#{msg['form.joursTravaillés.libellé']}"/>
<h:selectOneMenu id="comboEmployes" value="#{form.comboEmployesValue}">
<f:selectItems .../>
</h:selectOneMenu>
<h:inputText id="heuresTravaillees" value="#{form.heuresTravaillées}" required="true" requiredMessage="#{msg['form.heuresTravaillées.required']}" validatorMessage="#{msg['form.heuresTravaillées.validation']}">
<f:validateDoubleRange minimum="0" maximum="300"/>
</h:inputText>
<h:inputText id="joursTravailles" value="#{form.joursTravaillés}" required="true" requiredMessage="#{msg['form.joursTravaillés.required']}" validatorMessage="#{msg['form.joursTravaillés.validation']}">
<f:validateLongRange minimum="0" maximum="31"/>
</h:inputText>
<h:panelGroup></h:panelGroup>
<h:message for="heuresTravaillees" styleClass="error"/>
<h:message for="joursTravailles" styleClass="error"/>
</h:panelGrid>
<hr/>
</html>
该代码显示以下视图:
![]() |
问题:补全代码 XHTML 的第 13 行。员工下拉列表中的元素列表由 Bean [Form] 中的一个方法提供。编写该方法。 下拉列表中显示的条目,其 itemValue 属性将等于某位员工的编号 SS,而 itemLabel 属性将是一个由该员工名字和姓氏组成的字符串。
问题:应如何初始化 Bean [SessionData],以便在向表单发出初始请求 GET 时,页眉菜单显示为上图所示的内容?
12.5.2. 操作 [faireSimulation]
操作 [faireSimulation] 的代码 JSF 如下:
<h:commandLink id="cmdFaireSimulation" value="#{msg['form.menu.faireSimulation']}" action="#{form.faireSimulation}" rendered="#{sessionData.menuFaireSimulationIsRendered}"/>
操作 [faireSimulation] 用于计算工资单:

根据上一页的内容,得到以下结果:
![]() |
![]() |
模拟结果显示在以下页面 [simulation.xhtml] 中:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="layout.xhtml">
<ui:define name="part1">
<ui:include src="saisie2.xhtml"/>
</ui:define>
<ui:define name="part2">
<h:outputText value="#{msg['form.infos.employé']}" styleClass="titreInfos"/>
<br/><br/>
<h:panelGrid columns="3" rowClasses="libelle,info">
<h:outputText value="#{msg['form.employe.nom']}"/>
<h:outputText value="#{msg['form.employe.prénom']}"/>
<h:outputText value="#{msg['form.employe.adresse']}"/>
<h:outputText value="#{form.feuilleSalaire.employe.nom}"/>
<h:outputText value="#{form.feuilleSalaire.employe.prenom}"/>
<h:outputText value="#{form.feuilleSalaire.employe.adresse}"/>
</h:panelGrid>
<h:panelGrid columns="3" rowClasses="libelle,info">
<h:outputText value="#{msg['form.employe.ville']}"/>
<h:outputText value="#{msg['form.employe.codePostal']}"/>
<h:outputText value="#{msg['form.employe.indice']}"/>
<h:outputText value="#{form.feuilleSalaire.employe.ville}"/>
<h:outputText value="#{form.feuilleSalaire.employe.codePostal}"/>
<h:outputText value="#{form.feuilleSalaire.employe.indemnite.indice}"/>
</h:panelGrid>
<br/>
<h:outputText value="#{msg['form.infos.cotisations']}" styleClass="titreInfos"/>
<br/><br/>
<h:panelGrid columns="4" rowClasses="libelle,info">
<h:outputText value="#{msg['form.cotisations.csgrds']}"/>
<h:outputText value="#{msg['form.cotisations.csgd']}"/>
<h:outputText value="#{msg['form.cotisations.retraite']}"/>
<h:outputText value="#{msg['form.cotisations.secu']}"/>
<h:outputText value="#{form.feuilleSalaire.cotisation.csgrds} %"/>
<h:outputText value="#{form.feuilleSalaire.cotisation.csgd} %"/>
<h:outputText value="#{form.feuilleSalaire.cotisation.retraite} %"/>
<h:outputText value="#{form.feuilleSalaire.cotisation.secu} %"/>
</h:panelGrid>
<br/>
<h:outputText value="#{msg['form.infos.indemnites']}" styleClass="titreInfos"/>
<br/><br/>
<h:panelGrid columns="4" rowClasses="libelle,info">
<h:outputText value="#{msg['form.indemnites.salaireHoraire']}"/>
<h:outputText value="#{msg['form.indemnites.entretienJour']}"/>
<h:outputText value="#{msg['form.indemnites.repasJour']}"/>
<h:outputText value="#{msg['form.indemnites.congésPayés']}"/>
<h:outputFormat value="{0,number,currency}">
<f:param value="#{form.feuilleSalaire.employe.indemnite.baseHeure}"/>
</h:outputFormat>
<h:outputFormat value="{0,number,currency}">
<f:param value="#{form.feuilleSalaire.employe.indemnite.entretienJour}"/>
</h:outputFormat>
<h:outputFormat value="{0,number,currency}">
<f:param value="#{form.feuilleSalaire.employe.indemnite.repasJour}"/>
</h:outputFormat>
<h:outputText value="#{form.feuilleSalaire.employe.indemnite.indemnitesCP} %"/>
</h:panelGrid>
<br/>
<h:outputText value="#{msg['form.infos.salaire']}" styleClass="titreInfos"/>
<br/><br/>
<h:panelGrid columns="4" rowClasses="libelle,info">
<h:outputText value="#{msg['form.salaire.base']}"/>
<h:outputText value="#{msg['form.salaire.cotisationsSociales']}"/>
<h:outputText value="#{msg['form.salaire.entretien']}"/>
<h:outputText value="#{msg['form.salaire.repas']}"/>
<h:outputFormat value="{0,number,currency}">
<f:param value="#{form.feuilleSalaire.elementsSalaire.salaireBase}"/>
</h:outputFormat>
<h:outputFormat value="{0,number,currency}">
<f:param value="#{form.feuilleSalaire.elementsSalaire.cotisationsSociales}"/>
</h:outputFormat>
<h:outputFormat value="{0,number,currency}">
<f:param value="#{form.feuilleSalaire.elementsSalaire.indemnitesEntretien}"/>
</h:outputFormat>
<h:outputFormat value="{0,number,currency}">
<f:param value="#{form.feuilleSalaire.elementsSalaire.indemnitesRepas}"/>
</h:outputFormat>
</h:panelGrid>
<br/>
<h:panelGrid columns="3" columnClasses="libelle,col2,info">
<h:outputText value="#{msg['form.salaire.net']}"/>
<h:panelGroup></h:panelGroup>
<h:outputFormat value="{0,number,currency}">
<f:param value="#{form.feuilleSalaire.elementsSalaire.salaireNet}"/>
</h:outputFormat>
</h:panelGrid>
</ui:define>
</ui:composition>
</html>
- 第 8 行,页面 [simulation.xhtml] 插入到页面 [layout.xhtml] 中,
- 第9-11行:输入区域显示在布局页面的part1片段中,
- 第12-91行:在页面布局的part2片段中显示模拟结果。
问题:编写类 [Form] 中的方法 [faireSimulation]。模拟结果将保存在 Bean SessionData 中。
12.5.3. 错误处理
我们需要妥善处理在计算模拟过程中可能出现的异常。为此,方法 [faireSimulation] 的代码将使用 try / catch 语句:
// 菜单操作
public String faireSimulation(){
try{
// 计算工资单
feuilleSalaire= ...
// 显示模拟结果
...
// 更新菜单
...
// 返回模拟视图
return "simulation";
}catch(Throwable th){
// 清空之前的错误列表
...
// 创建新的错误列表
...
// 显示视图vueErreur
...
// 更新菜单
...
// 显示错误视图
return "erreurs";
}
}
第 15 行创建的错误列表如下:
// 视图模板
...
private List<Erreur> erreurs=new ArrayList<Erreur>();
...
Error 类定义如下:
package web.entities;
public class Erreur {
public Erreur() {
}
// 字段
private String classe;
private String message;
// 构造函数
public Erreur(String classe, String message){
this.setClasse(classe);
this.message=message;
}
// 获取器和设置器
...
}
错误将作为异常处理,其类名存储在“类”字段中,消息存储在“消息”字段中。
在方法 [faireSimulation] 中构建的错误列表包含:
- 已发生的初始异常 th(类型为 Throwable),
- 其原因(如有)th.getCause(),
- 以及该原因的根源 h.getCause().getCause()(如果存在)。
- ...
以下是一个错误列表的示例:

在上例中,员工 [Z Y] 在模拟层 [métier] 所使用的员工字典中不存在。在此情况下,模拟层 [métier] 会抛出异常(见下文第 6 行):
public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés) {
// 获取员工
Employe e=hashEmployes.get(SS);
// 如果员工不存在则抛出异常
if(e==null){
throw new PamException(String.format("L'employé de n° SS [%s] n'existe pas",SS),1);
}
...
}
所得结果如下:

该视图由以下页面 [erreurs.xhtml] 显示:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="layout.xhtml">
<ui:define name="part1">
<h3><h:outputText value="#{msg['erreur.titre']}"/></h3>
<h:dataTable value="#{form.erreurs}" var="erreur"
headerClass="erreursHeaders" columnClasses="erreurClasse,erreurMessage">
<f:facet name="header">
<h:outputText value="#{msg['erreur.exceptions']}"/>
</f:facet>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['exception.type']}"/>
</f:facet>
<h:outputText value="#{erreur.classe}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['exception.message']}"/>
</f:facet>
<h:outputText value="#{erreur.message}"/>
</h:column>
</h:dataTable>
</ui:define>
</ui:composition>
</html>
问题:完善方法 [faireSimulation],使其在发生异常时显示视图 [vueErreur]。
12.5.4. 操作 [effacerSimulation]
操作 [effacerSimulation] 允许用户获取一个空表单:
![]() |
链接 [effacerSimulation] 的代码 JSF 如下:
<h:commandLink id="cmdEffacerSimulation" onclick="raz()" value="#{msg['form.menu.effacerSimulation']}" action="#{form.effacerSimulation}" rendered="#{sessionData.menuEffacerSimulationIsRendered}"/>
点击链接 [EffacerSimulation] 会首先调用 JavaScript 函数 raz()。该方法在页面 [layout.xhtml] 中定义:
<script type="text/javascript">
function raz(){
// 修改提交的值
document.forms['formulaire'].elements['formulaire:comboEmployes'].value="0";
document.forms['formulaire'].elements['formulaire:heuresTravaillees'].value="0";
document.forms['formulaire'].elements['formulaire:joursTravailles'].value="0";
}
</script>
第4-6行修改了提交的值。需要注意的是
- 提交的值是有效的,c.a.d。它们将通过输入字段的验证测试 heuresTravaillees 和 joursTravailles。
- 函数 raz 不会提交表单。实际上,该表单将通过链接 cmdEffacerSimulation 进行提交。此 post 将在执行 JavaScript 函数 raz 之后执行。
在 post 过程中,提交的值将遵循常规流程:先进行验证,然后分配给模型字段。在类 [Form] 中,这些字段如下:
// 视图模型
private String comboEmployesValue;
private String heuresTravaillées;
private String joursTravaillés;
...
这三个字段将接收三个已提交的值 {"0","0","0"}。完成此赋值操作后,将执行方法 effacerSimulation。
问题:编写类 [Form] 中的方法 [effacerSimulation]。需确保:
-
仅显示输入区域,
-
下拉列表框定位在其第一个选项上,
-
输入字段 heuresTravaillees 和 joursTravailles 显示空字符串。
12.5.5. 操作 [enregistrerSimulation]
链接 [enregistrerSimulation] 的代码 JSF 如下:
<h:commandLink id="cmdEnregistrerSimulation" immediate="true" value="#{msg['form.menu.enregistrerSimulation']}" action="#{form.enregistrerSimulation}" rendered="#{sessionData.menuEnregistrerSimulationIsRendered}"/>
与该链接关联的操作 [enregistrerSimulation] 可将当前模拟保存到由类 [SessionData] 维护的模拟列表中:
private List<Simulation> simulations=new ArrayList<Simulation>();
模拟类如下:
package web.entities;
import metier.FeuilleSalaire;
public class Simulation {
public Simulation() {
}
// 模拟中的字段
private Integer num;
private FeuilleSalaire feuilleSalaire;
private String heuresTravaillées;
private String joursTravaillés;
// 构造函数
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();
}
// 获取器和设置器
...
}
该类用于存储用户执行的模拟:
- 第 11 行:模拟编号,
- 第12行:已计算的工资单,
- 第13行:工作小时数,
- 第14行:工作日数。
以下是一个记录示例:

根据上一页的内容,得到以下结果:

模拟编号是一个随每次新记录生成而递增的数字。它属于 Bean SessionData:
// 模拟
private List<Simulation> simulations = new ArrayList<Simulation>();
private int numDerniereSimulation = 0;
private Simulation simulation;
- 第2行:上次模拟的编号。
方法 [enregistrerSimulation] 可按以下方式处理:
- 从 Bean [SessionData] 中获取上次模拟的编号并递增,
- 将新模拟添加到由类 [SessionData] 维护的模拟列表中,
- 显示模拟列表:
![]() |
模拟列表由页面 [simulations.xhtml] 显示:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="layout.xhtml">
<ui:define name="part1">
<!-- 模拟图表 -->
<h:dataTable value="#{sessionData.simulations}" var="simulation"
headerClass="simulationsHeaders" columnClasses="simuNum,simuNom,simuPrenom,simuHT,simuJT,simuSalaireBase,simuIndemnites,simuCotisationsSociales,simuSalaireNet">
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['simulations.headers.numero']}"/>
</f:facet>
<h:outputText value="#{simulation.num}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['simulations.headers.nom']}"/>
</f:facet>
<h:outputText value="#{simulation.feuilleSalaire.employe.nom}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['simulations.headers.prenom']}"/>
</f:facet>
<h:outputText value="#{simulation.feuilleSalaire.employe.prenom}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['simulations.headers.heuresTravaillees']}"/>
</f:facet>
<h:outputText value="#{simulation.heuresTravaillées}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['simulations.headers.joursTravailles']}"/>
</f:facet>
<h:outputText value="#{simulation.joursTravaillés}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['simulations.headers.salaireBase']}"/>
</f:facet>
<h:outputText value="#{simulation.feuilleSalaire.elementsSalaire.salaireBase}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['simulations.headers.indemnites']}"/>
</f:facet>
<h:outputText value="#{simulation.indemnites}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['simulations.headers.cotisationsSociales']}"/>
</f:facet>
<h:outputText value="#{simulation.feuilleSalaire.elementsSalaire.cotisationsSociales}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['simulations.headers.salaireNet']}"/>
</f:facet>
<h:outputText value="#{simulation.feuilleSalaire.elementsSalaire.salaireNet}"/>
</h:column>
<h:column>
<h:commandLink value="Retirer" action="#{form.retirerSimulation}">
<f:setPropertyActionListener target="#{form.numSimulationToDelete}" value="#{simulation.num}"/>
</h:commandLink>
</h:column>
</h:dataTable>
</ui:define>
</ui:composition>
</html>
- 第 8 行,页面 [simulations.xhtml] 被插入到页面 [layout.xhtml] 中,
- 第9行,在名为part1的片段位置,
- 第 11 行,标签 <h:dataTable> 使用字段 #{sessionData.simulations} 作为数据源,c.a.d。下一个字段:
// 模拟
private List<Simulation> simulations;
-
属性 var="simulation" 用于在 <h:datatable> 标签内设定代表当前模拟的变量名称
-
属性 headerClass="simulationsHeaders" 设定表格列标题的样式。
-
属性 columnClasses="...." 设置了表格中每一列的样式
让我们查看表格中的一列,了解其构建方式:
![]() |
Nom列中的JSF代码如下:
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['simulations.headers.nom']}"/>
</f:facet>
<h:outputText value="#{simulation.feuilleSalaire.employe.nom}"/>
</h:column>
- 第 2-4 行:标签 <f:facet name="header"> 定义了列标题
- 第 5 行:员工姓名写为:
- simulation 引用了 <h:dataTable ...> 标签的 var 属性:
<h:dataTable value="#{sessionData.simulations}" var="simulation" ...>
simulation 指模拟列表中的当前模拟:首先是第 1 个,然后是第 2 个,……
- (续)
- simulation.feuilleSalaire 指向当前模拟中的字段 feuilleSalaire
- simulation.feuilleSalaire.employe 引用了字段 feuilleSalaire 中的“员工”字段
- simulation.feuilleSalaire.employe.nom 引用了字段 employe 中的字段 nom
对表中的所有列都重复了同样的处理。对于“Indemnités”列存在一个难题,该列是通过以下代码生成的:
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['simulations.headers.indemnites']}"/>
</f:facet>
<h:outputText value="#{simulation.indemnites}"/>
</h:column>
第5行,显示simulation.indemnites的值。 但 Simulation 类中并不存在 indemnites 字段。需要注意的是,indemnites 字段并非直接使用,而是通过方法 simulation.getIndemnites() 间接调用。因此,只要该方法存在即可。而字段 indemnites 则可以不存在。 方法 getIndemnites 应返回员工的津贴总额。这需要进行中间计算,因为该总额在工资单中无法直接获取。方法 getIndemnites 见第 12.5.5 节。
问题:编写类 [Form] 中的方法 [enregistrerSimulation]。
12.5.6. 操作 [retourSimulateur]
链接 [retourSimulateur] 的代码 JSF 如下:
<h:commandLink id="cmdRetourSimulateur" immediate="true" value="#{msg['form.menu.retourSimulateur']}" action="#{form.retourSimulateur}" rendered="#{sessionData.menuRetourSimulateurIsRendered}"/>
与该链接关联的操作 [retourSimulateur] 允许用户从视图 [vueSimulations] 返回至视图 [vueSaisies]:

生成的结果:

问题:编写类 [Form] 中的方法 [retourSimulateur]。显示的输入表单应如上所示为空。
12.5.7. 操作 [voirSimulations]
链接 [voirSimulations] 的代码 JSF 如下:
<h:commandLink id="cmdVoirSimulations" immediate="true" value="#{msg['form.menu.voirSimulations']}" action="#{form.voirSimulations}" rendered="#{sessionData.menuVoirSimulationsIsRendered}"/>
与该链接关联的操作 [voirSimulations] 允许用户获取模拟表格,无论其输入状态如何:

所得结果:

问题:编写类 [Form] 中的方法 [voirSimulations]。
需确保当模拟列表为空时,显示的视图为 [vueSimulationsVides]:

要显示上述视图,将使用以下页面 [simulationsVides.xhtml]:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="layout.xhtml">
<ui:define name="part1">
<h2>Votre liste de simulations est vide.</h2>
</ui:define>
</ui:composition>
</html>
12.5.8. 操作 [retirerSimulation]
用户可以从列表中移除模拟:

所得结果如下:

如果从上述列表中移除最后一个模拟,将得到以下结果:

模拟表中[Retirer]列的代码JSF如下:
<h:dataTable value="#{form.simulations}" var="simulation"
headerClass="simulationsHeaders" columnClasses="simuNum,simuNom,simuPrenom,simuHT,simuJT,simuSalaireBase,simuIndemnites,simuCotisationsSociales,simuSalaireNet">
...
<h:column>
<h:commandLink value="Retirer" action="#{form.retirerSimulation}">
<f:setPropertyActionListener target="#{form.numSimulationToDelete}" value="#{simulation.num}"/>
</h:commandLink>
</h:column>
</h:dataTable>
- 第 5 行:链接 [Retirer] 关联到类 [Form] 的方法 [retirerSimulation]。 该方法需要知道要删除的模拟编号。该编号由第 8 行中的 <f:setPropertyActionListener> 标签提供。该标签有两个属性:target 和 value;target 属性指定模型中的一个字段,value 属性的值将被赋给该字段。 在此,待删除的模拟编号 #{simulation.num} 将被赋值给类 [Form] 中的字段 numSimulationToDelete:
// 视图模型
...
private Integer numSimulationToDelete;
当类 [Form] 的方法 [retirerSimulation] 执行时,它将能够使用之前存储在字段 numSimulationToDelete 中的值。
问题:编写类 [Form] 的方法 [retirerSimulation]。
12.5.9. 操作 [terminerSession]
链接 [Terminer la session] 的代码 JSF 如下:
<h:commandLink id="cmdTerminerSession" immediate="true" value="#{msg['form.menu.terminerSession']}" action="#{form.terminerSession}" rendered="#{sessionData.menuTerminerSessionIsRendered}"/>
与该链接关联的操作 [terminerSession] 允许用户退出当前会话并返回空白的输入表单:


如果用户之前有一份模拟列表,该列表将被清空。此外,模拟编号将重置为1。
问题:编写类 [Form] 中的方法 [terminerSession]。
12.6. 将Web层集成到三层架构中 JSF / EJB
之前的Web应用程序架构如下:
![]() |
我们将模拟的 [métier] 层,替换为第 7.1 节中由 EJB 实现的 [métier, DAO, jpa] 层:
![]() |
实践作业:按照第11节的方法论,完成JSF和EJB图层的集成。
![]() |














