vlambda博客
学习文章列表

读书笔记《digital-java-ee-7-web-application-development》JSF流程和Finesse

Chapter 6. JSF Flows and Finesse

 

“我有机会驾驶过很多不同的飞机,但这与乘坐穿梭飞机完全不同。”

 
  --Commander Chris Hadfield

本章介绍的是 JSF 2.2 中的一个新特性 Faces Flow。流的概念源于工作流和业务流程管理的概念。工作流通常是为了有效地完成一个可实现的工作单元而执行的一系列精心安排且可重复的业务活动。工作单元可以涉及状态转换、数据处理和/或服务或信息的提供。

许多 Web 电子商务应用程序中的结帐流程是用户看到的工作流的一个很好的示例。当您从亚马逊购买产品时,该网站会将您带到网站的一个单独区域以输入详细信息。在幕后,亚马逊会优雅地将您从负责处理电子和摄影部分产品的微服务转移到作为结账工作流程第一步的微服务。您登录您的帐户或创建一个新帐户,然后您决定送货地址。接下来,您使用信用卡或借记卡付款,亚马逊将向您提供发票地址。最后,您可以选择您希望产品的交付方式。您可以选择将物品组合在一起,也可以选择快递或常规递送。亚马逊是一个复杂的复制工作流程;但是,JSF 允许数字开发人员从基本的简单流程开始构建。

工作流也出现在富用户客户端应用程序中,供使用台式计算机的人使用,尤其是在政府和金融服务行业。您可能已经见证过类似工作流的应用程序,例如案例工作系统、交易系统和仓库系统。这个想法本质上是相同的,即引导员工从头到尾完成业务流程中的各个步骤。

在 JSF 2.2 中,Faces Flow 提供了基本的编程 API 来创建类似于一般应用程序中的工作流的行为和用户体验。开源框架,例如 Apache MyFaces CODI(编排模块)、Spring Web Flow 和专有的 Oracle Application开发框架 (ADF) 启发了 Faces Flow 的设计。

What is Faces Flow?


Faces Flow 是将具有特殊范围的支持 bean 与相关页面封装到 模块中。 Faces Flow 是一个具有单个、定义明确的入口点和一个或多个出口点的模块。应用程序开发人员确定 Faces Flow 的组成方式及其运行方式。换句话说,Faces Flow 是一个低级 API,而其他框架,特别是 BPM,具有更高级别的配置和宏观级别的流程。

  • JSF Faces Flow 在执行中是模块化的;一个流可以嵌套方式调用另一个流。

  • Faces Flow 可以将参数传递给另一个嵌套流,并且嵌套流还可以通过称为 Flow Scope 的特殊映射属性返回数据。

  • 应用开发者可以将一个流和相应的页面打包成一个模块,可以分发给第三方开发者。

  • 有一个名为 FlowScoped 的全新作用域,它表示 POJO 是否为流作用域 bean。对此的注释是 @javax.faces.flow.FlowScoped。流范围 bean 与 CDI 兼容;因此您可以使用熟悉的 Java EE 注释并订购对其他 bean 和 EJB 元素的注入引用。

  • 您可以像使用 @RequestScoped, @ConversationScoped 一样编写动作控制器方法并处理流范围 bean 中的逻辑、@SessionScoped@ApplicationScoped bean。

Flow definitions and lifecycle


Faces Flows 使用 @FlowScoped bean,用户可以在其中输入单个页面,称为 首页。进入流程后,用户可以浏览与流程相关联的页面。用户可以在预定义的点退出流程。 流可以调用嵌套流。

@FlowScoped bean 的生命周期大于 @ViewScoped bean,但短于 @SessionScoped。因此,我们可以将流范围 bean 与它们的会话兄弟进行比较。 @ConversationalScoped bean 维护浏览器中所有视图和网页选项卡的状态。像他们的对话伙伴一样,@FlowScoped bean 可以承受多个请求;事实上,它们甚至更好,因为它们在会话中具有多个窗口的不同实例。流范围的 bean 不在浏览器选项卡之间共享。

当用户进入和离开应用程序中的流时,Faces Flows 有一个专用的 CDI 范围,JSF 框架实现使用它来激活和钝化 bean 的数据。

一旦用户离开流程,该实例就容易受到 JVM 的垃圾收集。因此,与 @SessionScoped@ConversationalScoped bean 相比,流 往往具有较低的内存需求。

以下 图说明了 Faces Flow 的范围。

读书笔记《digital-java-ee-7-web-application-development》JSF流程和Finesse

Simple Implicit Faces Flows


仅使用文件夹名称、空的 XML 配置和一些 Facelet 页面来创建隐式面孔流相对简单。 flow 是 Web 应用程序中的文件夹名称,最好位于根目录中。我们从名为 digitalFlow 的相同 目录中的基本流开始。您的流程必须与文件夹的名称匹配。

为了定义一个隐式流,我们创建一个空的 XML 文件,该文件具有通用的基本名称和后缀:digitalFlow/digitalFlow-flow.xhtml

我们现在在具有通用基本名称的文件夹中创建一个起始页。该文件是一个名为 digitalFlow/digitalFlow.xhtml 的 Facelet 视图页面。

我们可以在文件夹内的流程中创建其他页面,它们可以具有我们喜欢的任何名称。我们可能有 digitalFlow/digitalFlow1.xhtmldigitalFlow/checkout.xhtmldigitalFlow/歌曲.xhtml。只有定义的流 digitalFlow 才能访问这些页面。如果外部调用确实尝试访问这些页面中的任何一个,JSF 实现将报告错误。

为了退出隐式流,我们必须提供一个特殊的页面,/digitalFlow-return.xhtml ,在 Web 应用程序的根文件夹中,这意味着该文件位于该文件夹之外。

Implicit navigation

让我们在第一个 Faces Flow 导航示例中充分利用这些知识。在源代码中,项目被称为jsf-implicit-simple-flow。检查此项目的文件布局很有帮助,如下所示:

src/main/java

src/main/webapp

src/main/webapp/WEB-INF/classes/META-INF

src/main/webapp/index.xhtml

src/main/webapp/assets/

src/main/webapp/resources/

src/main/webapp/basic-layout.xhtml

src/main/webapp/view-expired.xhtml

src/main/webapp/digitalFlow/

src/main/webapp/digitalFlow/digitalFlow.xml

src/main/webapp/digitalFlow/digitalFlow.xhtml

src/main/webapp/digitalFlow/digitalFlow-p2.xthml

src/main/webapp/digitalFlow/digitalFlow-p2.xthml

src/main/webapp/digitalFlow/digitalFlow-p4.xthml

src/main/webapp/digitalFlow-return.xhtml

当您研究前面的布局时,您会注意到该项目有一个标准主页,称为 index.xhtml,正如我们所期望的那样。它有一个 digitalFlow 文件夹,这是专门用于此 Faces Flow 的网站的特殊区域。在这个目录中,有一堆 Facelet 文件和一个配置。起始页名为 digitalFlow.xhtml,并且为流定义保留了一个空 XML 文件 digitalFlow.xml

到目前为止,您已经知道资产和资源文件夹的用途,但我们很快就会回到 view-expired.xhtml 文件。我们如何确保我们的文件夹结构被视为 Faces Flow?

A Flow scoped bean

使用注解 @javax.faces.flow.FlowScoped,我们将 POJO 定义为流作用域 bean。 下面是我们第一个 Faces Flow 的代码,它是一个支持 bean:

package uk.co.xenonique.digital.flows.control;
import javax.faces.flow.FlowScoped;
import javax.inject.Named;
import java.io.Serializable;

@Named
@FlowScoped("digitalFlow")
public class DigitalFlow implements Serializable {
    public String debugClassName() {
        return this.getClass().getSimpleName();
    }

    public String gotoPage1() {
        return "digitalFlow.xhtml";
    }

    public String gotoPage2() {
        return "digitalFlow-p2.xhtml";
    }

    public String gotoPage3() {
        return "digitalFlow-p3.xhtml";
    }

    public String gotoPage4() {
        return "digitalFlow-p4.xhtml";
    }

    public String gotoEndFlow() {
        return "/digitalFlow-return.xhtml";
    }
}

简单的控制器类 DigitalFlow 具有 gotoPage1()gotoPage2( ) 将用户移动到流程中的相应页面。 gotoEndFlow() 方法导航到 JSF 检测到的返回 Facelet 视图以退出流程。

@FlowScoped 的注解需要一个字符串值参数,在这种情况下与文件夹的名称 digitalFlow 匹配。我们现在将转到视图。

Facelet views

我们在 Facelet 视图中使用流范围 bean,就像我们使用任何其他 CDI 范围 bean 一样。以下 是主页的摘录,index.xhtml

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://xmlns.jcp.org/jsf/passthrough" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core" >

  <ui:composition template="/basic_layout.xhtml">
    <ui:define name="mainContent">
      <h1> JSF Implicit Simple Flow</h1>

      <p>
        Welcome to a simple Faces Flow...
      </p>

      <div class="content-wrapper">
        <h:form>
          <h:commandButton styleClass="btn btn-primary btn-lg" action="digitalFlow" value="Enter Digital Flow" />
        </h:form>
      </div>

      <!-- ... -->

    </ui:define> <!--name="mainContent" -->
  </ui:composition>
</html>

在前面的视图中,<h:commandButton> 定义了一个名为流的操作,digitalFlow。调用此操作会导致 JSF 进入 Faces Flow,它与支持 bean 的相应注释名称相匹配。

JSF 识别出流具有隐式导航,因为 XML 流定义文件 digitalFlow.xml 是空的。该文件必须存在;否则,实现会报错。您也不需要文件中的开始或结束标签。

当用户调用该按钮时,JSF 在将其转发到起始页之前实例化一个流范围的 bean。以下是流digitalFlow.xhtml的起始页的摘录:

<html xmlns="http://www.w3.org/1999/xhtml" ...>
<ui:composition template="/basic_layout.xhtml> <ui:define name="mainContent">
    <!-- ... -->
    <div class="content-wrapper">
      <h1>Page <code>digitalFlow.xhtml</code></h1>
      <p>View is part of a flow scope? <code>
      #{null != facesContext.application.flowHandler.currentFlow}
      </code>.</p>

      <table class="table table-bordered table-striped">
          <tr>
              <th>Expression</th>
              <th>Value</th>
          </tr>
          <tr>
              <td>digitalFlow.debugClassName()</td>
              <td>#{digitalFlow.debugClassName()}</td>
          </tr>
      </table>

      <h:form prependId="false">
        <h:commandButton id="nextBtn1" styleClass="btn btn-primary btn-lg" value="Next Direct" action="digitalFlow-2" />
        &#160;
        <h:commandButton id="nextBtn2" styleClass="btn btn-primary btn-lg" value="Next Via Bean" action="#{digitalFlow.gotoPage2()}" />
        &#160;
        <h:commandButton id="exitFlowBtn1" styleClass="btn btn-primary btn-lg" value="Exit Direct" action="/digitalFlow-return" />
        &#160;
        <h:commandButton id="exitFlowBtn2" styleClass="btn btn-primary btn-lg" value="Exit Via Bean" action="#{digitalFlow.gotoEndFlow()}" />
        &#160;
      </h:form>
    </div>

  </ui:define> <!--name="mainContent" -->
</ui:composition>
</html>

该视图演示了使用熟悉的表达式语言以及直接的页面到页面导航来调用流范围 bean。为了允许用户移动到流程中的第二页,我们有两个命令按钮。具有属性 action 和值 digitalFlow-2 的命令按钮是直接页面导航,无需验证任何表单输入。具有属性 action 和表达式语言值的命令按钮 #{digitalFlow.gotoPage2()} 是一个调用< /a> 到流作用域 bean 的方法,这意味着执行整个 JSF 生命周期。

Tip

请参阅 第 2 章JavaServer Faces Lifecycle,如果您忘记了生命周期中的不同阶段。

在此视图中,我们还生成输出 #{digitalFlow.debugClassName()},以说明我们可以在流范围 bean 中调用任意方法。

让我也提请您注意通过以下实际内容确定视图是否是流程的一部分的表达语言:

#{null != facesContext.application.flowHandler.currentFlow}

这在功能上等同于以下 Java 语句:

null == FacesContext.getCurrentInstance().getApplication()
   .getFlowHandler().getCurrentFlow()

其他页面 digitalFlow-p2.xhtmldigitalFlow-p3.xhtml 非常相似,因为它们只是添加了命令按钮导航回以前的视图。您可以在本书的源代码分发中看到完整的代码。

Tip

在数字工作中,我们经常为某些 HTML 元素提供众所周知的标识符 (ID),尤其是表单控件。这有助于我们为 Web 自动化测试编写测试脚本,尤其是使用 Selenium Web Driver 框架(http://www.seleniumhq.org/)。

我们将跳转到最后一个视图,digitalFlow-p4.xhtml,然后提取表单元素进行研究:

<h:form prependId="false">
  <h:commandButton id="prevBtn1" styleClass="btn btn-primary btn-lg" value="Prev Direct" action="digitalFlow-p3" />
  &#160;
  <h:commandButton id="prevBtn2" styleClass="btn btn-primary btn-lg" value="Prev Via Bean" action="#{digitalFlow.gotoPage3()}" />
  &#160;
  <h:commandButton id="exitFlowBtn1" styleClass="btn btn-primary btn-lg" value="Exit Direct" action="/digitalFlow-return" />
  &#160;
  <h:commandButton id="exitFlowBtn2" styleClass="btn btn-primary btn-lg" value="Exit Via Bean" action="#{digitalFlow.gotoEndFlow()}" />
  &#160;
</h:form>

我们可以 看到,前面的内容说明了如何直接导航并通过 DigitalFlow 支持 bean。为了在隐式导航中退出流程,用户必须触发一个事件,将 JSF 框架带到 /digitalFlow-return.xhtml 视图,这会导致流程完成.直接导航到返回模式会避免验证,并且表单输入元素内的任何数据都会丢失。如果我们想将表单输入元素验证为表单请求,我们必须调用动作控制器中的一个方法,即支持 bean。

这就是 JSF Faces Flow 中的隐式导航。

真的就是这么简单,所以让我们看一些从主页 /index.html 开始的简单流程的截图:

读书笔记《digital-java-ee-7-web-application-development》JSF流程和Finesse

DigitalFlow 的起始页 digital-flow.xhtml 如下图所示:

读书笔记《digital-java-ee-7-web-application-development》JSF流程和Finesse

退出流程后,用户看到视图/digitalFlow-return.xhtml。以下是此视图的屏幕截图:

读书笔记《digital-java-ee-7-web-application-development》JSF流程和Finesse

如果我们在进入之前 尝试通过已知 URI /digitalFlow/digitalFlow.xhtml 直接访问 Facelet 视图流程,视图将类似于以下屏幕截图:

读书笔记《digital-java-ee-7-web-application-development》JSF流程和Finesse

在 GlassFish 4.1 中,我们收到 500 的 HTTP 响应代码,这是一个内部服务器 错误。此外,CDI 容器引发了一个异常,即没有活动的流范围。

Handling view expired


我向您保证我们会为我们的数字应用程序添加一些技巧。如果您已经使用 JSF 有一段时间了,那么您可能有相当多的堆栈跟踪,其中 javax.faces.application.ViewExpiredException 是根本原因。这是最臭名昭著的例外之一。您可以增加 HTTP 会话的生命周期以补偿过期的请求,但是对于普通人来说离开计算机需要多长时间?同时,对象将保留在内存中。有一种更好的方法,那就是使用 Web XML 部署描述符。

在应用程序 web.xml 文件中,我们需要触发重定向到更令人愉快的错误页面。以下是 XML 文件的摘录:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" ...>
  <!-- ... -->
  <error-page>
    <exception-type>
    javax.faces.application.ViewExpiredException
    </exception-type>
    <location>/view-expired.xhtml</location>
  </error-page>
  <!-- ... -->
</web-app>

<error-page> 元素指定异常类型和页面视图之间的关联。每当 JSF 遇到异常类型 ViewExpiredException 时,它都会将响应动作推进到页面视图 /view-expired.xhtml。以下屏幕截图说明了此行为的情况:

读书笔记《digital-java-ee-7-web-application-development》JSF流程和Finesse

我相信您会同意我们的公共客户会喜欢这种改进的专用页面视图,而不是对堆栈跟踪感到困惑。

Tip

流范围的 bean 依赖于 CDI,因此,它们需要 Java EE 7 环境,该环境指示 CDI 1.1 容器,例如 JBoss Weld。您还必须使用 @Named 而不是旧样式的 @ManagedBean 注释。如果您不使用 WildFly 8 或 GlassFish 4,那么在深入研究代码之前,请检查您的容器对最新 JSF 和 CDI 规范的实现支持。

A comparison with conversational scoped beans

如果您还记得,在使用 @ConversationScoped bean 时,我们必须明确划分会话的状态 。我们注入了Conversation实例,并从那里,在数字客户旅程的特定点,称为< code class="literal">begin() 和 end() 方法。使用 @FlowScoped CDI bean,范围会在定义的点自动开始和结束。

Capturing the lifecycle of flow scoped beans

由于 CDI 容器管理 流作用域的 bean,它们可以正常参与上下文生命周期。我们可以在中用@PostConstruct注解一个方法来初始化bean,获取一个 数据库资源,或计算可缓存数据。同样,当流超出范围时,我们可以使用 @PreDestroy 注释方法。

Declarative and nested flows


到目前为止,我们已经 看到了隐式流的作用。对于最简单的流,隐式流非常直接,它的执行就像一个基本的网络向导,用户可以在其中线性导航、前进和后退。它还可以使用随机访问来导航到页面。

如果我们想进一步了解 Faces Flow,那么我们必须深入研究 XML 流定义,但首先让我们定义一些术语。

The flow node terminology

基础技术受到工作流和 BPM 的启发,Faces Flow 规范声明了下表中给出的不同类型的节点:

节点类型名称

描述

查看

表示任何类型的应用程序 JSF 视图

方法调用

表示通过表达式语言EL)

流调用

表示使用出站(调用)和(返回)入站参数调用另一个流

流量返回

表示返回调用流程

切换

表示通过 EL 确定的逻辑进行的导航选择

下面是代表购物车业务流程的两个流的图示。外部流调用处理交付方法的嵌套流。

读书笔记《digital-java-ee-7-web-application-development》JSF流程和Finesse

An XML flow definition description file

鉴于 <<Flowname>> 是 Web 应用程序中的文件夹名称,流描述文件名 匹配模式 <<Flowname>>/<<Flowname>>.xml。该描述符文件的内容向 JSF 声明了流的某些 特征。它可以定义一个替代起始页面,定义一组返回结果,并将某些结果映射到特定页面。描述符还可以定义将结果映射到特定页面的条件布尔逻辑。

以下是 XML 流定义文件的示例:

<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">
  <flow-definition id="flow-id">
  
    <start-node>startPage</start-node>
    <view id="startPage">
      <vdl-document>/flowname/start.xhtml</vdl-document>
    </view>

    <view id="inside-flow-id-1"> <vdl-document>
      /flowname/inside-flow-id-1.xhtml </vdl-document>
    </view>
    <view id="inside-flow-id-2"> <vdl-document>
      /flowname/inside-flow-id-2.xhtml </vdl-document>
    </view>

    <flow-return id="return-from-flow-id-1">
      <from-outcome>/outside-page-1</from-outcome>
    </flow-return>
    <flow-return id="return-from-flow-id-2">
      <from-outcome>/outside-page-21</from-outcome>
    </flow-return>

  </flow-definition>
</faces-config>

元素必须是 <faces-config> 标签 使用适当的 XML 命名空间。您可能会对这种根元素的选择感到惊讶。这是因为通过在 /WEB-INF/faces-config.xml 文件中设置流定义,可以在整个应用程序中全局定义流定义。但是,这种做法是一种高级用例,不建议用于模块化开发。

A flow definition tag

<flow-definition> 元素使用定义的流 ID 建立 Faces Flow。标识符的值必须@FlowScoped bean。此元素包含用于建立起始页、视图、流返回或条件 switch 语句的标记集合。

A mandatory flow return tag

Faces Flow 必须至少有一个返回 结果。 <flow-return> 元素建立一个带有 ID 的流返回节点。它必须包含一个 <flow-outcome> 元素,其主体内容指定 Facelet 视图。

A view page tag

流可以 范围。元素<view>标签通过元素<vdl-document>建立视图描述语言节点。视图需要标识符。这个标签的正文内容只是引用了一个 Facelet 视图。因此,视图 ID 不一定与 Facelet 视图同名。

An optional start page tag

开发者 可以覆盖默认起始页的 名称并提供替代视图.元素 <start-node> 的主体内容指定了视图 ID,因此引用了适当的流程,即视图节点。

Switch, conditional, and case tags

流定义 为开发人员提供了 定义条件 逻辑通过一个名为 <switch> 的元素标签。这是 XML 中最高级别的低级特性。 <switch>标签指定一个Switch Node,和使用 <if> 标签在 /WEB-INF/faces-config.xml 文件中的 <navigation-case> 中,显然对于非流量导航。通过使用条件逻辑评估 EL 表达式,该开关允许流中的单个结果映射多个 Facelet 视图。

以下是 XML 流定义示例文件的扩展版本:

<faces-config version="2.2" ...>
  <flow-definition id="flow-id">
    ...
    <switch id="customerPaymentTab">
      <case>
        <if>
          #{controller.paymentType == 'CreditCard'}
        </if>
        <from-outcome>creditcard</from-outcome>
      </case>
      <case>
        <if>
          #{controller.paymentType == 'DebitCard'}
        </if>
        <from-outcome>debitcard</from-outcome>
      </case>
      <case>
        <if>
          #{controller.paymentType == 'PayPal'}
        </if>
        <from-outcome>PayPal</from-outcome>
      </case>
      <default-outcome>bacs-direct</default-outcome>
    </switch>
    ...
  </flow-definition>
</faces-config>

<switch> 标签包含许多 <case> 元素和单个 <default-outcome> 元素。每个 <case> 元素都包含一个 <if><from-结果> 元素。 <if> 的 body 内容定义了一个 条件逻辑EL。 <from-outcome> 映射到最终的 Facelet 视图,或者它引用 视图的标识符节点。为 Switch 节点设置默认结果是一个非常好的主意。 <default-outcome> 的主体内容在没有任何 case 条件发生计算为 true 时建立此结果。

在示例中,我们一个虚构的支付控制器用于电子商务网站的结帐流程。当流程遇到一个结果 customerPaymentTab,它是 Switch 节点的标识符时,JSF 按顺序处理每个案例条件逻辑。如果其中一个条件测试评估为真,则 JSF 选择该结果作为切换的结果。假设 #{controller.paymentType == 'DebitCard' } 为真,则借记卡是所选视图。如果没有一个测试评估为真,那么结果视图是 bacs-direct

Tip

但是不应该在控制器而不是开关节点中定义所有逻辑吗?无论哪种方式,答案都是有争议的,这取决于。如果您作为库开发人员正在使用复杂的 Faces Flows 图构建应用程序,则可以为外部配置器的这种灵活性争论不休。如果您正在构建简单的应用程序,那么遵循 YAGNI(您不需要它)的做法可能会受益于最小可行产品。

这涵盖了我们需要了解的有关基本流定义文件的所有内容。现在让我们转到嵌套的声明式流程。

A nested flow example

是时候看看一个使用嵌套声明式流定义的Faces Flow 的实际示例了。我们的应用程序很简单。它允许客户记录碳足迹数据。所以首先让我们定义一个数据记录:

package uk.co.xenonique.digital.flows.entity;
import javax.persistence.*;
import java.io.Serializable;

@Entity
@Table(name = "CARBON_FOOTPRINT")
@NamedQueries({
  @NamedQuery(name="CarbonFootprint.findAll",
    query = "select c from CarbonFootprint c "),
  @NamedQuery(name="CarbonFootprint.findById",
    query = "select c from CarbonFootprint c where c.id = :id"),
})
public class CarbonFootprint implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;

  private String applicationId;
  private String industryOrSector;
  // KWh (main source)
  private double electricity;
  // KWh (main source)
  private double naturalGas;
  // Litres (travel commute costs)
  private double diesel;
  // Litres (travel commute costs)
  private double petrol;

  public CarbonFootprint() { }
 
  // hashCode(), equals() and toString()
  // Getters and setters omited
}

CarbonFootprint 是一个 JPA 实体 bean,它声明了一组属性来存储客户的碳足迹数据。用户可以提供他们的行业或部门,以及他们在一段时间内消耗的电力、天然气、柴油和汽油的数量。该记录还有一个 applicationId 值,我们将使用它。

让我们查看这个项目的文件布局:

src/main/java

src/main/webapp

src/main/webapp/WEB-INF/classes/META-INF

src/main/webapp/index.xhtml

src/main/webapp/assets/

src/main/webapp/resources/

src/main/webapp/basic-layout.xhtml

src/main/webapp/view-expired.xhtml

src/main/webapp/section-flow/

src/main/webapp/section-flow/section-flow.xml

src/main/webapp/section-flow/section-flow.xhtml

src/main/webapp/section-flow/section-flow-1a.xthml

src/main/webapp/section-flow/section-flow-1b.xthml

src/main/webapp/section-flow/section-flow-1c.xthml

src/main/webapp/footprint-flow/

src/main/webapp/footprint-flow/footprint-flow.xml

src/main/webapp/footprint-flow/footprint-flow.xhtml

src/main/webapp/footprint-flow/footprint-flow-1a.xml

src/main/webapp/endflow.xhtml

该项目称为jsf-declarative-flows,可作为本书源代码的一部分。有两种流程:section-flowdigital-flow。部分流程捕获了这个虚构示例中的行业信息。足迹流捕获能源消耗数据。两个流共享一个客户详细信息记录,即 CarbonFootprint 实体对象,稍后您将在支持 bean 中看到它。

XML flow definitions

以下是 Sector流的XML流定义,sector-flow/sector-flow.xhtml

<faces-config version="2.2" ...>
  <flow-definition id="sector-flow">
    <flow-return id="goHome">
      <from-outcome>/index</from-outcome>
    </flow-return>
    <flow-return id="endFlow">
      <from-outcome>#{sectorFlow.gotoEndFlow()}</from-outcome>
    </flow-return>

    <flow-call id="callFootprintFlow">
      <flow-reference>
        <flow-id>footprint-flow</flow-id>
      </flow-reference>
      <outbound-parameter>
        <name>param1FromSectorFlow</name>
        <value>param1 sectorFlow value</value>
      </outbound-parameter>
      <outbound-parameter>
        <name>param2FromSectorFlow</name>
        <value>param2 sectorFlow value</value>
      </outbound-parameter>
      <outbound-parameter>
        <name>param3FromSectorFlow</name>
        <value>#{sectorFlow.footprint}</value>
      </outbound-parameter>
      <outbound-parameter>
        <name>param4FromSectorFlow</name>
        <value>#{sectorFlow.footprint.applicationId}</value>
      </outbound-parameter>
    </flow-call>
  </flow-definition>
</faces-config>

此流的标识符是扇区流,它与文件夹名称匹配。它还为支持 bean、动作控制器建立了 @FlowScoped 值,我们将在后面看到。

Flow Return节点有两个,分别是goHomeendFlow goHome 可以直接导航到主页,而 endFlow 通过 EL 值调用 bean 上的操作,#{sectorFlow.gotoEndFlow()}。这种技术对于确保客户在完成数字旅程之前输入正确且经过验证的数据特别有用。

新节点 callFootprintFlow 表示嵌套流调用。 <flow-call> 元素定义了一个 Flow Call 节点。它必须有一个 <flow-reference> 标签元素和一个嵌套标签 <flow-id>。后者的正文内容定义了目标流的标识符。

<outbound-parameter> 元素指定如何将参数和值对传递到目标流。每个参数都需要一个 <name> 和一个 <value> 元素。调用流中的传出参数的名称必须与调用流中的传入参数的名称匹配。

param1FromSectorFlowparam2FromSectorFlow 演示了如何将文字字符串值从一个流传递到另一个流。如果要传递数值,则必须在目标流中自己对这些值进行编码和解码。参数 param3FromSectorFlowparam4FromSectorFlow 也说明了如何使用 EL。请注意我们如何轻松地将实体记录 #{sectorFlow.footprint} 从 Sector Flow 传递到 Footprint Flow。我们也可以像在最后一个参数中所做的那样传递一个单独的属性:#{sectorFlow.footprint.applicationId}

以下是 Footprint Flow footprint-flow/footprint-flow.xml 的 XML 流定义:

<faces-config version="2.2" ...>
  <flow-definition id="footprint-flow">
    <flow-return id="goHome">
      <from-outcome>/index</from-outcome>
    </flow-return>
    <flow-return id="exitFromFootprintFlow">
      <from-outcome>#{footprintFlow.exitFromFootprintFlow}</from-outcome>
    </flow-return>
    <flow-return id="exitToSectionFlow">
      <from-outcome>/section-flow</from-outcome>
    </flow-return>

    <inbound-parameter>
      <name>param1FromSectorFlow</name>
      <value>#{flowScope.param1Value}</value>
    </inbound-parameter>
    <inbound-parameter>
      <name>param2FromSectorFlow</name>
      <value>#{flowScope.param2Value}</value>
    </inbound-parameter>
    <inbound-parameter>
      <name>param3FromSectorFlow</name>
      <value>#{flowScope.param3Value}</value>
    </inbound-parameter>
    <inbound-parameter>
      <name>param4FromSectorFlow</name>
      <value>#{flowScope.param4Value}</value>
    </inbound-parameter>

  </flow-definition>
</faces-config>

此流由名称footprint-flow.xml标识。它有一组回流节点。 goHome 节点实际上将流程退出到调用家,尽管视图文档值 /index 值。您可能会认为这种行为很奇怪。然而,JSF 是正确的,因为当前流 Footprint 是一个嵌套流,它驱动流指针指向调用流扇区的状态。 exitFromFootprintFlowexitToSectionFlow 节点分别代表不同的导航策略,间接和直接。

<inbound-parameter> 元素集指定流的传入参数。参数的名称非常重要,因为它们必须与调用流程中相应的 元素中的名称相匹配。这些值定义了 EL 对象引用,它说明了这些值的写入位置。换句话说,Faces Flow 中参数的传递就像映射的属性名称传递一样。

我们来分析sector-flow.xml中的第三个参数。它将实体记录实例 CarbonFootprint 的值发送到嵌套的足迹流:

<outbound-parameter>
  <name>param3FromSectorFlow</name>
  <value>#{sectorFlow.footprint}</value>
</outbound-parameter>

footprint-flow.xml 中,入站参数名称与传入参数名称匹配:

<inbound-parameter>
  <name>param3FromSectorFlow</name>
  <value>#{flowScope.param3Value}</value>
</inbound-parameter>

EL 指定 JSF 将参数值设置为对象属性的位置。在 这种情况下,flowScope 映射集合有一个键和一个值集。我们可以使用生命周期大于或等于流生命周期的任何现有对象。我们倾向于使用 flowScope,因为它旨在在 Face Flows 之间传递参数。因为我们可以引用对象中的属性,所以我们也可以从嵌套流中返回信息。调用流程可以在嵌套流程结束后检索该值。

现在我们从 XML 的角度理解了嵌套流,我们可以查看 Java 源代码。

Flow beans

sector-flow 支持 bean 如下所示:

package uk.co.xenonique.digital.flows.control;
import uk.co.xenonique.digital.flows.boundary.*;
import uk.co.xenonique.digital.flows.entity.*;
import uk.co.xenonique.digital.flows.utils.UtilityHelper;
// ...
@Named
@FlowScoped("sector-flow")
public class SectorFlow implements Serializable {
  @Inject UtilityHelper utilityHelper;
  @Inject CarbonFootprintService service;

  private CarbonFootprint footprint
   = new CarbonFootprint();

  public SectorFlow() {}

  @PostConstruct
  public void initialize() {
    footprint.setApplicationId(
      utilityHelper.getNextApplicationId());
  }

  public String gotoEndFlow() {
    return "/endflow.xhtml";
  }

  public String debugClassName() {
    return this.getClass().getSimpleName();
  }

  public String saveFootprintRecord() {
    service.add(footprint);
    return "sector-flow-1c.xhtml";
  }

  // Getters and setters ...
}

SectorFlow@FlowScoped 注解,值匹配流定义 XML 文件。我们用 @PostConstruct 注释方法 initialize() 以便在实体记录中设置随机应用程序 ID。请注意,这里我们不能将此逻辑放在普通的 Java 构造函数中,因为我们的 bean 是通过 CDI 容器赋予生命的。

我们将几个实例注入 SectorFlowUtilityHelper 是一个生成随机应用程序标识符的 @ApplicationScoped CDI POJO 类。有一个处理 JPA 持久性的有状态 EJB CarbonFootprintService

gotoEndflow() 方法是退出流程的导航结束。 saveFootprintRecord() 方法使用数据服务将 CarbonFootprint 实体存储到数据库中。

这样就完成了内部流程,即SectorFlow;嵌套支持 bean FootprintFlow 的代码如下:

@Named
@FlowScoped("footprint-flow")
public class FootprintFlow implements Serializable {
  private CarbonFootprint footprint;
  public FootprintFlow() { }

  @PostConstruct
  public void initialize() {}
    Map<Object,Object> flowMap =
      FacesContext.getCurrentInstance()
        .getApplication().getFlowHandler()
        .getCurrentFlowScope();
    footprint = (CarbonFootprint) flowMap.get("param3Value");
  }

  public String exitFromFootprintFlow() {
    return "/endflow.xhtml";
  }

  public String gotoPage1() {
    return "footprint-flow";
  }

  public String gotoPage2() {
    return "footprint-flow-1a";
  }

  public String debugClassName() {
    return this.getClass().getSimpleName();
  }

  // Getters and setters ...
}

我们重复 和以前一样的技巧;我们用 @PostConstruct 注释 FootprintFlow 类。在 initialize() 方法中,我们通过 FacesContext 实例以编程方式从流范围中检索对象。请注意,参数名称 param3Value 必须与 XML 定义中的值一致。流范围内的值恰好是 CarbonFootprint 实体。

您一定想知道为什么我们要麻烦地从作用域中检索实体并将其设置为 bean 属性?这只是一个示例,并允许页面内容设计者在页面中使用一致的标记。 EL #{footprintFlow.footprint.diesel}#{flowScope.param3Value.diesel} 更容易理解。

我们现在将继续讨论标记。

Page views

页面浏览量的 内容标记我们现在已经很熟悉了。我们来研究一下sector-flow/sector-flow.xhtml

<h1>Page <code>sector-flow.xhtml</code></h1>
...
<table class="table table-bordered table-striped">
  <tr>
    <th>Expression</th>
    <th>Value</th>
  </tr>
  ...
  <tr>
    <td>sectorFlow.footprint.applicationId</td>
    <td>#{sectorFlow.footprint.applicationId}</td>
  </tr>
  <tr>
    <td>sectorFlow.footprint</td>
    <td>#{sectorFlow.footprint}</td>
  </tr>
</table>

<h:form prependId="false">
  <div class="form-group">
    <label jsf:for="exampleInputEmail1">
    Email address</label>
    <input type="email" class="form-control" jsf:id="exampleInputEmail1" placeholder="Enter email" jsf:value="#{flowScope.email}"/>
  </div>
  <div class="form-group">
    <h:outputLabel for="industryOrSector">Industry or Sector</h:outputLabel>
    <h:inputText type="text" class="form-control" id="industryOrSector" placeholder="Your industry sector" value="#{sectorFlow.footprint.industryOrSector}"/>
  </div>

  <h:commandButton id="nextBtn" styleClass="btn btn-primary btn-lg" value="Next" action="sector-flow-1a" />
  &#160;
  <h:commandButton id="exitFlowBtn" styleClass="btn btn-primary btn-lg" value="Exit Flow" action="endFlow" />
  &#160;
  <h:commandButton id="homeBtn" styleClass="btn btn-primary btn-lg" value="Home" action="goHome" />
  &#160;
  <h:commandButton id="callFootPrintFlowBtn" styleClass="btn btn-primary btn-lg" value="Call Footprint" action="callFootprintFlow" />
  &#160;
  <h:commandButton id="saveBtn" styleClass="btn btn-primary btn-lg" value="Save Record" action="#{sectorFlow.saveFootprintRecord()}" />
  &#160;
</h:form>

视图混合使用了 HTML5 友好标记和标准 JSF 标记。输入文本字段 exampleInputEmail1 及其相关标签提醒我们 HTML5 友好标记。此输入控件与流范围映射集合相关联,即#{flowScope.email}。答案是肯定的,我们可以写这个,但我们最好将数据值存储在应用程序的某个地方!

输入元素 industryOrSector 向我们展示了 JSF 标准标签,它直接与 CarbonFootprint 实体记录相关联。

让我提请您注意命令按钮 saveBtn,它调用支持 bean 中的操作方法 saveFootprintRecord()。最后,有一个由 callFootPrintFlowBtn 标识的专用命令按钮,它调用嵌套流。 callFootprintFlow 动作与 sector-flow.xml 的 XML 流定义文件中的节点完全匹配。

有一个 nextBtn 命令按钮,可以直接导航到流程中的下一个视图。 homeBtn 命令按钮退出流程并返回主页。

视图的其余部分在 HTML Bootstrap 样式表中显示可调试输出。 JSF <h:form> 元素上的 prependId=false 通知 JSF 避免为控件添加属性 HTML 标识符.

Note

JSF 表单 prependId 指定 <h:form> 是否应在 code class="literal">clientId 生成过程。当导入或插入外部表单中的复合组件时,该标志变得相关。此值默认为 true。

以下是起始页sector-flow/sector-flow.xhtml的截图:

读书笔记《digital-java-ee-7-web-application-development》JSF流程和Finesse

在 source 代码中,我们使用 EL 导出了一组可调试值。该项目在本书的源代码中称为 jsf-declarative-form。你会发现一个没有可调试输出的清理专业版,称为 jsf-declarative-form-pro

以下是嵌套流的屏幕截图,footprint-flow-1a.xhtml。导航到开始页面后,我们点击 Next 按钮,在表单中输入一些数据并保存。

读书笔记《digital-java-ee-7-web-application-development》JSF流程和Finesse

当我们调用 SaveBtn回到 SectorFlow > 命令按钮,JSF 调用 saveFootprintRecord() 方法。记录被保存到数据库中,视图 sector-flow-1c.xthml 如下图所示:

读书笔记《digital-java-ee-7-web-application-development》JSF流程和Finesse

唯一剩下的部分是注入的POJO的源代码,UtilityHelper和EJB,CarbonFootprintService。不幸的是,我们无法显示本书中的所有列表。

A real-world example


总结 Faces Flows,让我们看看向我们的应用程序添加功能。我们希望我们的 JSF 应用程序具有质量和技巧。在本书的源代码中,您可以在 项目jsf-product-flow 和<代码类="literal">jsf-product-flow-s2。第一个项目展示了概念的原型设计。第二个项目说明了改进和清理的数字设计,其质量足以呈现给业务利益相关者。

Ensure the application populates the database

通常,我们开发针对 UAT 数据库进行测试的应用程序。我们编写的代码 用测试信息填充数据库,但不会投入生产。在很多情况下,我们希望引导我们的应用程序只是为了检查是否引入了正确的模式。

我们的第一个想法是创建一个带有注释 @PostConstruct@ApplicationScoped POJO,这将解决我们的引导问题。我们可以编写一个 DataPopulator 类,其唯一目的是在开发应用程序中创建数据。尽管我们有一个公司范围的应用程序实例,但我们无法确保在 Web 应用程序启动后调用我们的 bean。

在 Java EE 中,我们可以使用 @javax.ejb.Startup@javax.ejb.Singleton 来启动 EJB。 @Startup 注释确保 EJB 容器在应用程序部署后初始化 bean。 @Singleton 注释表示会话bean,它保证应用程序中最多有一个实例。

以下是 DataPopulator bean 的代码:

package uk.co.xenonique.digital.product.utils;
import javax.annotation.PostConstruct;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.inject.Inject;

@Singleton
@Startup
public class DataPopulator {
  @Inject ExtendedPersistenceLoaderBean loaderBean;

  @PostConstruct
  public void populate() {
    loaderBean.loadData();
  }
}

因此,为了用数据填充应用程序,我们委托另一个 bean。 ExtendedPersistenceLoaderBean 是一个具有新 CDI 1.1 事务范围的 CDI bean。该委托的代码如下:

package uk.co.xenonique.digital.product.utils;
import uk.co.xenonique.digital.product.boundary.*;
import uk.co.xenonique.digital.product.entity.*;
import uk.co.xenonique.digital.product.entity.*;
import javax.annotation.*;
import javax.ejb.*;
import javax.inject.Inject;
import java.io.Serializable;
import java.util.*;

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
@javax.transaction.TransactionScoped
public class ExtendedPersistenceLoaderBean implements Serializable {
  public static final String DEFAULT = "digital";

  @Inject
  UserProfileService service;

  @PostConstruct
  public void init() { /* ... */ }

  @PreDestroy
  public void destroy() { /* ... */ }

  public void loadData() {
    UserRole userRole = new UserRole("user");
    UserRole managerRole = new UserRole("manager");

    List<UserProfile> users = Arrays.asList(
      new UserProfile("[email protected]", DEFAULT, userRole),
      new UserProfile("[email protected]", DEFAULT, userRole),
      new UserProfile("[email protected]", DEFAULT, managerRole),
    );

    for (UserProfile user: users) {
      service.add(user);
    }
  }
}

我们用 @TransactionScoped 注释 DataPopulator bean。每当我们在 @TranscationScoped bean 上调用一个方法时,该方法,即 CDI 容器,将激活一个事务或创建一个事务。单个事务将在同一个 bean 上的许多方法之间共享,或者在可能被调用的其他 @TranscactionScoped bean 之间共享。换句话说,事务上下文在参与的组件中传递,而不需要开发人员添加显式方法参数来传递 javax.transaction.TransactionContext 实例。

回过头来,我们为 DataPopulator bean 添加了另一个特殊注释。

只要在这个 bean 上调用任何方法,线程上下文就会与一个全新的事务相关联,因为我们用 @TransactionAttribute 注释该类并传入一个属性值 TranscationAttributeType.NEW。这会强制容器创建一个新事务,无论是否已经存在一个事务。事务范围的生命周期是调用 loadData() 的持续时间。该方法只是在数据库中创建几个 UserRole 实体,然后使用 UserProfile 实体创建用于登录的用户帐户.

最后,init()destroy() 方法只是将调试信息打印到控制台, 详细信息未显示在摘录中。

Tip

尽管 CDI 1.1 为 CDI bean 定义了 @javax.inject.Singleton 注释,但规范中没有定义 CDI bean 的确切启动,因为在 <代码类="literal">@javax.inject.Startup。因此,我们必须依赖 EJB 单例启动 bean。我们可能要等到 CDI 2.0 和 Java EE 8 才能看到这个注解。

现在我们有了用户配置文件和角色,我们如何保护 JSF 应用程序?

Securing page views and flows

如果您想 使用标准的 Java EE 库,那么 规范有容器管理的身份验证功能。为了利用此功能,您可以扩展 Web 部署描述符文件 web.xml,并向您的应用程序添加安全约束。

以下是 web.xml 文件中的安全约束示例:

<security-constraint>
  <web-resource-collection>
    <web-resource-name>public</web-resource-name>
    <url-pattern>/products/*</url-pattern>
    <url-pattern>/cart/*</url-pattern>
    <url-pattern>/checkout/*</url-pattern>
    <url-pattern>/promotions/*</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <role-name>*</role-name>
  </auth-constraint>
  <user-data-constraint>
    <transport-guarantee>NONE</transport-guarantee>
  </user-data-constraint>
</security-constraint>

<security-constraint>
  <web-resource-collection>
    <web-resource-name>admin</web-resource-name>
    <url-pattern>/admin/*</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <role-name>admin</role-name>
  </auth-constraint>
  <user-data-constraint>
    <transport-guarantee>CONFIDENTIAL</transport-guarantee>
  </user-data-constraint>
</security-constraint>

第一个安全约束将本网站的产品、购物车、结帐和促销页面限制为任何用户。请注意以 作为角色名称的通配符。第二个安全约束将本网站的管理页面仅限于具有管理员角色的用户。

<user-data-constraint> 元素声明页面视图是否可以通过 HTTP 或 HTTPS 访问。它指定所需的安全级别。可接受的值为 NONEINTEGRALCONFIDENTIAL。将传输保证设置为 CONFIDENTIAL 会通知应用程序服务器这些页面和资源只能通过 SSL 访问。 INTEGRAL 的值在通信中很重要,因为从客户端或服务器通过线路发送的数据不应以任何方式更改。

Tip

提示 Java EE 8 安全性——关于未来 TODO 的一句话(请注意我要检查更新 和进度。请参阅 https://javaee8.zeef.com/arjan.tijms)。 标准 Java EE 安全性没有太多替代方案。其他选择是 Apache Shiro (http://shiro.apache.org/) 或Spring Security(以前称为 Acegi)。希望 Java EE 8 将包含一个改造概念,或许还有一个单独的规范。

虽然标准机制快速且易于添加,但它是特定于应用程序服务器的。该机制仅适用于粗粒度资源,没有可以应用于 CDI bean 的注释。 Java EE 安全需要配置安全领域,它定义了用户组的角色。为了保护具有细粒度权限的网站,我们必须添加多个角色,这会导致高度复杂性。

可以为 JSF 和 Web 应用程序定义我们自己的自定义安全性。这种方法的优点是我们有细粒度的控制,它可以跨容器工作。另一方面,如果我们忽略 Java EE 安全标准特性,那么任何自制的安全实现都不太可能在野外被充分证明是安全的。这样的组件将无法通过基本的渗透测试。充其量,如果要求简单明了并且对复杂权限的要求很少,自定义安全性就可以工作。

为了创建自定义安全性,我们将定义一个唯一的 javax.servlet.ServletFilter,它可以保护对我们网站某些区域的访问。 LoginAuthenticationFilter 定义如下:

package uk.co.xenonique.digital.product.security;
import uk.co.xenonique.digital.product.control.LoginController;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.*;
import java.io.IOException;

@WebFilter(urlPatterns={"/protected/*", "/simple/*"})
public class LoginAuthenticationFilter implements Filter {
  private FilterConfig config;

  public void doFilter(ServletRequest req,
    ServletResponse resp, FilterChain chain)
    throws IOException, ServletException
  {
    final HttpServletRequest request =
      (HttpServletRequest)req;
    final HttpServletResponse response =
      (HttpServletResponse)resp;
    if (request.getSession().getAttribute(
      LoginController.LOGIN_KEY) == null) {
      response.sendRedirect(
      request.getContextPath()+LoginController.LOGIN_VIEW);
    } else {
      chain.doFilter(req, resp);
    }
  }

  public void init(FilterConfig config)
  throws ServletException {
    this.config = config;
  }

  public void destroy() {
      config = null;
  }
}

我们用 带有 @WebFilter 的类>我们想要保护的 URL 资源。如果用户尝试访问 /protected/*/simple/* 文件夹下的页面,则过滤器 < code class="literal">LoginAuthenticationFilter 被触发。 servlet 容器调用 doFilter() 方法,我们检查是否定义了 HTTP 会话属性。如果键 LoginController.LOGIN_KEY 确实存在,则用户登录到站点,否则,用户将被重定向到登录页面视图。

让我们将 移到支持 bean LoginController,其中 允许用户登录网站:

package uk.co.xenonique.digital.product.control;
// imports elided...

@Named("loginController") @RequestScoped
public class LoginController {
  public final static String LOGIN_KEY="LOGIN_USERNAME";
  public final static String LOGIN_VIEW="/login.xhtml";

  private String username;
  private String password;

  @Inject UserProfileService userProfileService;

  public boolean isLoggedIn() {
    return FacesContext.getCurrentInstance()
      .getExternalContext().getSessionMap()
      .get(LOGIN_KEY) != null;
  }

  public String login() {
      List<UserProfile> users =
        userProfileService.findById(username);
      if ( users.isEmpty()) {
        throw new IllegalArgumentException("unknown user");
      }
      if ( !users.get(0).getPassword().equals(password)) {
        throw new IllegalArgumentException("invalid password");
      }

      FacesContext.getCurrentInstance().getExternalContext()
        .getSessionMap().put(LOGIN_KEY, username);
      return "/protected/index?faces-redirect=true";
  }

  public String logout() {
    FacesContext.getCurrentInstance().getExternalContext()
      .getSessionMap().remove(LOGIN_KEY);
    return "/index?faces-redirect=true";
  }

  // Getters and setter omitted ...
}

LoginController 支持 bean 接受两个基于表单的参数:用户名和密码。它依赖注入的 UserProfileService 来查找 UserProfile 记录 用户名。在 login() 方法中,如果密码参数与实体匹配,则允许 用户登录记录。该方法将用户名添加到密钥 LOGIN_KEY 下的 HTTP 会话中。

有几个有用的方法。 logout() 方法从 HTTP 会话密钥中删除登录密钥。 isLoggedIn() 方法检查用户是否已登录。

servlet 过滤器只处理直接导航资源、servlet、过滤器和路径。我们需要另一个 JSF 视图保护器,因为 LoginAuthenticationFilter 是不够的。

以下是名为 LoginViewAuthenticator 的支持 bean 控制器的代码:

package uk.co.xenonique.digital.product.security;
import javax.faces.application.NavigationHandler;
// other imports elided...

@Named("loginViewAuthenticator") @ApplicationScoped
public class LoginViewAuthenticator {
  // ...

  public void check() {
    FacesContext facesContext = FacesContext.getCurrentInstance();
    HttpSession session = (HttpSession)
      facesContext.getExternalContext().getSession(true);
    String currentUser = (String)session.getAttribute(
      LoginController.LOGIN_KEY);
    if (currentUser == null || currentUser.length() == 0) {
      NavigationHandler navigationHandler =
        facesContext.getApplication().getNavigationHandler();
      navigationHandler.handleNavigation(
        facesContext, null, LoginController.LOGIN_VIEW);
    }
  }
}

LoginViewAuthenticator 类有一个执行检查的 check() 方法。我们从 HTTP 会话中检索已知密钥 LOGIN_KEY。请注意,我们可以通过链式调用 FacesContext 上的 getExternalContext() 方法来访问部分 Java Servlet API。我们检索 HttpSession 实例或创建一个实例,然后检查 相关值。如果用户没有登录,那么我们改变当前NavigationHandler的目的地。 JSF 中的导航处理程序是实现定义的类型,它在 Faces 请求和响应交互期间携带目标结果字符串。

我们在页面视图中使用 LoginViewAuthenticator 来限制访问:

<ui:composition template="/basic_layout.xhtml">
  <ui:define name="mainContent">
    <f:metadata>
      <f:event type="preRenderView" listener="#{loginViewAuthenticator.check}" />
    </f:metadata>

    <div class="login-username-box pull-right">
      <b>#{sessionScope['LOGIN_USERNAME']}</b>
    </div>
    <h1> JSF Protected View </h1>

    <!-- ... -->
</ui:composition>

对于页面 view /protected/index.html,我们使用 <f:metadata> 插入预渲染视图事件> 部分。 <f:event> 元素调用 LoginViewAuthenticator< 的 check() 方法/代码>豆。

在项目中,我们还通过向页面视图 /simple.xhtml 添加相同的节来保护 Faces Flow。此视图是起始页,因此,在此处添加预渲染视图事件有效地限制了对流的访问。 LoginViewAuthenticator bean 确保未知网站用户被重定向到 /login.xhtml 视图。

Resource Library Contracts


作为 Java EE 7 的一部分,JSF 2.2 引入了在称为资源库合同的 设施下为网站设置主题和样式的能力。契约的想法是在运行时动态地重用 Facelets。现在可以通过合约在资源之间切换而无需重新部署应用程序。也可以为匹配 URL 模式的页面静态声明合同。

该规范保留了一个名为 /contracts 的特殊命名文件夹作为资源库合同的父文件夹。此文件夹是默认文件夹。如果您已经有一个名为此视图的文件夹,那么不幸的是,您将不得不按名称重构。

JAR 的类路径上还有另一个默认位置 META-INF/contracts。此位置允许将资源库合同打包为 JAR 以分发给第三方客户。

/contracts 文件夹中,开发人员可以定义命名的合约(或主题)。您只能在 location 文件夹 /contract 或 (/META-INF/contracts) 内创建文件夹,每个文件夹代表一个命名合同。在规范中,合约有一个声明的模板。每个合同都可以定义资源,例如图像、CSS、JavaScript 文件和其他内容文件。

本书的源代码分发中有一个名为 jsf-resource-library-contracts项目,并在其中您将看到以下文件的布局:

/src/main/webapp/contracts/
/src/main/webapp/contracts/default/
/src/main/webapp/contracts/default/template.xhtml
/src/main/webapp/contracts/default/styles/app.css
/src/main/webapp/contracts/default/images/
/src/main/webapp/contracts/victoria/
/src/main/webapp/contracts/victoria/template.xhtml
/src/main/webapp/contracts/victoria/styles/app.css
/src/main/webapp/contracts/victoria/images/

有两种资源库契约:defaultvictoria。这些文件夹共享相同的资源,尽管它们不是必须的。这两个 template.xhtml 文件是布局页面视图的 UI 组合文件。这两个 app.css 文件是 CSS。

一个资源契约必须至少有一个 UI 组合模板,在规范中称为声明模板。在每个合约文件夹中,文件 template.xhtml 是一个声明的模板。在规范提到的每个模板文件中,任何 <ui:insert> 标签都被称为声明的插入点。声明资源一词是指图像、CSS 和 JavaScript 以及其他资源的集合。

default/template.xhtml 文件中,我们有一个指向参考样式表的重要链接:

<h:head>
  <!-- ...-->
  <link href="#{request.contextPath}/contracts/default/ styles/app.css" rel="stylesheet"/>
</h:head>

同样,在 victoria/template.xhtml 中,我们有一个指向替代样式表的链接:

<link href="#{request.contextPath}/contracts/victoria/ styles/app.css" rel="stylesheet"/>

在每个资源契约中,我们可以改变 CSS 文件中共享 CSS 选择器的属性,以生成替代主题。以下是 default/styles/app.css 的摘录:

.fashion-headline {
    color: #ff4227;
    font-family: Consolas;
    font-style: italic;
    font-size: 22pt;
    margin: 30px 10px;
    padding: 15px 25px;
    border: 2px solid #8b200c;
    border-radius: 15px;
}

这类似于 victoria/styles/app.css

.fashion-headline {
    color: #22ff1e;
    font-family: Verdana, sans-serif;
    font-weight: bold;
    font-size: 20pt;
    margin: 30px 10px;
    padding: 15px 25px;
    border: 2px solid #31238b;
    border-radius: 15px;
}

颜色、字体系列、大小和样式存在 差异。

为了从匹配的 URL 模式配置静态使用的资源契约,我们在 Faces 配置文件 faces-config.xml 中声明标题。 JSF 2.2 引入了一个新的 <resource-library-contracts> 元素。每个合同都与一个名称和一个或多个 URL 模式相关联。

Static Resource Library Contract references

在我们的 example 项目中,我们应该有一个 Faces 配置文件,它具有以下内容代码:

<?xml version="1.0" encoding="UTF-8"?>
<faces-config ... version="2.2">
  <application>
    <resource-library-contracts>
      <contract-mapping>
        <url-pattern>/corporate/*</url-pattern>
        <contracts>victoria</contracts>
      </contract-mapping>

      <contract-mapping>
        <url-pattern>*</url-pattern>
        <contracts>default</contracts>
      </contract-mapping>
    </resource-library-contracts>
  </application>
</faces-config>

<contract-mapping> 元素定义了两个合约:defaultvictoria 合约的顺序对于处理很重要。对于整个网站,default 合约是 活跃的,而 victoria 合约仅对 /corporate/ URL 下的页面视图有效。合同映射可能有多个 URL 模式。

我们可以编写一个页面视图来静态触发这个合约。以下是页面视图的摘录,/corporate/index.xhtml

<!DOCTYPE html>
<html... >
  <f:view >
    <ui:composition template="/template.xhtml">
      <ui:define name="mainContent">
        <!-- ... -->
        <p>This is <code>/corporate/index.xhtml</code></p>

        <p class="fashion-headline">
        This is a fashion statement!</p>

        <a class="btn btn-info" href="#{request.contextPath}/index.xhtml">Go Home</a>
        <!-- ... -->
      </ui:define> <!--name="mainContent" -->
    </ui:composition>
  </f:view>
</html>

根据 faces-config.xml 中之前的资源库合约定义,CSS 类的段落,fashion-headline 应该是绿色的。请注意 JSF 如何搜索和查找位于公司文件夹中的 /template.xhtml 引用。所以定义可以静态切换的资源库契约是一个很好的特性,但是如果我们想动态地改变契约呢?我们可以实现这个目标,我们将在下一节中学习如何实现。

Dynamic Resource Library Contract references

如果您f:view 元素,就像我们在页面视图中所做的那样,然后我们可以添加另一个名为contracts 的新属性。此属性接受按名称引用资源协定的字符串表达式。

以下是此主页视图 /index.xhtml 的摘录:

<!DOCTYPE html>
<html ...>
  <f:view contracts="#{fashionSelector.theme}">
    <ui:composition template="/template.xhtml">
      <ui:define name="mainContent">
        <!-- ... -->
        <p> This is <code>/index.xhtml</code>. </p>

        <p class="fashion-headline">
          This is a fashion statement!</p>

        <!-- ... -->
        <div class="content-wrapper">
          <h:form>
            <div class="form-group">
              <label for="theme">Disabled select menu</label>
              <h:selectOneRadio id="theme" value="#{fashionSelector.theme}" styleClass="form-control peter-radio-box" required="true" layout="lineDirection">
              <f:selectItem itemValue="default" itemLabel="Default"/>
              <f:selectItem itemValue="victoria" itemLabel="Victoria"/>
              </h:selectOneRadio>
            </div>
            <h:commandButton styleClass="btn btn-primary" action="#{fashionSelector.changeTheme()}" value="Change Theme" />
          </h:form>
        </div>
      </ui:define> <!--name="mainContent" -->
    </ui:composition>
  </f:view>
</html>

#{fashionSelector.theme} 控制器引用了一个支持 bean 的 getter,我们将看到 一个 id="id742" class="indexterm"> 一会儿。表达式的值设置选择的资源库合同。我们使用 CSS 段落来直观地查看合同模板的运行情况。为了更改合同,我们使用了带有无线电选择元素的表单。 <f:selectItem> 标签定义合约名称。

我们的支持 bean FashionSelector 是一个具有一个操作方法的控制器:

package uk.co.xenonique.digital.flows.control;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;
import java.io.Serializable;

@Named
@SessionScoped
public class FashionSelector implements Serializable {
  private String theme = "default";

  public String changeTheme() {
    return "/index?faces-redirect=true";
  }

  // Getters and setters omitted
}

我们将控制器注释为 @SessionScoped bean,以便在许多请求-响应周期中保留合同更改。

资源库合同也很高兴与 Faces Flows 一起使用。使用静态 URL 模式或模板的动态选择器的技术等同于流程。在本书的源代码中,您会找到更多的演示和源代码。事实上,页面视图 /digitalFlow/digitalFlow.xhtml 看起来完全如下:

<html ...>
  <f:view contracts="#{fashionSelector.theme}">
    <ui:composition template="/template.xhtml">
    ... </u:compoition>
  </f:view>
</html>

如您所见,原则上完全没有区别。

Advice for flows


Faces Flows 在 JSF 2.2 中是一个非常有用的特性,因为它们允许开发人员和设计人员将实现客户(或以用户为中心)目标的组件组合在一起。它们还允许架构师将页面视图和控制器组定义为特定的业务定义组件。如果设计者小心,它们可以有效地连接在一起,并以有意义的策略从依赖关系中解耦。使用 Faces Flows 时应牢记以下几点:

  • 从小处着手:设计一个实现一个职责和一个目标的 Faces Flow。不要试图在单个流程中构建整个流程。

  • 传递实体和含义类型:实现接受数据实体和传​​输对象的 Faces Flows。

  • 将流程组合在一起:将实现相似目标的常见流程组合在一起。在结帐流程中,您可能有一个专门用于送货地址的流程和一个负责付款的流程。这两个流程可以由处理整个流程的主流程调用。

  • 封装流:尽可能地封装你的流,使其自给自足。

  • 保存用户数据:当客户完成一项任务时,确保用户数据保存在确定的退出点。

抵制构建完美工作流程的诱惑。相反,我建议您在设计 Faces Flow 时将改变放在心上。

在构建网站和应用程序的现代数字团队中,最重要的人是用户体验设计师。通常,经过几轮以用户为中心的测试后,您可能会发现网站的页面设计和信息架构在数周甚至数月内不断变化和反复。通过构建小型、面向目标的 Faces Flow 组件,您将保护开发团队免受 UX 设计团队驱动的不断变化的影响。设计您的流程不是为了重用,而是为了替换。

Summary


在本章中,我们研究了 JSF 2.2 版本的典型代表:Faces Flows。我们了解了流定义和生命周期。我们用隐式导航覆盖了基础,并使用 @FlowScoped 范围创建了 POJO。我们深入研究了流过程的术语,并研究了声明式和嵌套流。我们看到了如何通过调用将参数从一个流传递到另一个流。

我们还学习了如何通过处理过期视图来为我们的数字应用程序添加技巧。然后,我们为我们的新功能添加了围绕页面视图和资源库合同的安全性。我们了解合约如何允许开发人员向我们的 JSF 应用程序添加主题和样式。我们了解到的另一件事是资源库合同可以由静态声明驱动或由支持 bean 控制。

在下一章中,我们将远离 JSF,深入研究 JavaScript 编程和库框架。

Exercises


  1. 验证流程范围的 bean 对于多个 Web 浏览器和选项卡框架是唯一的。修改第一个流类DigitalFlow中的debugClassName()方法,上报的值java.lang.System.identityHashCode() 以及类名。结果是什么?

  2. 每个人几乎都知道如何做一份简单的炒鸡蛋早餐;写下这样做的步骤。您需要哪些流程?你需要什么输入?我们知道任务的结果;还有其他输出吗?

  3. 开发一个简单的 Faces Flow 应用程序,该应用程序从用户那里获取联系方式。想想你需要的属性数量。你需要所有这些吗? (提示:姓名、地址、电子邮件和电话号码现在就可以了。)

  4. 从上一个问题中获取联系人详细信息 Faces Flow 应用程序,现在将该实体记录数据保存到数据库中。

  5. 现在将联系人详细信息单流应用程序拆分为单独的流。将地址部分设置为嵌套流。 (提示:您可以将实体记录从一个流传递到另一个流。)

  6. 在联系方式应用程序中,我们如何允许客户检索实体?开发 Faces 应用程序,以便他或她可以临时保存数据。 (提示:可能客户需要一个临时的应用程序 ID,所以在联系方式实体中添加一个。)

  7. 此时,我们将把联系人应用程序复制到一个新项目中。我们将把项目重新命名为 welcome-new-bank-customer。在零售银行业,这个业务流程被称为on-boarding。您将需要一两个嵌套流。一个流程接受这个人的工作状态:他们的薪水、他们的职位,当然还有职业。如果您有信心,也许您可​​以添加一个工作地址作为另一个流程,如果您感觉更强大,请添加国家保险号和税务记录。对于一个更复杂的项目,考虑一下,如果可以重新排序流程会发生什么?您的设计封装得如何?开发人员能否轻松地重新安排流程以适应 UX 挑战?

  8. 鉴于到目前为止的联系方式/银行入职申请,您应该有许多联系人的数据库记录。在同一个 Web 应用程序中编写另一个 Faces Flow,允许受信任的员工(案例工作者)修改和删除客户记录。在真实的业务中,这样的员工坐在系统后面,并一一批准每个入职申请请求。您需要编写 HTTP 登录表单以确保安全并保护非公开页面视图。