10. 第 5 版 - Web 应用程序 PAM / JSF
10.1. 应用程序架构
Web 应用程序 PAM 的架构如下:
![]() |
在此版本中,Glassfish 服务器将托管应用程序的所有层:
- [web] 层由服务器的 Servlet 容器托管(下文第 1 点)
- 其余 [metier, DAO, jpa] 层由服务器的 EJB3 容器托管(如下图 2 所示)
![]() |
在 EJB3 容器中运行的应用程序组件 [metier, DAO] 已在第 7.1 节中研究的客户端/服务器应用程序中编写,其架构如下:
![]() |
[metier, DAO] 层在 Glassfish 服务器的 EJB3 容器中运行,而 [ui] 层则在另一台机器上的控制台或 Swing 应用程序中运行:
![]() |
在新应用程序的架构中:
![]() |
只需编写 [web / jsf] 层。其余的 [metier, DAO, jpa] 层已包含在内。
在文档 [ref3] 中,展示了一个 Web 应用程序,其中 Web 层使用 Java Server Faces 实现,其架构类似于以下结构:
![]() |
该架构实现了 MVC 设计模式(模型、视图、控制器)。客户端请求的处理流程如下:
如果请求通过 GET 发起,则执行以下两个步骤:
- 请求 - 客户端浏览器向控制器 [Faces Servlet] 发起请求。该控制器处理所有客户端请求,是应用程序的入口点,即 MVC 中的“C”。
- 响应 - 控制器 C 请求选定的页面 JSF 进行渲染。这就是视图,即 MVC 中的 V。 页面 JSF 使用一个 M 模型来初始化其需发送给客户端的响应中的动态部分。该模型是一个 Java 类,可调用 [métier] 和 [4a] 层,为视图 V 提供所需的数据。
如果请求是通过 POST 发出的,则在请求和响应之间会插入两个额外步骤:
- 请求 - 浏览器客户端向控制器 [Faces Servlet] 发起请求。
- 处理——控制器 C 处理该请求。实际上,POST 请求附带了需要处理的数据。为此,控制器借助了应用程序 [2a] 中的特定事件处理程序。 这些处理程序可能需要业务层 [2b]。事件处理程序可能需要更新某些 M 模型 [2c]。一旦处理了客户端的请求,该请求可能会触发各种响应。一个典型的例子是:
- 如果请求无法正确处理,则显示错误页面
- 否则显示确认页面
事件处理程序向控制器 [Faces Servlet] 返回一个字符串类型的结果,称为导航键。
- 导航 - 控制器根据事件处理程序返回的导航键,选择要发送给客户端的页面 JSF(即视图)。
- 响应 - 选定的页面 JSF 将向客户端发送响应。它使用其 M 模板来初始化动态部分。 该模板也可调用 [métier] 和 [4a] 层,为页面 JSF 提供所需的数据。
在 JSF 项目中:
- 控制器 C 是 Servlet [javax.faces.webapp.FacesServlet]。该 Servlet 位于库 [jsf-api.jar] 中。
- 视图 V 由页面 JSF 实现。
- 模型 M 和事件处理程序由通常被称为“后端 Bean”的 Java 类实现。
- 在版本 JSF 和 1.x 中,Bean 的定义以及页面间的导航规则均定义在文件 [faces-config.xml] 中。 该文件包含视图列表以及视图之间的过渡规则。从版本 JSF 2 开始,Bean 的定义可以使用注解来实现,而页面之间的过渡可以在 Bean 的代码中通过“硬编码”的方式实现。
10.2. 应用程序的工作原理
首次调用应用程序时,将显示以下页面:
![]() |
随后填写表单并查询工资:
![]() |
将获得以下结果:
![]() |
此版本计算的是一个虚拟薪资。请勿关注页面内容,而应关注其排版。点击按钮 [Raz] 后,将返回页面 [A]。
错误输入会被标记出来,如下例所示:
![]() |
10.3. NetBeans 项目
我们将构建该应用程序的初始版本,其中将模拟 [métier] 层。架构如下:
![]() |
当事件处理程序或模型向 [métier] [2b, 4a] 层请求数据时,该层将返回虚拟数据。 目的是构建一个能够正确响应用户请求的Web层。一旦实现这一目标,我们只需部署第7.1节中开发的服务器层:
![]() |
这将是我们应用程序的 Web 版本 2(PAM)。
第1版的NetBeans项目是以下Maven项目:
![]() |
- 在 [1] 中,配置文件
- 在 [2] 中,页面在 XHTML 中,样式表
- 在 [3] 中,QZXW2HTML 层的类在 [web] 中
- 在 [4] 中,[web] 层与 [métier] 层以及 [métier] 层之间交换的对象
- 为 [5],应用程序国际化的消息文件
- 在 [6] 中,应用程序的依赖项
下面我们将逐一介绍其中的一些内容。
10.3.1. 配置文件
文件 [web.xml] 是 NetBeans 默认生成的,其中还包含一个异常页面的配置:
- 第 30 行:[index.html] 是应用程序的首页
- 第 32-39 行:异常页面的配置
页面 [exception.html] 源自 [ref3]。其代码如下:
任何未被 Web 应用程序代码明确处理的异常都会导致显示如下所示的页面:
![]() |
文件 [faces-config.xml] 的内容如下:
请注意以下几点:
- 第 9-14 行:文件 [messages.properties] 将用于页面的国际化。可通过 msg 键在 XHTML 页面中访问该文件。
- 第15行:将文件[messages.properties]定义为优先处理由<h:messages>和<h:message>标签显示的错误消息的文件。这允许重写JSF中某些默认的错误消息。此处未使用此功能。
10.3.2. 样式表
文件 [styles.css] 内容如下:
.libelle{
background-color: #ccffff;
font-family: 'Times New Roman',Times,serif;
font-size: 14px;
font-weight: bold
}
body{
background-color: #ffccff
}
.error{
color: #ff3333
}
.info{
background-color: #99cc00
}
.titreInfos{
background-color: #ffcc00
}
以下是使用这些样式的 JSF 代码示例:
| |
| |
| ![]() |
10.3.3. 消息文件
消息文件 [messages_fr.properties] 内容如下:
form.titre=Feuille de salaire
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
这些消息均用于页面 [index.xhtml],但第 11-15 行中的消息除外,它们用于页面 [exception.xhtml]。
10.3.4. Bean的作用域
Bean [web.forms.Form] 的作用域为请求:
import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
@ManagedBean
@RequestScoped
public class Form implements Serializable {
Bean [web.utils.ChangeLocale] 的作用域为 application:
package web.utils;
import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
@ManagedBean
@SessionScoped
public class ChangeLocale implements Serializable{
// 页面本地化
private String locale="fr";
public ChangeLocale() {
}
public String setFrenchLocale(){
locale="fr";
return null;
}
public String setEnglishLocale(){
locale="en";
return null;
}
public String getLocale() {
return locale;
}
public void setLocale(String locale) {
this.locale = locale;
}
}
10.3.5. [métier] 层
[métier] 层实现了以下 IMetierLocal 接口:
package metier;
import java.util.List;
import javax.ejb.Local;
import jpa.Employe;
@Local
public interface IMetierLocal {
// 获取工资单
FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
// 员工名单
List<Employe> findAllEmployes();
}
该接口用于第7.1节所述的客户端/服务器应用程序的服务器端。
我们将用于测试 [web] 层的 Metier 类以如下方式实现了该接口:
package metier;
...
public class Metier implements IMetierLocal {
// 按编号索引的员工名录SS
private Map<String,Employe> hashEmployes=new HashMap<String,Employe>();
// 员工名单
private List<Employe> listEmployes;
// 获取工资单
public FeuilleSalaire calculerFeuilleSalaire(String SS,
double nbHeuresTravaillées, int nbJoursTravaillés) {
// 根据编号检索员工 SS
Employe e=hashEmployes.get(SS);
// 生成一份虚拟工资单
return new FeuilleSalaire(e,new Cotisation(3.49,6.15,9.39,7.88),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)));
// 按编号 SS 索引的员工字典
for(Employe e:listEmployes){
hashEmployes.put(e.getSS(),e);
}
}
// 返回员工列表
return listEmployes;
}
}
具体代码解析留待读者自行研读。需注意所采用的方法:为避免部署应用程序的 EJB 部分,我们模拟了 [métier] 层。 当 [web] 层被判定为正确后,我们便可将其替换为真正的 [métier] 层。
10.4. 表单 [index.xhtml] 及其模板 [Form.java]
现在,我们将构建该表单的页面 XHTML 及其模板。
[ref3]中的推荐阅读:
- 示例 3(mv-jsf2-03):表单中可用的标签列表
- 示例 4 (mv-jsf2-04):由模板填充的下拉列表
- 示例 6 (mv-jsf2-06):输入验证
- 示例 7 (mv-jsf2-07):按钮管理 [Raz]
10.4.1. 步骤 1
问题:构建表单 [index.xhtml] 及其模型 [Form.java],以生成以下页面:
![]() |
输入组件如下:
id | 类型 JSF | 模板 | 角色 | |
comboEmployes | <h:selectOneMenu> | 字符串 comboEmployesValue List<Employe> getEmployes() | 包含员工列表,格式为 “名 姓”。 | |
heuresTravaillees | <h:inputText> | 字符串 heuresTravaillées | 实际工作小时数 | |
joursTravailles | <h:inputText> | 字符串 joursTravaillés | 工作天数 - 整数 | |
btnSalaire | <h:commandButton> | 启动工资计算 | ||
btnRaz | <h:commandButton> | 将表单恢复为初始状态 |
- 方法 getEmployes 将返回从 [métier] 层获取的员工列表。 下拉列表中显示的对象将具有属性 itemValue(即员工的编号 SS),以及属性 itemLabel(由员工的姓和名组成的字符串)。
- 按钮 [Salaire] 和 [Raz] 目前尚未连接到事件处理程序。
- 将验证输入内容的有效性。

请测试此版本。请特别确认输入错误是否已正确提示。
注意:页面组件的 id 属性中切勿包含带重音的字符。在 Glassfish 3.1.2 环境下,这会导致应用程序崩溃。
10.4.2. 步骤 2
问题:填写表单 [index.xhtml] 及其模板 [Form.java],确保点击按钮 [Salaire] 后显示以下页面:
![]() |
按钮 [Salaire] 将连接到模板的事件处理程序 calculerSalaire。该方法将调用 [métier] 层中的方法 calculerFeuilleSalaire。 该工资单将针对在 [1] 中选定的员工生成。
在模板中,工资单将由以下私有字段表示:
private FeuilleSalaire feuilleSalaire;
该字段包含方法 get 和 set。
要获取该对象中的信息,可在页面 JSF 中编写如下表达式:
<h:outputText value="#{form.feuilleSalaire.employe.nom}"/>
value 属性的表达式将按以下方式进行求值:
[form].getFeuilleSalaire().getEmploye().getNom(),其中 [form] 表示类 [Form.java] 的一个实例。 读者可以验证,此处使用的 get 方法确实分别存在于 [Form]、[FeuilleSalaire] 和 [Employe] 类中。 如果并非如此,在评估该表达式时将抛出异常。
请测试此新版本。
10.4.3. 步骤 3
问题:填写表单 [index.xhtml] 及其模板 [Form.java],以获取以下附加信息:
![]() |
我们将遵循与之前相同的步骤。例如,[1]中的欧元货币符号存在一个难题。 在国际化应用程序中,最好采用所用 locale 的显示格式和货币符号(en、de、fr 等)。可通过以下方式获取:
<h:outputFormat value="{0,number,currency}">
<f:param value="#{form.feuilleSalaire.employe.indemnite.entretienJour}"/>
</h:outputFormat>
也可以这样写:
<h:outputText value="#{form.feuilleSalaire.employe.indemnite.entretienJour} є">
但若使用 en_GB 区域设置(英语 GB),显示仍会为欧元,而实际应使用英镑 £。 标签 <h:outputFormat> 允许根据当前显示页面 JSF 的 locale 参数来显示信息:
- 第 1 行:显示参数 {0},这是一个代表货币金额的数字
- 第 2 行:<f:param> 标签为参数 {0} 赋值。第二个 <f:param> 标签将为标记为 {1} 的参数赋值,以此类推。
10.4.4. 步骤 4
推荐阅读:[ref3] 中的示例 7 (mv-jsf2-07)。
问题:完善表单 [index.xhtml] 及其模板 [Form.java],以管理按钮 [Raz]。
按钮 [Raz] 将表单恢复到首次通过 GET 调用时的状态。此处存在若干难点,其中部分已在 [ref3] 中进行过说明。
[Raz]按钮生成的表单并非完整表单,而是其中saisie部分:

可通过以下方式使用 <f:subview> 标签来获得此结果:
<f:subview id="viewInfos" rendered="#{form.viewInfosIsRendered}">
... la partie du formulaire qu'on veut pouvoir ne pas afficher
</f:subview>
<f:subview> 标签包裹了表单中可能显示或隐藏的全部部分。通过 rendered 属性,可以控制任何组件的显示或隐藏。 若 rendered="true",则组件显示;若 rendered="false",则不显示。若 rendered 属性在模板中取值,则可通过程序控制组件的显示。
在上文中,我们将通过以下字段控制视图 viewInfos 的显示:
private boolean viewInfosIsRendered;
并配合其方法 get 和 set。 处理按钮点击的 [Salaire] 和 [Raz] 方法将根据视图 viewInfos 是否需要显示来更新该布尔值。

















