vlambda博客
学习文章列表

读书笔记《digital-java-ee-7-web-application-development》对话与旅程

Chapter 5. Conversations and Journeys

 

“成功就是喜欢自己,喜欢你所做的事情,喜欢你做事的方式。”

 
  --Maya Angelou

在本章中,我们将注意力集中在 JSF 对话范围上。此范围定义跨越请求和会话范围的托管支持 bean 的生命周期。这允许表单中的数据在请求范围和会话范围之间的生命周期中存活。对话范围也被认为是上下文相关的。该术语取自 上下文和依赖注入 (CDI ) 规范,这意味着标有会话 范围的 bean 的生命周期被视为上下文的一部分。您可以将其视为 CDI 容器围绕对象实例绘制的一个虚线标记,以将它们定义为一个私有组,这表示一个生命周期。 CDI 容器完成了将对象实例收集在一起的这项工作,因为它将一个对象 bean 与另一个对象 bean 相关联。

在 CDI 中,Context 表示 CDI 容器将一组作为有状态组件的对象实例绑定到定义良好且可扩展的生命周期中的能力。

在 CDI 中,依赖注入 表示 CDI 容器在考虑类型安全的情况下将组件注入应用程序的能力。 CDI 容器在运行时选择要注入的 Java 接口的实现。

JavaServer Faces 集成到标准 CDI 范围中,包括会话范围。对话的示例包括几种类型的当代数字客户旅程。您可能在网上申请新工作、通过电子商务网站的运输和交付流程或建立政府时亲眼目睹了这一点资源或功能,如税收评估或申报。在本章中,我们将看一个示例客户旅程,其中开发人员或用户正在申请即时担保贷款。您可能已经看过这些,或者实际上已经幸运或不幸地细读了发薪日贷款工具。

会话范围与客户端保持状态。与会话范围划分的控制器或 POJO,其组件实例成为其状态的一部分。

下面的 图概述了我们将在本章中学习的托管 bean 控制器的对话范围:

读书笔记《digital-java-ee-7-web-application-development》对话与旅程

具有不同 CDI 范围的几个 bean 实例的图示

上图显示了登录到企业应用程序的两个不同客户。从左到右开始,我们有 UserProfile 实例来捕获客户的登录信息,存储在 CDI 会话范围内。这些 bean 仅与与 javax.servlet.http.HttpSession 对象关联的特定客户共享。

移到右侧,我们有一个 bean 实例的对象图,即LendingControllerContactDetailBankDetails,其中 存储在会话范围内。

在图的底部,在应用程序范围内,我们有 bean实例,UtilityDataHelperController。所有 Web 应用程序用户共享 bean。会话 bean 能够访问当前会话范围和应用程序范围内的共享信息。

Tip

有关 CDI 的更多信息,请阅读姊妹书,Java EE 7 开发人员手册Packt Publishing

Digital e-commerce applications


Java EE 应用程序非常适合维护状态的数字站点。如果网站在客户旅程中与用户保持任何 排序状态,则用户通常参与对话。 UX 测试表明,对于许多发生大量交互的企业站点,存在多个对话。套用 Java Champion 同胞 Antonio Gonclaves 的话说,他是 Java EE 7 专家组的成员,如果您打算构建数字 Web 应用程序,那么它必须能够处理复杂的流程管理。

即时贷款与快速初创企业和企业家有效提供的作为全球经济信贷紧缩的最终解决方案的产品不同。随着来自这些新的敏捷新贵的竞争加剧,一些发达国家的许多国内家庭银行不得不快速组装即时贷款工具产品。在本章中,我们将开发一种即时安全贷款工具。我们的产品不是一个完整的解决方案,但它展示了为数字客户交付初始原型的方式。我们不与金融服务集成,而商业解决方案需要管理信息报告以及与商业银行基础设施的集成。

让我们更广泛地进入对话范围。

Conversational scope


会话范围由跨越到服务器的许多 HTTP 请求的生命周期定义。 开发人员确定范围的开始和结束时间,最重要的是,它与用户相关联。关键注释由名为 @javax.enterprise.context.ConversationScoped 的 CDI 规范定义。当您将此注解应用于控制器或 POJO 时,请记住确保您实现了标记接口 java.io.Serializable

CDI 还定义了一个接口,javax.enterprise.context.Conversation 表示会话接口。对话可以是两种不同的存在状态:短暂的和长期的。瞬态意味着对话是暂时的状态。当您使用 @ConversationScoped 注释 bean 时,默认情况下它将处于瞬态状态。

开发人员控制对话何时从瞬态切换到长时间运行状态。然后,对话变为活动状态,并保持 HTTP 用户连接的保持状态,该状态通常与特定的 Web 浏览器选项卡相关联。从本质上讲,对话是一个工作单元。对话开始并最终结束。

以下是javax.enterprise.context.Conversation接口的定义:

public interface Conversation {
  void begin();
  void begin(String id);
  void end();
  String getId();
  long getTimeout();
  void setTimeout(long milliseconds);
  boolean isTransient();
}

begin() 方法启动对话。 CDI 容器将会话范围 POJO 标记为长期运行存储。一个对话有一个标识符;另一种方法 begin(String id) 允许开发人员提供一个明确的方法。

end() 方法终止对话,CDI 容器有效地丢弃与 POJO 关联的上下文信息,状态返回瞬态。为了确定会话是否是暂时的,调用 isTransient() 被使用。

以下 图说明了 CDI 对话范围 bean 的生命周期:

读书笔记《digital-java-ee-7-web-application-development》对话与旅程

Conversation timeout and serialization

正如我们前面所讨论的,会话范围的生命周期超出了请求范围,但不能超出会话范围。 CDI 容器可以使会话范围超时并终止上下文信息,以保留 或恢复资源。这就是带有 @ConversationScoped 的注释 bean 必须是 Serializable 的部分原因。智能 CDI 容器和 servlet 容器可以将对话传输到磁盘,甚至传输到另一个正在运行的 JVM 实例,但如果没有序列化,它永远不会尝试这样做。

应用程序开发人员可以检索超时并使用方法 getTimeout()setTimeout() 进行设置。

所以现在我们知道什么是 @ConversationScopedConversation。让我们在我们的即时安全借贷应用程序中充分利用它们。

The conversation scope controller


我们数字客户旅程的核心是一个名为LendingController的托管bean。在阅读本章时,我们将把它轻轻分解成更简单的部分。

最初的 实现如下所示:

package uk.co.xenonique.digital.instant.control;
import uk.co.xenonique.digital.instant.boundary.ApplicantService;
import uk.co.xenonique.digital.instant.entity.Address;
import uk.co.xenonique.digital.instant.entity.Applicant;
import uk.co.xenonique.digital.instant.entity.ContactDetail;
import uk.co.xenonique.digital.instant.util.Utility;
// imports elided

@Named("lendingController")
@ConversationScoped
public class LendingController implements Serializable {
  @EJB ApplicantService applicantService;
  @Inject Conversation conversation;
  @Inject Utility utility;

  public final static int DEFAULT_LOAN_TERM = 24;
  public final static BigDecimal DEFAULT_LOAN_AMOUNT = new BigDecimal("7000");
  public final static BigDecimal DEFAULT_LOAN_RATE = new BigDecimal("5.50");

  private int dobDay;
  private int dobMonth;
  private String dobYear;
  private BigDecimal minimumLoanAmount = new BigDecimal("3000");
  private BigDecimal maximumLoanAmount = new BigDecimal("25000");
  private BigDecimal minimumLoanRate   = new BigDecimal("3.0");
  private BigDecimal maximumLoanRate   = new BigDecimal("12.0");

  private String currencySymbol = "£";

  private BigDecimal paymentMonthlyAmount = BigDecimal.ZERO;
  private BigDecimal totalPayable = BigDecimal.ZERO;
  private Applicant applicant;

  public LendingController() {
    applicant = new Applicant();
    applicant.setLoanAmount( DEFAULT_LOAN_AMOUNT);
    applicant.setLoanRate( DEFAULT_LOAN_RATE );
    applicant.setLoanTermMonths( DEFAULT_LOAN_TERM );
    applicant.setAddress(new Address());
    applicant.setContactDetail(new ContactDetail());
  }

  public void checkAndStart() {
    if ( conversation.isTransient()) {
        conversation.begin();
    }
    recalculatePMT();
  }

  public void checkAndEnd() {
    if (!conversation.isTransient()) {
        conversation.end();
    }
  }
  /* ... */
}

这可能 在第一次观察时看起来是一个复杂的控制器,但是,这里有两个重要的项目。首先,我们用 @ConversationScoped 注释 LendingController,其次,我们要求 CDI 注入一个 Conversation 实例到这个 bean 中。我们还实现了 Serializable 标记接口,以允许 servlet 容器可以自由地在运行中保持和重新加载 bean,如果它选择并且实现支持此功能。

请特别注意帮助方法,checkAndStart()checkAndEnd()。如果当前状态是瞬态的,checkAndStart() 方法会启动一个新的长时间运行的对话。如果当前 Conversational 实例处于运行状态,checkAndEnd() 方法会终止长时间运行的对话。

您可以看到之前的联系方式应用程序的一些元素已经进入了我们的即时借贷应用程序。这是经过深思熟虑的设计。

LendingController bean 包含 Applicant 的实例成员,它是域主详细信息记录。它是一个 JPA 实体 bean,用于存储申请人的数据。您已经看到了出生日期字段。控制人还拥有与每月支付金额和贷款应付总额相关的成员。它还包含贷款金额和利率的下限和上限,它们作为 getter 和 setter 公开。

最后,CDI 将实用程序实例注入 LendingController。这是一个应用程序范围的 POJO,它巧妙地让我们避免编写静态单例。稍后我们将看到实用程序类的详细信息,但首先我们必须绕过设计模式。

The Entity-Control-Boundary design pattern

这个即时借贷应用程序利用了一种称为实体控制边界的特定设计模式。这是一种分离应用程序中一组对象的关注点和职责的模式。线索在 LendingController 的导入包名称中。

为了非常简单地解释,实体的概念表示软件应用程序中的数据模型。控制 元素是软件应用程序中管理信息流的组件。边界元素属于应用系统,但位于系统的外围。

您认为这种模式类似于模型-视图-控制器是完全正确的,除了 ECB 适用于整个软件系统并且控制元素比控制器和用户界面更负责。

在这个应用程序中,我将 LendingController 放在了控件包中,因为源代码显示它包含了大部分的业务逻辑。也许,对于一个适当的生产应用程序,我们可以将我们的逻辑委托给另一个 CDI bean 或 EJB。正如一位顾问曾经对他的客户说的那样,视情况而定

在实体包里面,没有争议;我添加了 ApplicantContactDetailAddress 类。这些是具有持久性的对象。您已经在 Chapter 4 中看到了 ContactDetail 实体 bean,JSF 验证和 AJAX

我把ApplicantService EJB放在边界包里,因为它位于外围,负责数据访问。

The customer journey

让我们深入研究 LendingController 并揭示我们的客户旅程。我们假设我们有 与创意设计师和 UX 团队坐在一起并提出一个设计。该应用程序基于一系列组织成向导的线性网页。对于这个基本示例,我们只允许消费者在成功输入当前页面的有效信息后进入下一页。

以下是每一页的地形标题:

描述

1

入门

向客户提供有关资格标准的信息

2

你的资料

客户输入他们的个人联系方式和出生日期

3

您的费率

客户选择他们的贷款金额和期限

4

你的地址

客户输入他们的完整家庭地址和电话号码

5

确认

消费者同意法律服务条款并查看摘要

6

完成

消费者看到他们的申请表提交的确认

这现在 在控制器中实现起来非常简单,使用以下摘录:

@Named("lendingController")
@ConversationScoped
public class LendingController implements Serializable {
  /* ... */

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

  public String jumpGettingStarted() {
      return "getting-started?faces-redirect=true";
  }

  public String doGettingStarted() {
      checkAndStart();
      return "your-details?faces-redirect=true";
  }

  public String doYourDetails() {
      checkAndStart();
      Calendar cal = Calendar.getInstance();
      cal.set(Calendar.DAY_OF_MONTH, dobDay);
      cal.set(Calendar.MONTH, dobMonth-1);
      int year = Integer.parseInt(dobYear);
      cal.set(Calendar.YEAR, year);
      applicant.getContactDetail().setDob(cal.getTime());
      return "your-rate?faces-redirect=true";
  }

  public String doYourRate() {
      checkAndStart();
      return "your-address?faces-redirect=true";
  }

  public String doYourAddress() {
      checkAndStart();
      return "confirm?faces-redirect=true";
  }

  public String doConfirm() {
      /* ... */
      return "completion?faces-redirect=true";
  }

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

  /* ... */
}

LendingController bean有几个对应用户需求的action方法,分别是doGettingStarted(), doYourDetails(), doYourRate(), doYourAddress(), doConfirm ()doCompletion()。这些操作方法只需返回名称即可让 客户进入下一页视图。对于这些方法中的大多数,除了 doCompletion() 之外,我们通过调用 checkAndStart()< 来确保会话处于长时间运行状态/代码>。在 doCompletion()cancel() 方法中,我们调用 checkAndEnd() 以确保对话恢复到瞬态状态。 doCompletion() 方法利用 ApplicationService 保存数据,Applicant实体实例,到底层数据库。

Tip

在示例代码中,我们通过在每个操作方法的开头应用 checkAndStart() 有点作弊。对于生产代码,如果用户跳转到应该进行对话的可书签 URL,我们通常应该确保这是一个错误或重定向。

让我们检查实体并填写更多空白。

Entity classes

实体 Applicant 是主从记录。这被称为核心域对象。它存储客户申请即时担保贷款的数据。我们捕获客户的贷款信息,例如联系方式(ContactDetail)、地址(Address)、电话号码(家庭、工作和移动),最重要的是财务细节。

Applicant 实体如下:

package uk.co.xenonique.digital.instant.entity;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.Date;

@Entity
@Table(name="APPLICANT")
@NamedQueries({
  @NamedQuery(name="Applicant.findAll",
          query = "select a from Applicant a " +
                  "order by a.submitDate"),
  @NamedQuery(name="Applicant.findById",
          query = "select a from Applicant a where a.id = :id"),
})
public class Applicant {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;

  @OneToOne(cascade = CascadeType.ALL)
  private ContactDetail  contactDetail;
  @OneToOne(cascade = CascadeType.ALL)
  private Address  address;

  private String workPhone;
  private String homePhone;
  private String mobileNumber;

  private BigDecimal loanAmount;
  private BigDecimal loanRate;
  private int loanTermMonths;
  private boolean termsAgreed;

  @Temporal(TemporalType.TIMESTAMP)
  private Date submitDate;

  public Applicant() { }

  // Getters and setters omitted ...
  // hashCode(), equals(), toString() elided
}

Applicant 实体存储贷款金额、利率、期限以及提交日期。它还包含 家庭、工作和移动电话号码。申请人与 ContactDetailAddress 实体具有一对一的单向关系。

对于 loanRateloanAmount 等财务属性,请注意我们更喜欢使用 BigDecimal 而不是用于计算期间货币准确性的原始浮点类型。

向利益相关者解释域对象的方式是:客户有贷款利率、贷款期限,并且必须以电子方式同意法律条件。有了这些信息,系统就可以计算出贷款和客户每月还款多少,并在申请贷款时显示出来。

您已经看到了 ContactDetail 实体。和之前一模一样,只是包名被重构为实体。以下是 Address 实体 bean 的提取代码:

package uk.co.xenonique.digital.instant.entity;
import javax.persistence.*;

@Entity
@Table(name="ADDRESS")
@NamedQueries({
  @NamedQuery(name="Address.findAll",
    query = "select a from Address a "),
  @NamedQuery(name="Address.findById",
    query = "select a from Address a where a.id = :id"),
})
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name="ADDRESS", nullable = false,
            insertable = true, updatable = true,
            table = "ADDRESS")
    private long id;

    String houseOrFlatNumber;
    String street1;
    String street2;
    String townOrCity;
    String region;
    String areaCode;
    String country;

    // toString(), hashCode(), equalsTo() elided
    /* ... */
}

Address 实体代表申请人的通信和法定个人地址。 这里没什么特别的。它是您将在电子商务应用程序中看到的沼泽标准实体 bean。

请注意,示例的源代码是在线的,是本书的一部分,供您参考。

Data service

我们如何将客户的输入保存到持久性存储中?我们的应用程序使用有状态会话 EJB,它提供了保存和检索 Applicant 实体记录的方法。

ApplicantService 类如下:

package uk.co.xenonique.digital.instant.boundary;
import uk.co.xenonique.digital.instant.entity.Applicant;
import javax.ejb.Stateful;
import javax.persistence.*;
import java.util.List;

@Stateful
public class ApplicantService {
  @PersistenceContext(unitName = "instantLendingDB",
    type = PersistenceContextType.EXTENDED)
  private EntityManager entityManager;

  public void add(Applicant applicant) {
    entityManager.persist(applicant);
  }
 
  /* ... */

  public List<Applicant> findAll() {
    Query query = entityManager.createNamedQuery(
        "Applicant.findAll");
    return query.getResultList();
  }

  public List<Applicant> findById(Integer id) {
    Query query = entityManager.createNamedQuery(
        "Applicant.findById").setParameter("id", id);
    return query.getResultList();
  }
}

add() 方法 将新申请人插入数据库。 findAll()findById() 未在即时贷款示例中使用。这些查询方法仅用于说明目的。据推测,需要访问完整申请的另一部分中的申请人数据。

我们已经介绍了应用程序的实体、控件和边界。是时候检查页面浏览量了。

Page views


视图的控制流由客户旅程定义。每个页面视图都代表业务利益相关者希望看到的特定需求。索引页面视图是一项要求,因为贷方希望客户看到登录页面。这也是国家政府当局要求的法定合规义务。您还会注意到,客户旅程映射到线性流,但并非针对所有旅程。

Note

发薪日贷款计划必须遵循合规要求。请参阅英国金融行为监管局的网站 (https://goo.gl/NfbFbK ) 和美国消费者金融保护局 (http://goo.gl/3V9fxk)。

下表概述了控制器操作和视图页面之间的关系:

查看源代码

查看目标

动作方法

索引

入门

jumpGettingStarted()

入门

你的详细信息

doGettingStarted()

你的详细信息

你的费率

doYourDetails()

你的费率

你的地址

doYourRate()

你的地址

确认

doYourAddress()

确认

完成

doConfirm()

完成

索引

doCompletion()

所有视图 页面都以上表中的扩展名xthml 为后缀。很明显,对话中发生了线性的工作流程。理想情况下,对话范围在客户通过 jumpGettingStarted() 操作方法进入入门视图时开始。

An initial page view

让我们看看 初始index.xhtml 页面视图。这是贷款申请的登陆页面。以下是我们的贷款申请和登陆页面的截图:

读书笔记《digital-java-ee-7-web-application-development》对话与旅程

此页面 index.xhtml 的视图 非常简单。它 具有一个基本的链接按钮组件和一个 Bootstrap 轮播:

<!DOCTYPE html>
<html ...>
    <ui:composition template="/basic_layout.xhtml">
      ...
      <ui:define name="mainContent">
        <h1> JSF Instant Secure Lending</h1>
        <p>
            Welcome to Duke Penny Loan where developers,
            designers and architect can secure
            an instant loan. <em>You move. We move.</em>
        </p>

        <div class="content-wrapper center-block">
          <div id="carousel-example-generic" class="carousel slide" data-ride="carousel" data-interval="10000">
            <!-- Indicators -->
            <ol class="carousel-indicators">
                <li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li>
                <li data-target="#carousel-example-generic" data-slide-to="1"></li>
                <li data-target="#carousel-example-generic" data-slide-to="2"></li>
                <li data-target="#carousel-example-generic" data-slide-to="3"></li>
            </ol>
            ...
          </div>
        </div><!-- content-wrapper -->

        <div class="content-wrapper">
          <h:link styleClass="btn btn-primary btn-lg" outcome="#{lendingController.jumpGettingStarted()}">
              Apply Now!
          </h:link>
        </div>

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

<h:link> 元素是这个视图最重要的特征。此自定义 标记的结果引用了 jumpGettingStarted() 方法class="indexterm"> 控制器,它开始一个长时间的对话。

即使在这个阶段,在对话开始之前,我们也可以将信息传递给客户。因此,在页面视图的另一部分,我们使用表达语言告诉客户最低和最高贷款金额以及利率。

下面是代码,也是页面视图index.xhtml的一部分:

  <div class="content-wrapper">
    <p>
      Apply for a Dukes Dollar loan. You borrow from
      <b>
        <h:outputText value="#{lendingController.minimumLoanAmount}" >
          <f:convertNumber currencyCode="GBP" type="currency" />
        </h:outputText>
      </b>
        to
      <b>
        <h:outputText value="#{lendingController.maximumLoanAmount}" >
          <f:convertNumber currencyCode="GBP" type="currency" />
        </h:outputText>
      </b>
        on a rate from
      <b>
        <h:outputText value="#{lendingController.minimumLoanRate}" >
          <f:convertNumber pattern="0.00" />
        </h:outputText>&#37;
      </b>
        to
      <b>
        <h:outputText value="#{lendingController.maximumLoanRate}" >
          <f:convertNumber pattern="0.00" />
        </h:outputText>&#37;
      </b>.
    </p>
  </div>

此页面 使用 JSF 核心标签 <f:convertNumber> 将浮点数格式化为货币格式。 HTML 实体字符 &#37; 表示百分比字符 (%)。请记住,视图技术严格来说是 Facelets,而不是 HTML5。

Getting started page view

入门视图更加简单。我们向 客户提供有关其申请贷款资格的信息。客户必须年满 18 岁;我们重复他们借了多少,借了多久。

该视图名为 getting-started.xhtml,如下图所示:

读书笔记《digital-java-ee-7-web-application-development》对话与旅程

有一个 单个 JSF 表单,带有一个按钮可以将 客户移动到下一个页面视图,your-details.xhtml。无需查看此视图​​的完整源代码,因为它主要是标记 HTML。但是,我们还有另一个命令链接:

<h:link styleClass="btn btn-primary btn-lg" outcome="#{lendingController.doGettingStarted()}">
  Next</h:link>

Contact details page view

下一个 视图是熟悉的联系人详细信息屏幕。我们已将前几章中的它包含在即时安全贷款示例中。我们还重新利用了JSF 表达式语言来引用控制器和嵌套属性。

名字字段的页面创作代码如下:

<h:inputText class="form-control" label="First name" value="#{lendingController.applicant.contactDetail.firstName}" id="firstName" placeholder="First name">
    <f:validateRequired/>
    <f:validateLength maximum="64" />
    <f:ajax event="blur" render="firstNameError"/>
</h:inputText>

EL #{lendingController.applicant.contactDetail.firstName} 指的是相关的嵌套实体 bean 属性。我们还保留了 Chapter 4JSF 验证和 AJAX< /em> 提供丰富的客户旅程。

对于这个视图,我们使用一个 JSF 命令按钮来提交表单:

<h:commandButton styleClass="btn btn-primary" action="#{lendingController.doYourDetails()}" value="Submit" />
&#160;
&#160;
<h:commandButton styleClass="btn btn-default" action="#{lendingController.cancel()}" immediate="true" value="Cancel"/>

我们也有强制取消操作,以防客户当天不想再申请贷款。

以下是 your-details.xhtml 视图的屏幕截图,允许客户输入他们的联系方式:

读书笔记《digital-java-ee-7-web-application-development》对话与旅程

现在是 的时候了。为旧的 JavaServer Faces 添加 一些 HTML5 优点怎么样?

Your rate page view

贷款 金额和利率页面视图依赖于 HTML5 范围 控制元素,该元素,在大多数兼容标准的浏览器上,呈现为水平滑块。 JSF 没有对范围控制的内置支持;所以对于这个视图,我们利用了 JSF HTML5 友好的支持能力。 JSF 规范允许我们编写看起来像标准 HTML 组件的标记,但如果我们提供特殊属性,JSF 会将其视为 UI 组件。传递功能仅适用于类似于现有 JSF 核心控件的标记。

一张图片值一千字,下面我们来看看 your-rate.xhtml 的截图:

读书笔记《digital-java-ee-7-web-application-development》对话与旅程

该视图使用 AJAX 部分更新和 HTML5 友好的标记工具。让我向您展示表单的代码:

<h:form id="yourRateForm" styleClass="form-horizontal" p:role="form">
  <div class="form-group">
    <h:outputLabel for="loanAmount" class="col-sm-3 control-label">
        Loan Amount</h:outputLabel>
    <div class="col-sm-9">
      <input class="form-control" jsf:label="Loan Amount" jsf:value="#{lendingController.applicant.loanAmount}" type="range" min="#{lendingController.minimumLoanAmount}" max="#{lendingController.maximumLoanAmount}" step="250" id="loanAmount" >
      <f:validateRequired/>
        <f:ajax event="blur" render="loanAmountError"/>
        <f:ajax event="valueChange" listener="#{lendingController.recalculatePMT()}" render="paymentMonthlyOutput loanRateOutput totalPayableOutput" />
      </input>
      <h:message id="loanAmountError" for="loanAmount" styleClass="alert validation-error"/>
    </div>
  </div>

与所有 JSF 表单一样,我们首先声明一个名为 yourRateForm 的表单,并使用 Bootstrap CSS 对其进行样式设置。专注于控制元素,您 会注意到它被写为 <input> 而不是 <代码类="literal"><h:inputText>。这是因为 JSF <h:inputText> 不支持新的 HTML5 Range 元素。通常,无法访问更丰富的 UI 组件将成为即时安全借贷的问题。

HTML5 Range Input 元素接受最小值、最大值和当前值。它还接受步长。

HTML5 friendly support

JSF 2.2 允许 HTML5 友好组件使用新的标签库 URI 用于 XML 命名空间 xmlns:jsf="http:// /xmlns.jcp.org/jsf”。使用属性 jsf:idjsf:labeljsf:attribute,HTML5 标记在 JSF 框架内具有可见性。

your-rate.xhtml 的完整 XML 命名空间如下所示:

<!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" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" xmlns:jsf="http://xmlns.jcp.org/jsf" xmlns:xen="http://xmlns.jcp.org/jsf/composite/components">

我们将在本章后面讨论复合组件。 HTML5 友好的标签库向 JSF 生命周期公开了一个标准的 HTML 输入组件。没有 JSF 或 Java 经验的创意人员也更容易理解页面视图。我们不必再担心 JSF 应用于视图 ID 的特殊名称修改;这意味着组件 ID 对于 HTML 和 JavaScript 都是有用的。

Using AJAX for a partial update

第 4 章JSF 验证和 AJAX,我们学习了如何使用 Ajax 验证表单属性。 JSF 允许开发人员使用 <f:ajax> 自定义标记执行部分页面更新。

为了启用丰富的 用户示例,每当客户更改贷款金额滑块时,我们都会调用服务器端重新计算每月支付金额。我们通过将事件侦听器附加到值的变化来实现这一点。代码如下:

<f:ajax event="valueChange" listener="#{lendingController.recalculatePMT()}" render="paymentMonthlyOutput loanRateOutput totalPayableOutput" />

代码中新增的一个属性是 render 属性,它指定 JSF UI 组件的唯一 ID,该组件将在 AJAX 响应上重新呈现。换句话说,我们以声明方式指定要在 AJAX 行为完成后重新呈现给 JSF 的组件,从而获得部分更新。

Binding components

让我们看看 HTML5 Range 元素(在本例中为贷款金额)绑定的 其他组件。

看看下面的代码:

<c:set var="loanAmountWidth" value="#{100.0 * (lendingController.applicant.loanAmount - lendingController.minimumLoanAmount) / (lendingController.maximumLoanAmount - lendingController.minimumLoanAmount)}" />

  <div class="progress">
      <div id="loanAmountProgress" class="progress-bar progress-bar-success progress-bar-striped" role="progressbar" aria-valuenow="#{lendingController.applicant.loanAmount}" aria-valuemin="#{lendingController.minimumLoanAmount}" aria-valuemax="#{lendingController.maximumLoanAmount}" style="width: ${loanAmountWidth}%;">
          #{lendingController.applicant.loanAmount}
      </div>
  </div>

  <div class="content-wrapper">
    <p id="loanAmountText" class="monetary-text">
        You would like to borrow
        <b> #{lendingController.currencySymbol}
        <h:outputText value="#{lendingController.applicant.loanAmount}" >
            <f:convertNumber pattern="#0,000" />
        </h:outputText> </b>
    </p>
  </div>

progress 标记直接复制自 Bootstrap CSS 组件示例。我们插入值表达式以从 LendingControllerApplicant 实例中提取信息。

在前面代码摘录的顶部,我们使用 JSTL 核心标记 <c:set> 设置进度条的初始值。

<c:set var="loanAmountWidth" value="#{100.0 * (lendingController.applicant.loanAmount - lendingController.minimumLoanAmount) / (lendingController.maximumLoanAmount - lendingController.minimumLoanAmount)}" />

这表明 EL 3.0 中的统一表达式语言能够检索 JSF 中的后期有界值以计算结果。结果设置在名为 loanAmountWide 的页面范围变量中。稍后使用 $(loanAmountWidth) 访问此变量,它设置 Bootstrap CSS 进度条组件的初始位置值。

HTML5 标准没有内置支持显示适用于所有顶级 Web 浏览器的 HTML5 Range 元素的值。在撰写本文时,该功能尚不存在,W3C 或 WHATWG 可能会在不久的将来加强 HTML5 规范中的这一弱点。在那之前,我们将使用 jQuery 和 JavaScript 来填补这一空白。

如果您注意到,文本用loanAmountText标识,进度组件用loanAmountProgress 在前面的代码中。编写用于将 HTML5 Range 元素绑定到这些字段的 jQuery 非常简单。

我们需要一个 JavaScript 模块来实现绑定。 /resources/app/main.js的完整代码如下:

var instantLending = instantLending || {};

instantLending.Main = function()
{
  var init = function()
  {
    $(document).ready( function() {
      associateRangeToText(
        '#loanAmount', '#loanAmountProgress', '#loanAmountText',
        3000.0, 25000.0,
        function(value) {
            var valueNumber = parseFloat(value);
            return "You would like to borrow <b>£" +
                valueNumber.formatMoney(2, '.', ',') + "</b>";
        });
    });
  };

  var associateRangeToText = function( rangeElementId,
    rangeProgressId, rangeTextId, minimumValue,
    maximumValue, convertor) {
    var valueElem = $(rangeElementId);
    var progressElem = $(rangeProgressId);
    var textElem = $(rangeTextId);
    valueElem.change( function() {
      var value = valueElem.val();
      progressElem.html(value);
      progressElem.attr("aria-valuenow", value);

      var percentage = 100.0 * ( value - minimumValue) /
        ( maximumValue - minimumValue );
      progressElem.css("width", percentage+"%");

      var monetaryText = convertor( value )
      textElem.html( monetaryText );
    });
  }

  var oPublic =
  {
    init: init,
    associateRangeToText: associateRangeToText
  };

  return oPublic;
}(jQuery);

instantLending.Main.init();

instantLending.Main 模块定义了一个 HTML Range 元素与另外两个 组件的绑定:一个进度条和一个标签文本区域。有关 JavaScript 模块的快速修订,请参阅 第 1 章Digital Java EE 7

该模块有一个 init() 函数,该函数使用 jQuery 文档加载机制设置绑定。它调用一个名为 associateRangeToText() 的函数,该函数计算进度条经过的百分比并将该值写入文本元素区域。该函数接受相关组件的文档 ID:范围、进度和文本标签组件。它将一个匿名函数附加到 range 元素;当用户更改组件时,它会更新关联的组件。

main.js 模块还定义了一个有用的原型方法,添加到 JavaScript 数字类型。下面的代码展示了它是如何工作的:

// See http://stackoverflow.com/questions/149055/how-can-i-format-numbers-as-money-in-javascript
Number.prototype.formatMoney = function(c, d, t){
  var n = this,
      c = isNaN(c = Math.abs(c)) ? 2 : c,
      d = d == undefined ? "." : d,
      t = t == undefined ? "," : t,
      s = n < 0 ? "-" : "",
      i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "",
      j = (j = i.length) > 3 ? j % 3 : 0;
  return s + (j ? i.substr(0, j) + t : "") +
      i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) +
      (c ? d + Math.abs(n - i).toFixed(c).slice(2) : "");
};

formatMoney() 方法将浮点值类型格式化为字符串作为货币输出。此代码由 Patrick Desjardins 贡献给 Stack Overflow。以下代码说明了如何调用此函数:

var p = 128500.99
console.log(p.formatMoney(2, '.', ',') ) // 128,500.99

第一个 参数为固定小数大小,第二个参数确定小数符号,第三个参数指定千位字符。

通过这个模块,我们将 HTML5 Range 元素绑定到页面中的其他元素,从而展示了 JSF 中对 HTML5 的友好支持。

Updating areas with AJAX partial updates

JSF 如何使用 AJAX 响应更新页面区域?开发人员指定使用 <f:ajax> 的 render 属性更新的 UI 组件标签。在现代网页设计中,哪个组件可以被视为标准 JSF 渲染套件中的 HTML 层元素 <div>?答案是使用 <h:panelGroup> JSF 自定义标签。我们可以为这个 UI 组件提供一个唯一的标识符,当 AJAX 行为完成时,JSF 会渲染这个组件。

以下是即时贷款利率的代码摘录,其中 div 元素由 loanRateOutput 标识:

<c:set var="loanRateWidth" value="#{100.0 * (lendingController.applicant.loanRate - lendingController.minimumLoanRate) / (lendingController.maximumLoanRate - lendingController.minimumLoanRate)}" />

<h:panelGroup layout="block" id="loanRateOutput">
  <div class="progress">
    <div id="loanRateProgress" class="progress-bar progress-bar-info progress-bar-striped" role="progressbar" aria-valuenow="#{lendingController.recalculateLoanRate()}" aria-valuemin="#{lendingController.minimumLoanRate}" aria-valuemax="#{lendingController.maximumLoanRate}" style="width: ${loanRateWidth}%;">
      #{lendingController.applicant.loanRate}
    </div>
  </div>
  <div class="content-wrapper">
    <p id="loanRateText" class="monetary-text">
      The tax rate will be
      <b> <h:outputText value="#{lendingController.applicant.loanRate}" >
        <f:convertNumber pattern="0.000" />
      </h:outputText>&#37;</b>
    </p>
  </div>
</h:panelGroup>

<h:panelGroup> 默认渲染一个 div 层,因此包含进度条组件和文本输出内容。 div 在调用 LendingController 中的方法 recalculatePMT() 后呈现。有关此代码的提醒,请参阅前面的部分。

函数 recalclulatePMT()recalculateLoanRate() 如下所示:

public BigDecimal recalculatePMT() {
  recalculateLoanRate();
  paymentMonthlyAmount = new BigDecimal(utility.calculateMonthlyPayment(
      applicant.getLoanAmount().doubleValue(),
      applicant.getLoanRate().doubleValue(),
      applicant.getLoanTermMonths()));

  totalPayable = paymentMonthlyAmount.multiply(
    new BigDecimal( applicant.getLoanTermMonths()));
  return paymentMonthlyAmount;
}

public BigDecimal recalculateLoanRate() {
  applicant.setLoanRate(
    utility.getTaxRate(applicant.getLoanAmount()));
  return applicant.getLoanRate();
}

recalculatePMT() 函数使用经典的数学公式,根据本金金额、期限长度,当然还有利率来评估贷款的每月还款额。

recalculateLoanRate() 函数使用一个实用程序,一个应用程序范围的 CDI bean,根据贷款账户不同的利率限制表计算利率。

所以让我们回顾一下。 JavaScript 模块 instantLending::Main 在客户端更新。当客户更改贷款金额时,此模块会更改进度条组件和文本内容。同时,JSF 向服务器端调用 AJAX 请求并调用动作事件侦听器 recalculatePMT()。框架最终接收到 AJAX 响应,然后重新渲染贷款利率、期限控制和摘要 区域。

要完成 XHTML,让我们检查此页面视图中的剩余内容,即您的 -rate.xhtml。以下是贷款期限的内容,它是一个下拉组件:

<div class="form-group">
  <h:outputLabel for="loanTerm" class="col-sm-3 control-label">
    Loan Term (Months)</h:outputLabel>
  <div class="col-sm-9">
    <h:selectOneMenu class="form-control" label="Title" id="loanTerm" value="#{lendingController.applicant.loanTermMonths}">
      <f:selectItem itemLabel="12 months" itemValue="12" />
      <f:selectItem itemLabel="24 months" itemValue="24" />
      <f:selectItem itemLabel="36 months" itemValue="36" />
      <f:selectItem itemLabel="48 months" itemValue="48" />
      <f:selectItem itemLabel="60 months" itemValue="60" />
      <f:validateRequired/>
      <f:ajax event="blur" render="loanTermError"/>
      <f:ajax event="valueChange" listener="#{lendingController.recalculatePMT()}" render="paymentMonthlyOutput loanRateOutput monthTermsOutput totalPayableOutput" />
    </h:selectOneMenu>
    <h:message id="loanTermError" for="loanTerm" styleClass="alert validation-error"/>
  </div>
</div>

该组件还具有调用重新计算事件侦听器的 <f:ajax> 自定义标记。因此,如果客户选择不同的贷款期限,loanRateOutputpaymentMonthlyOutput 也会随着摘要的一部分而改变AJAX 更新。

最后,我们来看看汇总区的内容:

<div class="content-wrapper" >
  <div class="row">
    <div class="col-md-12">
      <p class="monetary-text-large">
        Your monthly payment is <b>
        #{lendingController.currencySymbol}<h:outputText id="paymentMonthlyOutput" value="#{lendingController.recalculatePMT()}">
          <f:convertNumber pattern="#0.00" />
        </h:outputText></b>
      </p>
    </div>
  </div>
  <div class="row">
    <div class="col-md-6">
      <p class="monetary-text">
        Loan term
        <h:outputText id="monthTermsOutput" value="#{lendingController.applicant.loanTermMonths}"/>
         months
      </p>
    </div>
    <div class="col-md-6">
      <p class="monetary-text">
        Total payable
        #{lendingController.currencySymbol}<h:outputText id="totalPayableOutput" value="#{lendingController.totalPayable}">
            <f:convertNumber pattern="#0,000" />
        </h:outputText>
      </p>
    </div>
  </div>
</div>

前面的代码提取中,我们使用 <h:outputText> 而不是 <h:panelGroup> 使用部分 AJAX 更新仅更新内容的某些部分。 JSF 输出文本元素是一个 JSF UI 组件,因此它通过请求重新呈现 AJAX 行为来工作。

The address page view

地址 页面视图捕获客户的主要家庭 地址。此页面还具有客户端的 AJAX 验证功能。

此代码与联系详细信息表单非常相似,因此我们将在此处省略代码提取和树。我将只显示以下代码中的第一个 houseOrFlatNumber 字段:

<h:form id="yourAddressForm" styleClass="form-horizontal" p:role="form">
  <div class="form-group">
    <h:outputLabel for="houseOrFlatNumber" class="col-sm-3 control-label">
        House number</h:outputLabel>
    <div class="col-sm-9">
      <h:inputText class="form-control" label="House or Flat Number" value="#{lendingController.applicant.address.houseOrFlatNumber}" id="houseOrFlatNumber" placeholder="First name">
        <f:validateLength maximum="16" />
        <f:ajax event="blur" render="houseOrFlatNumberError"/>
      </h:inputText>
      <h:message id="houseOrFlatNumberError" for="houseOrFlatNumber" styleClass="alert validation-error"/>
    </div>
  </div>
  ...
</h:form>

以下是 your-address.xhtml 页面浏览的屏幕截图。

读书笔记《digital-java-ee-7-web-application-development》对话与旅程

The confirmation page view

确认页面视图是客户查看其即时贷款的所有详细信息的地方。在这个视图中,他们有机会阅读合同的条款和条件。客户必须选择复选框以接受 协议,或者他们可以点击取消按钮终止对话。取消按钮调用 LendingController 中的 cancel() 方法,该方法又调用 checkAndEnd()

这里唯一相关的代码是协议条款复选框。代码摘录如下:

<h:form id="yourConfirmForm" styleClass="form-horizontal" p:role="form"> ...
  <div class="form-group">
    <h:outputLabel for="tocAgreed" class="col-sm-6 control-label">
      Do you agree with the <em>Terms of Conditions</em>?
    </h:outputLabel>
    <div class="col-sm-6">
      <h:selectBooleanCheckbox class="form-control" label="TOC Agreement" id="tocAgreed" value="#{lendingController.applicant.termsAgreed}" validator="#{lendingController.validateTermsOrConditions}" >
          <f:ajax event="blur" render="tocAgreedError"/>
      </h:selectBooleanCheckbox>
      <h:message id="tocAgreedError" for="tocAgreed" styleClass="alert validation-error"/>
    </div>
  </div>
  ...
</h:form>

我们使用 <h:selectBooleanCheckBox> 对模糊事件进行即时 AJAX 验证。这可确保在服务器端将布尔属性设置为 true。但是,我们仍然必须验证表单提交,正如我们在动作控制器方法中看到的:

  public String doConfirm() {
    if ( applicant.isTermsAgreed()) {
      throw new IllegalStateException(
        "terms of agreements not set to true");
    }
    recalculatePMT();
    applicant.setSubmitDate(new Date());
    applicantService.add(applicant);
    return "completion?faces-redirect=true";
  }

doConfirm() 方法中,我们重新计算每月付款期限以确保确定。我们检查申请人的数据值没有改变,设置提交日期,然后我们调用ApplicationService向数据库中插入一条新记录。在这个方法之后,据说客户申请成功了。

我们对 isTermsAgreed() 进行了手动检查,因为这是合同中的法律要求客户接受条款和条件。在此处引发应用程序错误 IllegalStateException 可能会引起争议。更有可能的是,开发人员会将消息打印到错误日志中,并引发异常。 servlet 规范允许捕获不同的异常并将其发送到某个错误页面。因此,如果我们创建了一个自定义的运行时异常,例如 LegalTermsAgreementException,我们可以负责任地处理这些情况。

在生产系统中,这个序列的结束可能会触发一个额外的业务流程。例如,可以使用消息总线 JMS 将工作消息发送到另一个案例工作区。在现代数字应用程序中,客户应该期望收到一封电子邮件,其中包含确认和贷款合同详细信息。当然,这是读者提供此要求的额外练习。

以下是确认视图的屏幕截图,confirm.xhtml

读书笔记《digital-java-ee-7-web-application-development》对话与旅程

让我们移动 到完成的最终页面视图。

The completion page view

完整的 阶段很简单。客户已经提交了申请,所以我们只需要通知他或她,然后对话就结束了。以下是LendingControllerdoCompletion()方法的完整代码:

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

这种方法只是结束了对话范围,因为到那时用户的数字客户旅程就完成了。

现在我们有了一个完整的流程,一个数字客户旅程。什么不见​​了?我们应该添加步骤以接受有效的银行账户、银行分类代码、IBAN 号码以及与国家银行基础设施的集成!当然,我们也需要一定程度的金融资本,足够让监管者满意的资金;在英国,这将是金融行为监管局 (http://www.fca. org.uk/)。

这个页面视图的截图,completion.xhtml,如下:

读书笔记《digital-java-ee-7-web-application-development》对话与旅程

Utility classes

通常在应用程序中,我们将常用方法和属性重构为一个单独的实用程序类,该类具有 常见的特性,以至于它们在 任何特定的包域。我们经常将这些概念放在单例中的静态方法中。使用 Java EE,我们可以做得更好。由于 CDI 支持应用程序范围,我们可以简单地将我们的公共方法移动到 POJO 中,并使 CDI 将 bean 注入到依赖对象中。这是处理 LendingController 示例中的数据、时间和每月付款期限计算的智能方式。

应用程序 范围 bean DateTimeController 充当助手 对于页面作者视图:

package uk.co.xenonique.digital.instant.control;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import java.io.Serializable;
import java.text.DateFormatSymbols;
import java.util.*;

@Named("dateHelperController")
@ApplicationScoped
public class DateHelperController implements Serializable {
  private List<Integer> daysOfTheMonth = new ArrayList<>();
  private Map<String,Integer> monthsOfTheYear
    = new LinkedHashMap<>();

  @PostConstruct
  public void init() {
    for (int d=1; d<=31; ++d) { daysOfTheMonth.add(d); }
    DateFormatSymbols symbols = new DateFormatSymbols(Locale.getDefault());
    for (int m=1; m<=12; ++m) {
        monthsOfTheYear.put(symbols.getMonths()[m-1], m );
    }
  }

  public List<Integer> getDaysOfTheMonth() {
    return daysOfTheMonth;
  }
  public Map<String,Integer> getMonthsOfTheYear() {
    return monthsOfTheYear;
  }
}

DateHelperController 方法用于your-details.view,它生成下拉日期和月份的数据出生日期字段。这段代码最初是 Chapter 4ContactDetailsController 方法的一部分,JSF 验证和 AJAX。它已被重构以供重用。

还有一个具有应用程序范围的 POJO,称为 Utility。

package uk.co.xenonique.digital.instant.util;
import javax.enterprise.context.ApplicationScoped;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.*;

@ApplicationScoped
public class Utility implements Serializable {
  protected List<LoanRateBounds> bounds = Arrays.asList(
      new LoanRateBounds("0.0",       "4500.0",   "22.50"),
      new LoanRateBounds("4500.0",    "6000.0",   "9.79"),
      new LoanRateBounds("6000.0",    "9000.0",   "7.49"),
      new LoanRateBounds("9000.0",    "11500.0",  "4.49"),
      new LoanRateBounds("11500.0",   "15000.0",  "4.29"),
      new LoanRateBounds("15000.0",   "20000.0",  "5.79"),
      new LoanRateBounds("20000.0",   "25000.0",  "6.29"),
      new LoanRateBounds("30000.0",   "50000.0",  "6.99")
      );

  public BigDecimal getTaxRate( BigDecimal amount ) {
    for ( LoanRateBounds bound : bounds ) {
      if ( bound.getLower().compareTo(amount) <= 0 &&
            bound.getUpper().compareTo(amount) > 0 ) {
        return  bound.getRate();
      }
    }
    throw new IllegalArgumentException("no tax rate found in bounds");
  }

  public double calculateMonthlyPayment( double pv, double apr, int np ) {
    double ir = apr / 100 / 12;
    return (pv * ir) / (1 - Math.pow(1+ir, -np));
  }
}

前面的代码中,方法 calculateMonthlyPayment() 计算每月支付金额。参数是 pv(指定本金值)、apr(指定年百分比)和 np,其中代表以月为单位的通知期。

getTaxRate() 方法在给定本金价值(即客户想要的贷款金额)的情况下查找适当的税率。 LoanRateBounds 类是一个简单的 POJO,如下代码所示:

package uk.co.xenonique.digital.instant.util;
import java.math.BigDecimal;

public class LoanRateBounds {
  private final BigDecimal lower;
  private final BigDecimal upper;
  private final BigDecimal rate;

  public LoanRateBounds(String lower, String upper, String rate) {
    this(new BigDecimal(lower), new BigDecimal(upper),
        new BigDecimal(rate));
  }

  public LoanRateBounds(final BigDecimal lower,
    final BigDecimal upper, final BigDecimal rate) {
      this.lower = lower;
      this.upper = upper;
      this.rate = rate;
  }

  // toString(), hashCode(), equals() and getters omitted
}

这个 LoanRateBounds POJO 是 一个不可变对象并且是 线程安全。

Composite custom components


JSF 还具有您(开发人员)可以编写的自定义组件。事实上,即时安全 借贷示例使用一个:对话中每个页面视图的顶部标题。这是一个提示,告知客户他或她在流程中的哪个位置。我称它为 WorkerBannerComponent

在 JSF 中,自定义组件描述了一段可重用的页面内容,它可以多次插入到 Facelet 视图中。自定义组件可能有也可能没有后备 bean,它可能会或可能不会将一组属性组合到一个表单中。如第2章中所述,JavaServer Faces Lifecycle ,我们可以使用自定义组件构建重复的页面内容,利用最新的 HTML 框架(如 Bootstrap)并抽象出更深层次的细节。企业可以使用自定义组件来建立页面内容和标记的通用结构。

Components with XHTML

WorkerBannerComponent 是显示标头逻辑的后备 bean,它标识 流的部分客户处于活动状态。自定义组件的代码如下:

package uk.co.xenonique.digital.instant.control;
import javax.faces.component.*;
import javax.faces.context.FacesContext;
import java.io.IOException;

@FacesComponent("workerBannerComponent")
public class WorkerBannerComponent extends UINamingContainer {
  private String gettingStartedActive;
  private String yourDetailsActive;
  private String yourRateActive;
  private String yourAddressActive;
  private String confirmActive;
  private String completedActive;

  @Override
  public void encodeAll(FacesContext context) throws IOException {
    if (context == null) {
        throw new NullPointerException("no faces context supplied");
    }
    String sectionName = (String)getAttributes().get("sectionName");
    gettingStartedActive = yourDetailsActive = yourRateActive = yourAddressActive = confirmActive = completedActive = "";

    if ( "gettingStarted".equalsIgnoreCase(sectionName)) {
        gettingStartedActive = "active";
    }
    else if ( "yourDetails".equalsIgnoreCase(sectionName)) {
        yourDetailsActive = "active";
    }
    else if ( "yourRate".equalsIgnoreCase(sectionName)) {
        yourRateActive = "active";
    }
    else if ( "yourAddress".equalsIgnoreCase(sectionName)) {
        yourAddressActive = "active";
    }
    else if ( "confirm".equalsIgnoreCase(sectionName)) {
        confirmActive = "active";
    }
    else if ( "completed".equalsIgnoreCase(sectionName)) {
        completedActive = "active";
    }
    super.encodeAll(context);
  }

  // Getters and setters omitted
}

我们将注解 @javax.faces.component.FacesComponent 应用到 POJO WorkerBannerComponent。这个注解向 JSF 声明我们有一个名为 workerBannerComponent 的自定义组件。 @FacesComponent 在 JSF 2.2 中被扩展,这样我们就可以用 Java 编写生成输出 HTML 的所有代码。幸运的是,我们不需要创建同时注册自己的自定义标签的自定义组件的能力,因为在轻量级 中控制标记非常方便像 Sublime 或 VIM 这样的编辑器。

我们的 WorkerBannerComponent 扩展了 javax.faces.component.UINamingContainer,这是一个由 JSF 能够添加唯一标识符。在 JSF 用语中,命名容器是一个组件的存储桶,它具有唯一的名称,并且还可以存储具有相同特征的子组件。

被覆盖的方法 encodeAll() 通常是呈现自定义标签输出的地方,该标签提供自己的标记。在这里,我们使用决定哪个工作选项卡处于活动状态而哪个不处于活动状态的逻辑来支持意图。类似于上一章的自定义事件处理(Chapter 4, JSF Validation and AJAX, Invoking an action event listener 部分),我们可以查询属性以检索参数从页面内容传递给我们的组件。

让我们检查一下这个组件的页面内容。文件名为worker-banner.xhtml,提取出来的页面内容如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" xmlns:cc="http://xmlns.jcp.org/jsf/composite" >
  <cc:interface componentType="workerBannerComponent">
    <cc:attribute name="sectionName" required="true"/>
  </cc:interface>

  <cc:implementation>
    <div class="workflow-wrapper">
      <div class="workflow-column">
        <div class="workflow-title #{cc.gettingStartedActive} pull-left" >
            Getting Started
        </div>
        <div class="workflow-arrow-right #{cc.gettingStartedActive} pull-left"></div>
      </div>

      <div class="workflow-column">
        <div class="workflow-title #{cc.yourDetailsActive} pull-left" >
            Your Details
        </div>
        <div class="workflow-arrow-right #{cc.yourDetailsActive} pull-left"></div>
      </div>

      <div class="workflow-column">
        <div class="workflow-title #{cc.yourRateActive} pull-left" >
            Your Rate
        </div>
        <div class="workflow-arrow-right #{cc.yourRateActive} pull-left"></div>
      </div>

      <div class="workflow-column">
        <div class="workflow-title #{cc.yourAddressActive} pull-left" >
            Your Address
        </div>
        <div class="workflow-arrow-right #{cc.yourAddressActive} pull-left"></div>
      </div>

      ...

      </div>
    </div>
  </cc:implementation>
</html>

在 JSF 中,自定义复合组件内容必须放在特殊目录 /resources 中。此内容的完整路径是 /resources/components/workflow-banner.xhtml。复合组件注册 在一个 XML 命名空间下,即 http://xmlns.jcp.org/jsf/composite。自定义组件需要一个接口和一个实现。 Facelets 定义了两个标签 <cc:interfaces><cc:implementation>

库标签 <cc:interface> 声明一个复合组件,属性 componentType 引用 Java 组件的名称。这里指的是WorkerBannerComponent。外部标签还包含一组 <cc:attribute> 标签,用于声明组件接受的属性。在我们的组件中,我们只接受 sectionName 属性,它允许页面作者说明客户在他们的旅程中的位置。

标签 <cc:implementation> 声明了实际的实现,也就是渲染的输出。该标记还将一个名为 cc 的特殊命名变量(代表复合 组件)放入 JSF 页面范围。我们 可以使用它来访问自定义复合组件中的属性,而这个特殊的变量只能在<cc:implementation> 标签。因此,值表达式 #{cc.gettingStartedActive} 访问 WorkerBannerComponent 中名为 gettingStartedActive 的属性 。该逻辑确保只有命名的部分将通过 CSS 突出显示为活动的。逻辑放置在一个 bean 中,因为我们需要它在 JSF 生命周期的 Render-Response 阶段而不是在构建时执行。 JSF 还在页面范围中添加了另一个特殊变量,称为组件。此变量指的是在渲染阶段正在处理的实际组件。

Tip

为什么 JSTL 不起作用?

您可能认为我们可以放弃服务器端组件并使用旧的 JavaServer Pages Tag Library (JSTL)。不幸的是,这将无法工作,因为 JSF 使用生命周期运行 ,因此,框架的后期绑定使得使用核心 JSTL 标记,例如 <c:if><c:choose><c:set> ; 行不通。此外,优秀的软件工程师知道最佳实践意味着将表示标记与业务逻辑分开。

尽管本节未显示,但可以访问 <cc:implementation> 正文内容内的自定义组件上提供的属性。值表达式 cc.attrs 提供了这样的访问。因此,如果我们想编写一个组件来访问输入属性,那么我们可以使用 #{cc.attrs.sectionName} 检索标记中的部分名称。

这就是编写复合组件的全部内容。为了使用它,我们需要添加一个 XML 命名空间,该命名空间与标签相关联到使用它的页面。让我们看看如何从我们的即时安全贷款申请中的 your-rate.xhtml 的页面内容中使用它,如以下代码所示:

<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" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" xmlns:jsf="http://xmlns.jcp.org/jsf" xmlns:xen="http://xmlns.jcp.org/jsf/composite/components">

  <ui:composition template="/basic_layout.xhtml">
    <ui:define name="mainContent">
      <xen:workflow-banner sectionName="yourRate"/>
      ...         
      
</html>

命名空间是 http://xmlns.jcp.org/jsf/composite/components ,它由名称 xen 标识。特殊目录 /resources/components 很重要,因为 JSF 默认在此位置搜索自定义组件。

现在我们可以通过元素名称<xen:workflow-banner>。在柜台下,JSF 知道它必须查找一个名为 workflow-banner.xhtml 的自定义组件定义;然后它关联组件类型 WorkerBannerComponent

回顾一下,复合组件允许 JSF 开发人员创建可重用的动态内容。自定义组件使用 XHTML Facelet 视图,并且通常使用支持 bean 或其他动作控制器。复合组件可以包括其他模板视图。只要是格式良好且有效的 XHTML Facelets,对标记的种类没有任何限制。页面作者可以在许多页面中使用此复合组件。最重要的是,这些复合组件完全支持 JSF 操作侦听器、验证器和转换器。

Composite components and custom components

前面的 部分所述,JSF 自定义组件对复合父级和渲染阶段的组件。在 <cc:implementation> 中,可以使用变量 cc 访问复合父级,使用变量访问实际处理的组件零件。

一个区别的例子将使它非常清楚。让我们创建一个只包含 XHTML 标记的自定义复合组件。 Web 根目录下的文件路径是 /resources/components/component-report.xhtml

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:cc="http://xmlns.jcp.org/jsf/composite" >

  <cc:interface/><cc:implementation>
    <div class="alert alert-info">
      <h:outputText value="Own ID: #{component.id}, parent composite ID: #{cc.id}" />
      <br/>
      <h:outputText value="Own ID: #{component.id}, parent composite ID: #{cc.id}" />
      <br/>
      <h:outputText value="Own ID: #{component.id}, parent composite ID: #{cc.id}" />
    </div>
  </cc:implementation>
</html>

默认情况下,JSF 使用基本名称 component-report 引用 XHTML。该组件只是将组件和复合 ID 转储到页面。该组件是三个 <h:outputText> 标签之一。这些的父级是复合组件本身。实际上,可以通过在抽象类 javax.faces.component 上调用静态帮助方法 getCompositeComponentParent() 以编程方式派生组件.UIComponent

让我们检查一下使用复合组件 /composite-demo.xhtml 的页面视图:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:pro="http:/www.xenonique.co.uk/jsf/instant/lending" xmlns:xen="http://xmlns.jcp.org/jsf/composite/components">

  <ui:composition template="/basic_layout.xhtml">
    <ui:define name="mainContent">
      <h1> Custom Composite Demonstations</h1>

      <xen:workflow-banner sectionName="gettingStarted"/>
      <pro:infoSec message="The definition of digital transformation" />
      <xen:component-report/>

      <a class="btn btn-primary btn-lg" href="#{request.contextPath}/index.xhtml"> Home </a>

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

XHTML 元素 <xen:component-report> 表示自定义组件。它使用命名空间 http://xmlns.jcp.org/jsf/ 定义复合/组件,和以前一样。

以下是页面视图的屏幕截图,说明了复合标签和组件标签的标识符:

读书笔记《digital-java-ee-7-web-application-development》对话与旅程

您可以查看每次处理的组件 ID 更改。

Composite component with self-generating tag

在 JSF 2.2 中,@FacesComponent 能够在不指定 任何 XML 声明的情况下生成自定义标记。这个 特性与以前版本的规范形成对比,在以前的版本中,编写可维护和可重用的自定义组件要困难得多。 JSF 2.2 添加了三个附加属性,包括属性 createTag

下表概述了 @FacesComponent 注释的属性:

属性

类型

描述

字符串

此表达式的 值是自定义组件的名称。默认情况下,它是 POJO 类的驼峰式大小写,第一个字符为小写,就像任何 Java 标识符一样。

createTag

布尔

如果为真,那么 JSF 会为此组件创建一个自定义标签。

tagName

字符串

指定 自定义组件的标签名称。

命名空间

字符串

指定自定义组件的命名空间。如果没有给出,默认是 http://xmlns.jcp.org/jsf/组件

例如,我们将编写一个基本的安全信息自定义标签,它是一个自定义 组件。

以下是名为 自定义组件的完整代码strong">InfoSecurityComponent

package uk.co.xenonique.digital.instant.control;
import javax.faces.component.FacesComponent;
import javax.faces.component.UINamingContainer;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import java.io.IOException;
import java.security.Principal;

@FacesComponent(
  value="informationSecurity",
  namespace = "http:/www.xenonique.co.uk/jsf/instant/lending",
  tagName = "infoSec", createTag = true)
public class InfoSecurityComponent extends UINamingContainer {
  private String message;

  @Override
  public String getFamily() {
    return "instant.lending.custom.component";
  }

  @Override
  public Object saveState(FacesContext context) {
    Object values[] = new Object[2];
    values[0] = super.saveState(context);
    values[1] = message;
    return ((Object) (values));
  }

  @Override
  public void restoreState(FacesContext context, Object state) {
    Object values[] = (Object[]) state;
    super.restoreState(context, values[0]);
    message = (String) values[1];
  }

  public void encodeBegin(FacesContext context)
        throws IOException {
    ResponseWriter writer = context.getResponseWriter();
    writer.startElement("div", this);
    writer.writeAttribute("role", "alert", null );
    Principal principal = FacesContext.getCurrentInstance()
        .getExternalContext().getUserPrincipal();
    String name;
    if ( principal !=null ) {
      writer.writeAttribute("class","alert  alert-success",null);
      name = principal.getName();
    }
    else {
      writer.writeAttribute("class","alert  alert-danger",null);
      name = "unknown";
    }
    writer.write(
      String.format("[USER: <strong>%s</strong>] - %s",
      name, message));
  }

  public void encodeEnd(FacesContext context)
          throws IOException {
    ResponseWriter writer = context.getResponseWriter();
    writer.endElement("div");
    writer.flush();
  }
  // Getter and setter omitted
}

再次,我们的 InfoSecurityComponent 组件扩展类 UINamingContainer,因为它处理了许多有用的 JSF 接口,例如 NamingContainerUniqueIdVendorStateHolder FacesListener。我们使用 @FacesComponent 进行注释,这一次,我们提供命名空间、createTag 和值标签。

getFamily() 方法指定该组件所属的集合。如果您正在创建一个可重用的分发组件库,并有效地帮助第三方编程工具,这将很有帮助。

saveState()restoreState() 方法演示了我们如何在多个 HTTP 请求中保持组件的状态。 StateHelpert 存在的原因是 JSF 生命周期与 HTTP 的无状态特性之间的阻抗。正如您现在已经知道的,JSF 为页面构建组件树的动态图。在 JSF 阶段之间的转换过程中,此树的状态会发生变化。保存状态允许 JSF 在用户提交页面时保存来自 Web 表单的信息。如果在转换或验证过程中出现故障,JSF 可以恢复视图的状态。

saveState() 方法中,我们创建了一个必要大小的 Object[] 数组并用值填充它。数组的第一个元素必须是保存的上下文。

另一方面,JSF 使用对象状态调用 loadState() 方法,该对象状态也是一个 Object[] 数组。我们忽略了第一个元素,因为这是一个无关紧要的,并且很可能是前一个请求的陈旧上下文。我们从数组的剩余元素中重新分配属性。

encodeBegin()encodeEnd() 方法才是真正有趣的地方。这些方法旨在呈现自定义组件的标记,即标签的输出。因为自定义组件可能嵌入其他组件,所以拆分渲染输出是一个好主意。在这里,我们使用 javax.faces.context.ResponseWriter 来构建 HTML 输出。抽象类有名为 startElement()endElement() 的方法来呈现内容的开头和结尾标记元素分别。 writeAttribute() 方法处理元素的标记属性。

所以 InfoSecurityComponent 使用 Bootstrap CSS 警报类呈现一个 div 层元素。它尝试检索当前 Java EE 安全主体的名称(如果已定义),并将该信息显示给客户。

当给定 XHTML 页面视图时:

<html xmlns="http://www.w3.org/1999/xhtml" ... xmlns:pro="http:/www.xenonique.co.uk/jsf/instant/lending" xmlns:xen="http://xmlns.jcp.org/jsf/composite/components">

...
    <pro:infoSec message="Hello world component" />

输出的 HTML 应该 如下所示:

<div class="alert alert-success" role="alert">
  [USER: <strong>unknown</strong>] - Hello world component
</div>

请注意, XHTML 中的命名空间与自定义标记的注释相匹配。

请看以下在 iOS 模拟器上运行的 your-rate.xhtml 视图的屏幕截图。它演示了应用程序的响应式网页设计功能:

读书笔记《digital-java-ee-7-web-application-development》对话与旅程

Summary


在本章中,我们通过研究一种流行的当代商业模式,在实现工作应用程序方面取得了巨大进步。我们研究了对话范围如何帮助推动即时安全贷款应用程序。对话范围使我们能够轻松编写客户旅程和引导用户逐步完成流程的向导表单。对话范围确保数据在请求和会话范围之间的生命周期内存储。

我们非常简短地讨论了一种有用的设计模式,称为实体-控制-边界。揭示了这种模式与 MVC 模式的相似之处。

在此过程中,我们看到了一个将 HTML5 范围组件与 Bootstrap CSS Progress 元素链接在一起的 JavaScript 模块。我们研究了 JSF 如何为 AJAX 提供视图的部分更新。我们还了解到,我们可以用 CDI 应用程序范围的 POJO 替换静态单例类。

最后,我们深入研究了自定义复合组件。我们现在知道如何编写部分横幅组件,甚至可以将有关 Java EE 安全主体的信息提供给页面视图。 JSF 2.2 绝对是一个有趣的标准。我认为现在您同意它非常适合现代 Web 架构。

在下一章中,我们将了解 Faces Flow。

Exercises


  1. 用简单的步骤描述本地银行更改地址的数字客户旅程。您可以先从识别步骤开始。不要试图深入研究银行安全;而是保持在 30,000 个要素的高度,并列出或列出您可能期望看到的步骤。

  2. 在研究的这个阶段,您需要知道如何将数据持久化到后备存储中。如果您还没有这样做,请修改您最喜欢的持久层,无论是 JPA、Hibernate、Mongo DB 还是其他东西。您知道如何从持久性存储中检索、保存、修改和删除实体吗?

  3. 复制并重命名空白应用程序,并编写一个简单的会话范围 bean,它可以像留言簿一样捕获 Web 评论。

  4. 确保您的支持 bean 使用 @ConversationScoped。在对话开始之前留言簿会发生什么?信息是否保留? (早期数字阶段的留言簿是一个非常简单的网络应用程序,允许互联网用户在网页上写评论。评论列表会越来越多,直到机器重新启动或崩溃。现在没有人会写这样的专业应用程序并将其部署在网站上。)

  5. 打开另一个浏览器并将其指向同一个留言簿应用程序。您是否看到与之前或之后相同的客人条目?

  6. 让我们回到您在前几章中构建的 Hobby Book Club 应用程序。我们现在将通过允许将书籍作为对话进行评论来添加它。我们将通过用户故事保持简单:

    • 作为评论者,我希望能够将我的书评添加到俱乐部的网站

    • 作为审稿人,我想看到其他人对书籍的评论,包括我自己的

    • 作为审稿人,我想编辑任何评论

    • 作为审稿人,我想删除所有评论

  7. 编写一个 @ConversationScoped 支持 bean,用于处理添加、修改和删除书评的客户旅程。要将此任务分解为更简单的里程碑,您可能更喜欢首先使用基本 Java 集合存储数据记录,而不是持久化到内存中。构建功能后,您可以使用真实的数据库。

  8. Conversation 范围非常适合数据捕获应用程序,尤其是在用户跨多个复杂部分输入信息的情况下。考虑一个捕获简历(履历)的商业网站:CV Entry Application。根据以下部分编写客户旅程的输出:

    • 个人信息(全名、资格)

    • 地址(家庭住址、电子邮件和电话号码)

    • 技能矩阵(专业行业技能)

    • 工作经验(工作地点)

    • 成就(奖项)

    • 教育(教育)

  9. 为 CV Entry Application 绘制客户旅程。使用捕获此信息的 @ConversationScoped 构建网站项目。应用 KISS(保持简单愚蠢)原则。您只需要跨多个页面演示会话状态,而不是构建一个完整的专业应用程序。

  10. 在 CV Entry Application 中,您是否关注过在页面视图中跟踪用户旅程?用户如何知道他或她在流程中的位置?编写一个启用此 UX 功能的自定义组件。

  11. 自定义组件和复合组件有什么区别?这些组件类型中的任何一个都需要支持 bean 吗?

  12. 在 CV Entry Application 中,可能还有其他可以应用内容重用的领域。编写一个捕获技能集条目的复合组件。您可能需要一个集合:Collection ,其中 SkillSet 实体具有以下属性:title (String)描述(字符串),以及年或月的经验(整数)。你是如何组织数据结构的,以使所呈现的技能的顺序与用户输入的顺序完全相同?这是对高级技能集本身的 CRUD 的点缀吗?