Skip to content

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 发起,则执行以下两个步骤:

  1. 请求 - 客户端浏览器向控制器 [Faces Servlet] 发起请求。该控制器处理所有客户端请求,是应用程序的入口点,即 MVC 中的“C”
  2. 响应 - 控制器 C 请求选定的页面 JSF 进行渲染。这就是视图,即 MVC 中的 V。 页面 JSF 使用一个 M 模型来初始化其需发送给客户端的响应中的动态部分。该模型是一个 Java 类,可调用 [métier] 和 [4a] 层,为视图 V 提供所需的数据。

如果请求是通过 POST 发出的,则在请求和响应之间会插入两个额外步骤:

  1. 请求 - 浏览器客户端向控制器 [Faces Servlet] 发起请求。
  2. 处理——控制器 C 处理该请求。实际上,POST 请求附带了需要处理的数据。为此,控制器借助了应用程序 [2a] 中的特定事件处理程序。 这些处理程序可能需要业务层 [2b]。事件处理程序可能需要更新某些 M 模型 [2c]。一旦处理了客户端的请求,该请求可能会触发各种响应。一个典型的例子是:
    • 如果请求无法正确处理,则显示错误页面
    • 否则显示确认页面

事件处理程序向控制器 [Faces Servlet] 返回一个字符串类型的结果,称为导航键

  1. 导航 - 控制器根据事件处理程序返回的导航键,选择要发送给客户端的页面 JSF(即视图)。
  2. 响应 - 选定的页面 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 默认生成的,其中还包含一个异常页面的配置:

<?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">
  <context-param>
    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
    <param-value>client</param-value>
  </context-param>  
  <context-param>
    <param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
    <param-value>true</param-value>
  </context-param> 
  <context-param>
    <param-name>javax.faces.PROJECT_STAGE</param-name>
    <param-value>Development</param-value>
  </context-param>
  <servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>/faces/*</url-pattern>
  </servlet-mapping>
  <session-config>
    <session-timeout>
      30
    </session-timeout>
  </session-config>
  <welcome-file-list>
    <welcome-file>faces/index.xhtml</welcome-file>
  </welcome-file-list>
  <error-page>
    <error-code>500</error-code>
    <location>/faces/exception.xhtml</location>
  </error-page>
  <error-page>
    <exception-type>java.lang.Exception</exception-type>
    <location>/faces/exception.xhtml</location>
  </error-page>
</web-app>
  • 第 30 行:[index.html] 是应用程序的首页
  • 第 32-39 行:异常页面的配置

页面 [exception.html] 源自 [ref3]。其代码如下:

<?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">
  <f:view locale="#{changeLocale.locale}">
    <h:head>
      <title>JSF</title>
      <h:outputStylesheet library="css" name="styles.css"/>
    </h:head>
    <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
      <h:form id="formulaire">
        <h3><h:outputText value="#{msg['exception.header']}"/></h3>
        <h:panelGrid columnClasses="col1,col2" columns="2" border="1">
          <h:outputText value="#{msg['exception.httpCode']}"/>
          <h:outputText value="#{requestScope['javax.servlet.error.status_code']}"/>
          <h:outputText value="#{msg['exception.message']}"/>
          <h:outputText value="#{requestScope['javax.servlet.error.exception']}"/>
          <h:outputText value="#{msg['exception.requestUri']}"/>
          <h:outputText value="#{requestScope['javax.servlet.error.request_uri']}"/>
          <h:outputText value="#{msg['exception.servletName']}"/>
          <h:outputText value="#{requestScope['javax.servlet.error.servlet_name']}"/>
        </h:panelGrid>
      </h:form>
    </h:body>
  </f:view>
</html>

任何未被 Web 应用程序代码明确处理的异常都会导致显示如下所示的页面:

文件 [faces-config.xml] 的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!-- =========== FULL CONFIGURATION FILE ================================== -->
<faces-config version="2.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-facesconfig_2_0.xsd">

  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
    <message-bundle>messages</message-bundle>
  </application>
</faces-config>

请注意以下几点:

  • 第 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 代码示例:


<h:outputText value="#{msg['form.infos.employé']}"
 styleClass="titreInfos"/>

<h:panelGrid columns="3" 
rowClasses="libelle,info">

<h:message for="heuresTravaillées"
 styleClass="error"/>

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
模板
角色
1
comboEmployes
<h:selectOneMenu>
字符串 comboEmployesValue
List<Employe> getEmployes()
包含员工列表,格式为
“名 姓”。
2
heuresTravaillees
<h:inputText>
字符串 heuresTravaillées
实际工作小时数
3
joursTravailles
<h:inputText>
字符串 joursTravaillés
工作天数 - 整数
4
btnSalaire
<h:commandButton>
 
启动工资计算
5
btnRaz
<h:commandButton>
 
将表单恢复为初始状态
  • 方法 getEmployes 将返回从 [métier] 层获取的员工列表。 下拉列表中显示的对象将具有属性 itemValue(即员工的编号 SS),以及属性 itemLabel(由员工的姓和名组成的字符串)。
  • 按钮 [Salaire] 和 [Raz] 目前尚未连接到事件处理程序。
  • 将验证输入内容的有效性。

Image

请测试此版本。请特别确认输入错误是否已正确提示。

注意:页面组件的 id 属性中切勿包含带重音的字符。在 Glassfish 3.1.2 环境下,这会导致应用程序崩溃。

10.4.2. 步骤 2


问题:填写表单 [index.xhtml] 及其模板 [Form.java],确保点击按钮 [Salaire] 后显示以下页面:


按钮 [Salaire] 将连接到模板的事件处理程序 calculerSalaire。该方法将调用 [métier] 层中的方法 calculerFeuilleSalaire。 该工资单将针对在 [1] 中选定的员工生成。

在模板中,工资单将由以下私有字段表示:


  private FeuilleSalaire feuilleSalaire;

该字段包含方法 getset

要获取该对象中的信息,可在页面 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部分

Image

可通过以下方式使用 <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;

并配合其方法 getset。 处理按钮点击的 [Salaire] 和 [Raz] 方法将根据视图 viewInfos 是否需要显示来更新该布尔值。