HTML5 和 JSF 高级教程(五)

原文:Pro JSF and HTML5 Building Rich Internet Components

协议:CC BY-NC-SA 4.0

十、创建基本的 JSF 2.2 应用

在本章中,您将详细了解如何在 Java EE 7 环境中创建一个基本的 JSF 2.2 应用。这个应用将向你展示如何在 Java EE 7 环境中设计和开发你的 JSF 应用。该应用利用 JSF 2.2 创建页面和处理页面流,CDI(上下文和依赖注入)进行 bean 管理,EJB 3.2 进行事务处理,JPA 2.1 进行数据持久化。

结构化天气应用

基本应用是关于显示保存在他/她的简档中的用户地点的天气信息的应用。在天气应用中,用户需要首先在应用中注册。为了在应用中注册,用户需要在由三个页面组成的流程中输入他/她的信息。如图图 10-1 所示,在第一页中,用户必须输入他/她的首选用户名、密码和电子邮件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-1 。天气应用注册(首页)

如果用户输入空用户名、密码或电子邮件,将显示必填字段消息,并且当用户输入无效格式的电子邮件时,将显示无效电子邮件格式消息。

在第一页输入信息后,用户进入流程的第二页,用户输入他/她的名字、姓氏和职业,如图图 10-2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-2 。天气应用注册(第二页)

最后,在注册流程的最后一页,用户在最后一页输入他/她的邮政编码,如图 10-3 所示,然后点击“完成”按钮。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-3 。天气应用注册(第三页)

在应用中注册后,用户将能够使用他/她的用户名和密码登录应用,如图 10-4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-4 。天气应用登录页面

登录应用后,如图 10-5 所示,用户将被转到天气屏幕,在该屏幕中,用户将能够了解他在注册最终页面中输入的地方的天气信息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-5 。天气应用主页

现在,在浏览完天气应用的页面后,让我们看看如何构建它。图 10-6 显示了天气应用的结构。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-6 。天气应用结构

如上图所示,该应用具有以下结构:

  1. XHTML 页面: 这些表示天气应用页面。它使用 JSF 表达式语言(EL)来使用支持 bean 和受管 bean。
  2. **Backing bean:**这些是普通的托管 bean,它们在概念上与 UI 页面相关,并且不是应用模型的一部分。Backing beans 是集中处理页面操作的理想选择。在天气应用中,backing beans 主要获取托管 bean 的实例,这些实例携带用户输入的数据,然后调用 UserManager EJB 来执行所需的操作。
  3. 用户经理 EJB: 为了执行不同的业务操作,backing beans 调用用户经理 EJB。用户管理器 EJB 是一个无状态会话 EJB*,它使用 JPA 实体和 JPA EntityManager 来执行所需的数据库操作。
  4. **JPA 实体(CDI 托管 bean)😗*JPA 实体表示映射到数据库表的数据类。在 weather 应用中,JPA 实体被用作应用的 CDI 托管 beanss,这些 bean 使用 EL 与 XHTML 页面绑定在一起。

注意,为了简单起见,应用使用 Oracle Java DB。Java DB 是 Oracle 支持的 Apache Derby 开源数据库的发行版。它通过 JDBC 和 Java EE APIs 支持标准 ANSI/ISO SQL,并包含在 JDK 中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意要知道 JPA 可以不用 EJBs 然而,在 JPA 应用中使用 EJB 有一个很大的优势,那就是通过 EJB 容器隐式地处理应用事务(容器管理的事务)。尽管天气应用是一个基本的 JSF 2.2 应用,但我们坚持引入包含 JPA 的 EJB,以便向您展示这些技术如何在 JSF 2.2 应用中协同工作。

在接下来的部分中,我们将深入应用组件的细节。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意需要注意的是,深入 EJB 和 JPA 的细节超出了本书的范围。为了了解学习它们的能力,我们推荐你阅读 Oracle Java EE 教程:docs.oracle.com/javaee/7/tutorial/doc/

构建 JSF 页面

天气应用有以下 XHTML 页面:

  1. 主页(home.xhtml):表示应用的登录页面,如图图 10-4 所示。
  2. 注册页面(/registration/*。xhtml):它们表示包含注册流程的页面;注册页面包括以下页面:
    • a.registration.xhtml 页面,表示图 10-1 所示流程中的第一个注册页面。
    • b.extraInfo.xhtml 页面,表示图 10-2 所示流程中的第二个注册页面。
    • c.final.xhtml 页面,表示图 10-3 所示流程中的最终注册页面。
  3. 天气页面(/protected/weather.xhtml),代表图 10-5 所示的天气页面。

清单 10-1 显示了 home.xhtml 页面代码。

清单 10-1。 首页 XHTML 代码

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html">

<ui:composition template="/WEB-INF/templates/main.xhtml">
    <ui:define name="title">
        #{bundle['application.loginpage.title']}
    </ui:define>
    <ui:define name="content">
        <h:form>
            <h:panelGrid columns="3">
                <h:outputText value="#{bundle['user.id']}"></h:outputText>
                <h:inputText id="userID"
                             value="#{appUser.id}"
                             required="true"
                             requiredMessage="#{bundle['user.id.validation']}">
                </h:inputText>
                <h:message for="userID" styleClass="errorMessage"/>

                <h:outputText value="#{bundle['user.password']}"></h:outputText>
                <h:inputSecret id="password"
                               value="#{appUser.password}"
                               required="true"
                               requiredMessage="#{bundle['user.password.validation']}">
                </h:inputSecret>
                <h:message for="password" styleClass="errorMessage"/>
            </h:panelGrid>

            <h:commandButton value="#{bundle['application.login']}" action="#{loginBacking.login}"/> <br/>
            <h:link value="#{bundle['application.loginpage.register']}" outcome="registration"/>
            <br/><br/>
            <h:messages styleClass="errorMessage"/>
        </h:form>
    </ui:define>
</ui:composition>

</html>

如前面的代码所示,天气应用的主页包含用户名 InputText 和密码 InputSecret。login CommandButton 调用 LoginBacking bean 的 login 方法,registration 链接导航到“registration”流(将在下一节中详细说明)。清单 10-2 显示了 LoginBacking bean。

清单 10-2。 登录备份豆

package com.jsfprohtml5.weather.backing;

import com.jsfprohtml5.weather.model.AppUser;
import com.jsfprohtml5.weather.model.UserManagerLocal;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped;
import javax.faces.application.FacesMessage;
import javax.inject.Named;

@Named
@RequestScoped
public class LoginBacking extends BaseBacking {

    @EJB
    private UserManagerLocal userManager;

    public String login() {
        AppUser currentAppUser = (AppUser) evaluateEL("#{appUser}", AppUser.class);

        try {
            AppUser appUser = userManager.getUser(currentAppUser.getId(), currentAppUser.getPassword());

            if (appUser == null) {
                getContext().addMessage(null, new FacesMessage(INVALID_USERNAME_OR_PASSWORD));
                return null;
            }

            //Set Necessary user information
            currentAppUser.setEmail(appUser.getEmail());
            currentAppUser.setFirstName(appUser.getFirstName());
            currentAppUser.setLastName(appUser.getLastName());
            currentAppUser.setZipCode(appUser.getZipCode());
            currentAppUser.setProfession(appUser.getProfession());
        } catch (Exception ex) {
            Logger.getLogger(LoginBacking.class.getName()).log(Level.SEVERE, null, ex);
            getContext().addMessage(null, new FacesMessage(SYSTEM_ERROR));
            return null;
        }

        return "/protected/weather";
    }

    ...
}

LoginBacking bean 是处理登录操作的后备 bean。它调用用户管理器 EJB,以便知道用户是否使用 getUser()方法在系统中注册(用户管理器 EJB 将在“应用后端”一节中详细说明)。为了获取与用户名和密码字段绑定的 AppUser 的 CDI 托管 bean 实例,调用 evaluateEL()方法来计算#{appUser}表达式。evaluateEL()方法位于 base backing bean (BaseBacking)类中。

如果 getUser()方法返回 null,这意味着用户没有使用输入的用户名和密码组合在系统中注册,并且为用户显示无效的用户名或密码消息。如果用户名和密码组合有效,则在#{appUser}托管 bean 实例(appUser 既是请求范围的 CDI 托管 bean,也是 JPA 实体类,将在“应用后端”一节中详细说明)中检索和设置用户信息,并将页面转发到天气页面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意 @EJB 注释可以用来注释 bean 的实例变量,以指定对 EJB 的依赖。Application Server 使用依赖注入,通过引用它所依赖的 EJB,自动初始化带注释的变量。这个初始化发生在调用 bean 的任何业务方法之前和设置 bean 的 EJBContext 之后。

如图 10-7 所示,所有天气应用的后台 beans 都从 BaseBacking 类扩展而来。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-7 。天气应用的支持 beans

清单 10-3 显示了 BaseBacking 类的代码。

***清单 10-3。***base backing Bean 类

package com.jsfprohtml5.weather.backing;

import java.util.Map;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpSession;

public class BaseBacking {

    protected FacesContext getContext() {
        return FacesContext.getCurrentInstance();
    }

    protected Map getRequestMap() {
        return getContext().getExternalContext().getRequestMap();
    }

    protected HttpSession getSession() {
        return (HttpSession) getContext().getExternalContext().getSession(false);
    }

    protected Object evaluateEL(String elExpression, Class beanClazz) {
        return getContext().getApplication().evaluateExpressionGet(getContext(), elExpression, beanClazz);
    }

    ...
}

BaseBacking class 是一个基类,它包含获取 JSF Faces 上下文、获取 HTTP 会话、获取 HTTP 请求映射和评估 JSF 表达式的快捷方式。清单 10-4 显示了 weather.xhtml 页面代码。

清单 10-4。 天气主页

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:mashup="http://code.google.com/p/mashups4jsf/">

<ui:composition template="/WEB-INF/templates/main.xhtml">
    <ui:define name="title">
        #{bundle['application.weatherpage.title']}
    </ui:define>
    <ui:define name="content">
        <h:form>
            #{bundle['application.welcome']}, #{appUser.firstName} #{appUser.lastName}! <br/><br/>

            #{bundle['application.weatherpage.currentInfo']} for #{appUser.zipCode}:
            <mashup:yahooWeather temperatureType="c" locationCode="#{appUser.zipCode}"/> <br/><br/>

            <h:commandLink value="#{bundle['application.weatherpage.logout']}"
                           action="#{weatherBacking.logout}"></h:commandLink> <br/><br/>
        </h:form>
    </ui:define>
</ui:composition>

</html>

天气页面向用户和 Yahoo!使用 Mashups4JSF 库的 yahooWeather 组件(code.google.com/p/mashups4jsf/)来检索天气信息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意 Mashups4JSF 是一个开源项目,旨在集成 Mashup 服务和 JavaServer Faces 应用。使用 Mashups4JSF,JSF 开发人员将能够通过使用简单的标记来构建丰富的定制 Mashups。Mashups4JSF 还允许通过用@Feed 注释对应用域类进行注释,将 Java 企业应用数据作为 Mashup feeds 导出。更多信息请查看项目主页:code.google.com/p/mashups4jsf/

yahooWeather 组件使您能够查看世界上特定位置的当前天气状况(使用 Yahoo!引擎盖下的天气服务)使用其邮政编码。它有两个主要属性,如表 10-1 所示。

表 10-1 。Mashups4JSF yahooWeather 组件

|

组件属性

|

描述

|
| — | — |
| 位置代码 | 该位置的邮政编码 |
| 温度类型 | 以华氏(f)或摄氏(c)为单位的温度。默认是c。 |

为了在我们的 JSF 应用中配置 Mashups4JSF,我们需要向您的 web 应用的 lib 文件夹添加两个 jar:

  • Mashups4JSF 1.0.0 核心 jar。
  • 罗马 0.9 jar。

如果我们的应用是一个 Maven 应用,我们需要将这些 jar 添加到应用的 pom.xml 中,如清单 10-5 所示。

***清单 10-5。***POM . XML 中的 Mashups4JSF 依赖

<project ...>
    ...
    <dependencies>
        ...
        <dependency>
            <groupId>com.googlecode.mashups4jsf</groupId>
            <artifactId>mashups4jsf-core</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>rome</groupId>
            <artifactId>rome</artifactId>
            <version>0.9</version>
        </dependency>
    </dependencies>

    <repositories>
        ...
        <repository>
            <id>googlecode.com</id>
            <url>[`mashups4jsf.googlecode.com/svn/trunk/mashups4jsf-repo</url`](http://mashups4jsf.googlecode.com/svn/trunk/mashups4jsf-repo</url) >
        </repository>
    </repositories>
</project>

将 Mashups4JSF jars 添加到应用的依赖项之后,我们可以将它包含在 XHTML 页面中,如下所示:

<html FontName">http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:mashup="http://code.google.com/p/mashups4jsf/">

天气页面有一个 logout CommandLink,其操作与 WeatherBacking bean 类的 logout 方法绑定在一起。清单 10-6 显示了 WeatherBacking bean 类。

清单 10-6。 逆风豆类

package com.jsfprohtml5.weather.backing;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;

@Named
@RequestScoped
public class WeatherBacking extends BaseBacking {
    public String logout() {
        getSession().invalidate();

        return "/home.xhtml?faces-redirect=true";
    }
}

在 logout()方法中,会话被无效,用户被转到主页。请注意,所有应用页面都使用/WEB-INF/templates 文件夹下的 main.xhtml 模板。清单 10-7 显示了 main.xhtml 模板页面。

清单 10-7。 main.xhtml 模板页面

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html">

<h:head>
  <title><ui:insert name="title">#{bundle['application.defaultpage.title']}</ui:insert></title>
  <link href="#{request.contextPath}/css/main.css" rel="stylesheet" type="text/css"/>
</h:head>

<h:body>
    <div id="container">
        <div id="header">
            <ui:insert name="header">
                <h1>#{bundle['application.defaultpage.header.content']}</h1>
            </ui:insert>
        </div>

        <div id="content">
            <ui:insert name="content">
                #{bundle['application.defaultpage.body.content']}
            </ui:insert>
        </div>

        <div id="footer">
            <ui:insert name="footer">
                #{bundle['application.defaultpage.footer.content']}
            </ui:insert>
        </div>
    </div>
</h:body>
</html>

该模板使用 main.css 样式文件,它有三个主要部分:页眉、页脚和内容。应用中的不同页面(主页、天气和注册页面)应该替换这些内容。应用的文本在 messages.properties 文件中具体化,如清单 10-8 中的所示。

清单 10-8。 messages.properties 文件

user.id = Username
user.password = Password
user.email = Email
user.fname = First name
user.lname = Last name
user.profession = Profession
user.zipCode = Zip code

user.id.validation = You need to enter a username
user.password.validation = You need to enter a password
user.email.validation = You need to enter an email
user.email.invalid = Invalid Email
user.fname.validation = You need to enter first name
user.lname.validation = You need to enter last name
user.zipCode.validation = You need to enter zip code

user.profession.profession1 = Software Engineer
user.profession.profession2 = Project Manager
user.profession.profession3 = Other

application.next = Next
application.back = Back
application.cancel = Cancel
application.finish = Finish

application.login = Login
application.loginpage.title = Login page
application.loginpage.register = New user? register now!

application.welcome = Welcome
application.weatherpage.title = Weather page
application.weatherpage.logout = Logout
application.weatherpage.currentInfo = Current Weather Information

application.register = Register
application.register.title = Registration page
application.register.return = Back to home

application.defaultpage.title = Default Title
application.defaultpage.header.content = Welcome to the weather application
application.defaultpage.body.content = Your content here ...
application.defaultpage.footer.content = Thanks for using the application

模板的 main.css 文件用于处理外观和布局,如清单 10-9 所示。

清单 10-9。 main.css 样式文件

h1, p, body, html {
    margin:0;
    padding:0;
    font-family: sans-serif;
}

body {
    background-color: #B3B1B2;
}

#container {
    width:100%;
}

a {
    font-size: 12px;
}

#header {
    background-color: #84978F;
    padding: 50px;
}

#header h1 {
    margin-bottom: 0px;
    text-align: center;
}

#content {
    float: left;
    margin: 10px;
    height: 400px;
    width: 100%;
}

#footer {
    clear:both;    /*No floating elements are allowed on left or right*/
    background-color: #84978F;
    text-align:center;
    font-weight: bold;
    padding: 10px;
}

.errorMessage {
    font-size: 12px;
    color: red;
    font-family: sans-serif;
}

为了保护天气页面,它被放在一个名为("/protected ")的自定义文件夹中,并创建了一个自定义 JSF 相位监听器来保护页面,如清单 10-10 中的所示。

清单 10-10。 授权监听器类

package com.jsfprohtml5.weather.util;

import javax.faces.application.NavigationHandler;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;

public class AuthorizationListener implements PhaseListener {

    @Override
    public void afterPhase(PhaseEvent event) {
        FacesContext context = event.getFacesContext();
        NavigationHandler navigationHandler = context.getApplication().getNavigationHandler();

        String currentPage = context.getViewRoot().getViewId();

        boolean isProtectedPage = currentPage.contains("/protected/");

        //Restrict access to protected pages ...
        if (isProtectedPage) {
            navigationHandler.handleNavigation(context, null, "/home?faces-redirect=true");
        }
    }

    @Override
    public void beforePhase(PhaseEvent event) {
        //Nothing ...
    }

    @Override
    public PhaseId getPhaseId() {
        return PhaseId.RESTORE_VIEW;
    }
}

AuthorizationListener 阶段侦听器禁止任何用户直接访问受保护文件夹中的页面。

在下一节中,我们将浏览注册页面,以了解如何利用 JSF 2.2 Faces 流在我们的天气应用中实现注册流行为。

利用面流

正如您从第五章中了解到的,JSF 2.2 中引入了 Faces Flow 来支持 JSF 应用中的流管理。过去,为了在 JSF 应用中实现流,JSF 开发人员要么使用额外的框架,如 Spring Web Flow 或 ADF Task Flows,要么使用 HTTP 会话手动实现。使用 HTTP 会话手动实现它不是一种有效的实现方式,因为 JSF 开发人员必须在流完成或退出后处理会话清理。

在 JSF Faces Flow 中,开发人员可以在一组相关页面(或视图或节点)上定义 流,并定义好入口点和出口点。在天气应用中,我们将流页面打包在单个目录(/registration)中,以方便遵守 JSF 流公约规则,这些规则如下:

  1. 流目录中的每个 XHTML 文件都充当流的视图节点。
  2. 流的开始节点是视图,其名称与流的名称(registration.xhtml)相同。
  3. 流目录中页面之间的导航被视为流内的导航。
  4. 导航到流程目录之外的视图被认为是流程的出口。

最后,为了定义 Faces 流,您应该在 Faces 配置文件(faces-config.xml)中声明它,如清单 10-11 所示。

清单 10-11。 在 Faces 配置文件 中定义 Faces 流

<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="2.2"
    FontName">http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">

    <flow-definition id="registration">
        <flow-return id="flowReturn">
            <from-outcome>/home</from-outcome>
        </flow-return>
    </flow-definition>

    ...
</faces-config>

为了定义流,您使用标签来指定流的 ID。标记代表流返回,它必须有一个元素;在我们的天气应用中,流返回 ID 是“flow return ”,当返回结果“/home ”(表示主页)时,流返回。现在,让我们浏览一下注册页面。清单 10-12 显示了 registration.xhtml 页面。

清单 10-12。 registration.xhtml 页面

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

<ui:composition template="/WEB-INF/templates/main.xhtml">
    <ui:define name="title">
        #{bundle['application.register.title']}
    </ui:define>
    <ui:define name="content">
        <h:form>
            <h:panelGrid columns="3">
                <h:outputText value="#{bundle['user.id']}"></h:outputText>
                <h:inputText id="userID"
                             value="#{flowScope.id}"
                             required="true"
                             requiredMessage="#{bundle['user.id.validation']}">
                </h:inputText>
                <h:message for="userID" styleClass="errorMessage"/>

                <h:outputText value="#{bundle['user.password']}"></h:outputText>
                <h:inputSecret id="password"
                               value="#{flowScope.password}"
                               required="true"
                               requiredMessage="#{bundle['user.password.validation']}">
                </h:inputSecret>
                <h:message for="password" styleClass="errorMessage"/>

                <h:outputText value="#{bundle['user.email']}"></h:outputText>
                <h:inputText id="email"
                             value="#{flowScope.email}"
                             required="true"
                             requiredMessage="#{bundle['user.email.validation']}"
                             validatorMessage="#{bundle['user.email.invalid']}">

                    <f:validateRegex pattern="[\w\.-]*[a-zA-Z0-9_]@[\w\.-]*[a-zA-Z0-9]\.[a-zA-Z][a-zA-Z\.]*[a-zA-Z]"/>
                </h:inputText>
                <h:message for="email" styleClass="errorMessage"/>
            </h:panelGrid>

            <h:commandButton value="#{bundle['application.cancel']}" action="flowReturn"
                             immediate="true"/>
            <h:commandButton value="#{bundle['application.next']}" action="extraInfo"/> <br/>
        </h:form>
    </ui:define>
</ui:composition>

</html>

使用#{flowScope} EL 对象,我们可以将对象存储在流作用域中,它相当于 facesContext.getApplication()。getFlowHandler()。getCurrentFlowScope() API。表达式#{flowScope.id}、#{flowScope.password}和#{flowScope.email}与用户 id、密码和电子邮件输入字段绑定。另一个需要注意的重要事情是“cancel”command button 的动作,它被设置为注册流返回 ID(“flow return”);这意味着当点击“取消”命令按钮时,用户将被转到主页。清单 10-13 显示了注册流程(extraInfo.xhtml)页面中的第二页。

清单 10-13。 extraInfo.xhtml 页面

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

<ui:composition template="/WEB-INF/templates/main.xhtml">
    <ui:define name="title">
        #{bundle['application.register.title']}
    </ui:define>
    <ui:define name="content">
        <h:form>
            <h:panelGrid columns="3">
                <h:outputText value="#{bundle['user.fname']}"></h:outputText>
                <h:inputText id="fname"
                             value="#{flowScope.fname}"
                             required="true"
                             requiredMessage="#{bundle['user.fname.validation']}">
                </h:inputText>
                <h:message for="fname" styleClass="errorMessage"/>

                <h:outputText value="#{bundle['user.lname']}"></h:outputText>
                <h:inputText id="lname"
                               value="#{flowScope.lname}"
                               required="true"
                               requiredMessage="#{bundle['user.lname.validation']}">
                </h:inputText>
                <h:message for="lname" styleClass="errorMessage"/>

                <h:outputText value="#{bundle['user.profession']}"></h:outputText>
                <h:selectOneMenu id="profession"
                                 value="#{flowScope.profession}">

                    <f:selectItem itemLabel="#{bundle['user.profession.profession1']}" itemValue="SE"/>
                    <f:selectItem itemLabel="#{bundle['user.profession.profession2']}" itemValue="PM"/>
                    <f:selectItem itemLabel="#{bundle['user.profession.profession3']}" itemValue="OT"/>
                </h:selectOneMenu>
                <h:message for="profession" styleClass="errorMessage"/>
            </h:panelGrid>

            <h:commandButton value="#{bundle['application.cancel']}" action="flowReturn"
                             immediate="true" />
            <h:commandButton value="#{bundle['application.back']}" action="registration"
                              immediate="true" />
            <h:commandButton value="#{bundle['application.next']}" action="final"/> <br/>
        </h:form>
    </ui:define>
</ui:composition>

</html>

表达式#{flowScope.fname}、#{flowScope.lname}和#{flowScope.profession}与用户名、姓和职业输入字段绑定。需要注意的一点是,只要用户在流页面之间导航,流数据就是活动的;这意味着如果用户单击 back 按钮转到初始注册,那么他将能够看到他之前在初始页面中输入的数据。清单 10-14 显示了注册流程(final.xhtml)页面的最后一页。

***清单 10-14。***final . XHTML 页面

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html">

<ui:composition template="/WEB-INF/templates/main.xhtml">
    <ui:define name="title">
        #{bundle['application.register.title']}
    </ui:define>
    <ui:define name="content">
        <h:form prependId="false">
            <h:panelGrid columns="3">
                <h:outputText value="#{bundle['user.zipCode']}"></h:outputText>
                <h:inputText id="woeid"
                             value="#{flowScope.zipCode}"
                             required="true"
                             requiredMessage="#{bundle['user.zipCode.validation']}">
                </h:inputText>
                <h:message for="woeid" styleClass="errorMessage"/>
            </h:panelGrid>

            <h:commandButton value="#{bundle['application.cancel']}"
                             immediate="true" action="flowReturn" />
            <h:commandButton value="#{bundle['application.back']}" immediate="true" action="extraInfo"/>
            <h:commandButton value="#{bundle['application.finish']}" action="#{registrationBacking.register}"/> <br/>
            <h:messages styleClass="errorMessage"/>
        </h:form>
    </ui:define>
</ui:composition>

</html>

最后,#{flowScope.zipCode}与用户邮政编码输入文本绑定。当用户单击“Finish”命令按钮时,将调用 RegistrationBacking bean 的 register()方法在应用中注册用户。清单 10-15 显示了注册支持 bean。

清单 10-15。 注册后台 Bean 类

package com.jsfprohtml5.weather.backing;

import com.jsfprohtml5.weather.model.AppUser;
import com.jsfprohtml5.weather.model.UserExistsException;
import com.jsfprohtml5.weather.model.UserManagerLocal;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.inject.Named;

@Named
@RequestScoped
public class RegistrationBacking extends BaseBacking {

    @EJB
    private UserManagerLocal userManager;

    public String register() {
        FacesContext context = FacesContext.getCurrentInstance();
        Map<Object, Object> flowScope = context.getApplication().getFlowHandler().getCurrentFlowScope();

        AppUser appUser = new AppUser();

        appUser.setId((String) flowScope.get("id"));
        appUser.setPassword((String) flowScope.get("password"));
        appUser.setEmail((String) flowScope.get("email"));

        appUser.setFirstName((String) flowScope.get("fname"));
        appUser.setLastName((String) flowScope.get("lname"));
        appUser.setProfession((String) flowScope.get("profession"));

        appUser.setZipCode((String) flowScope.get("zipCode"));

        try {
            userManager.registerUser(appUser);
        } catch (UserExistsException ex) {
            Logger.getLogger(RegistrationBacking.class.getName()).log(Level.SEVERE, null, ex);
            context.addMessage(null, new FacesMessage(USERNAME_ALREADY_EXISTS));
            return null;
        } catch (Exception ex) {
            Logger.getLogger(RegistrationBacking.class.getName()).log(Level.SEVERE, null, ex);
            context.addMessage(null, new FacesMessage(SYSTEM_ERROR));
            return null;
        }

        return "flowReturn";
    }

    ...
}

RegistrationBacking bean 是一个处理用户注册的 Backing bean。为了获得流数据,使用 context.getApplication()检索流范围。getFlowHandler()。getCurrentFlowScope() API。AppUser JPA 实体类用来自流范围的用户数据进行实例化和传播,然后传递给 UserManager EJB 的 regiserUser()方法。如果注册成功,则返回注册流程,并将用户转到主页。

AppUser JPA 实体类将在下一节中说明。

构成受管 bean(JPA 实体 bean)

在天气应用中,我们有一个单独的托管 bean(和 JPA 实体类),它是 AppUser 类。清单 10-16 显示了 AppUser 类。

清单 10-16。 AppUser 实体类

package com.jsfprohtml5.weather.model;

import java.io.Serializable;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Entity
@Table(name = "APP_USER")
@Named
@RequestScoped
public class AppUser implements Serializable {
    private static final long serialVersionUID = 134523456789194332L;

    @Id
    @NotNull
    @Size(min = 1, max = 64)
    @Column(name = "ID")
    private String id;

    @NotNull
    @Size(min = 1, max = 32)
    @Column(name = "FIRST_NAME")
    private String firstName;

    @NotNull
    @Size(min = 1, max = 32)
    @Column(name = "LAST_NAME")
    private String lastName;

    @NotNull
    @Size(min = 1, max = 32)
    @Column(name = "PASSWORD")
    private String password;

    @NotNull
    @Size(min = 1, max = 32)
    @Column(name = "PROFESSION")
    private String profession;

    @NotNull
    @Size(max = 64)
    @Column(name = "EMAIL")
    private String email;

    @NotNull
    @Size(max = 32)
    @Column(name = "ZIP_CODE")
    private String zipCode;

    public AppUser() {
    }
    public AppUser(String id) {
        this.id = id;
    }
    public AppUser(String id, String firstName, String lastName, String password, String profession, String zipCode) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.password = password;
        this.profession = profession;
        this.zipCode = zipCode;
    }

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }

    public String getProfession() {
        return profession;
    }
    public void setProfession(String profession) {
        this.profession = profession;
    }

    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }

    public String getZipCode() {
        return zipCode;
    }
    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }

    @Override
    public String toString() {
        return "ID = " + id;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        if (!(object instanceof AppUser)) {
            return false;
        }
        AppUser other = (AppUser) object;
        if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
            return false;
        }
        return true;
    }
}

AppUser 是一个 JPA 实体类。@Entity 批注用于将类标记为实体类;@Table 注释用于显式设置 JPA 实体映射到的表名。@Named 和@RequestScoped 都用于在请求范围内将 AppUser 类声明为 CDI 托管 bean。如果我们研究 AppUser 类属性,我们会发现以下 JPA 注释:

  • @Id 注释用于将类属性标记为唯一标识符。
  • @Column 注释用于显式设置 JPA 类属性映射到的列名。

在下一节中,我们将看到如何配置 JPA 持久性单元并创建用户管理器 EJB。

应用后端(EJB 3.2 + JPA 2.1)

现在我们来看关于应用后端的部分,它使用 EJB 3.2 和 JPA 2.1,它们是 Java EE 7 平台的一部分。在上一节中,我们已经看到了应用的单个 JPA 实体(AppUser)类,但是我们不知道如何使用实体 bean 来执行不同的数据库操作。为了执行数据库操作,我们需要定义 persistence.xml 文件,如清单 10-17 所示。

***清单 10-17。***persistence . XML 文件

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" FontName">http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">

     <persistence-unit name="weatherUnit" transaction-type="JTA">
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <jta-data-source>jdbc/weatherDB</jta-data-source>
     </persistence-unit>
</persistence>

在 persistence.xml 文件中(在/resources/META-INF 下),定义了持久性单元名称“weatherUnit”,事务类型设置为“JTA”(Java 事务 API)。在持久性单元内部,我们定义了 JPA provider 类(org . eclipse . persistence . JPA . persistence provider)和 to be (jdbc/weatherDB)。元素表示 JDBC 数据源的 JNDI 名称。清单 10-18 显示了用户管理器本地 EJB 接口。

清单 10-18。 UserManager 本地 EJB 接口

package com.jsfprohtml5.weather.model;

import javax.ejb.Local;

@Local
public interface UserManagerLocal {
    public AppUser getUser(String userID, String password);
    public void registerUser(AppUser user) throws UserExistsException;
}

用户管理器 EJB 实现了用户管理器本地接口,如清单 10-19 所示。

清单 10-19。 用户经理 EJB

package com.jsfprohtml5.weather.model;

import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

@Stateless
public class UserManager implements UserManagerLocal {

    @PersistenceContext(unitName = "weatherUnit")
    EntityManager em;

    @Override
    public AppUser getUser(String userID, String password) {
        Query query = em.createQuery("select appUser from AppUser appUser where "
                    + "appUser.id = :id and appUser.password = :password");

        query.setParameter("id", userID);
        query.setParameter("password", password);

        List<AppUser> result = query.getResultList();

        if (result != null && result.size() > 0) {
            return result.get(0);
        }

        return null;
    }

    @Override
    public void registerUser(AppUser appUser) throws UserExistsException {
        Query query = em.createQuery("select appUser from AppUser appUser where "
                    + "appUser.id = :id");

        query.setParameter("id", appUser.getId());

        List<AppUser> result = query.getResultList();

        if (result != null && result.size() > 0) {
            throw new UserExistsException();
        }

        em.persist(appUser);
    }
}

@Stateless annotation 将 UserManager 类定义为无状态会话 EJB。@PersistenceContext 注释用于注入容器管理的实体管理器实例。使用注入的实体管理器实例,我们将能够执行数据库操作。在用户管理器 EJB 中,有两种主要方法:

  1. getUser()方法,,该方法使用用户名和密码从数据库中检索用户。如果用户不存在,则返回 null。
  2. registerUser()方法,该方法执行以下操作:
  • 如果用户 ID 已经存在,它将抛出 UserExistsException。
  • 如果用户 ID 不存在,那么用户将被保存在数据库中。

清单 10-20 显示了用户存在异常类。

***清单 10-20。***userixsexception 类

package com.jsfprohtml5.weather.model;

public class UserExistsException extends Exception {
}

UserExistsException 是一个简单的自定义异常,它扩展了 Exception 类。

天气应用是在 GlassFish 版下开发的。清单 10-21 显示了定义应用数据源的 glassfish-resources.xml。

清单 10-21。 天气应用 glassfish-resources.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource
Definitions//EN" "http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
<resources>
    <jdbc-connection-pool ...>
        <property name="serverName" value="localhost"/>
        <property name="portNumber" value="1527"/>
        <property name="databaseName" value="weatherDB"/>
        <property name="User" value="weather"/>
        <property name="Password" value="password"/>
        <property name="URL" value="jdbc:derby://localhost:1527/weatherDB"/>
        <property name="driverClass" value="org.apache.derby.jdbc.ClientDriver"/>
    </jdbc-connection-pool>
    <jdbc-resource enabled="true" jndi-name="jdbc/weatherDB" object-type="user" pool-name="derby_net_weatherDB_weatherPool"/>
</resources>

为了在 glassfish 4 中添加 glassfish-resources.xml 的已定义资源,您需要通过从服务器的 bin 目录运行以下命令来启动 GlassFish 服务器。

> asadmin start-domain

服务器启动后,您可以使用 asadmin add-resources 命令,如下所示,以便在服务器中添加已定义的资源。

> asadmin add-resources <<full path>>/glassfish-resources.xml

运行前面的命令后,资源将被添加到您的 GlassFish 服务器中。最后清单 10-22 显示了我们在 weatherDB 中的单个 APP_USER 表。

***清单 10-22。***APP _ 用户表 DDL 脚本

CREATE TABLE APP_USER (
    ID VARCHAR(64) PRIMARY KEY,
    FIRST_NAME VARCHAR(32),
    LAST_NAME VARCHAR(32),
    PASSWORD VARCHAR(32),
    PROFESSION VARCHAR(32),
    EMAIL VARCHAR(64),
    ZIP_CODE VARCHAR(32)
);

weatherDB 是一个 JavaDB Derby 数据库;它包含在(src/main/database)目录下的应用源代码中,供您参考。为了在 GlassFish 服务器中安装数据库,如果 GlassFish 服务器正在运行,请按如下方式停止它:

> asadmin stop-domain domain1

我们在这里假设您的 GlassFish 域名是(domain1)。停止服务器后,还要停止 GlassFish Java DB,如下所示:

> asadmin stop-database

停止服务器和 Java DB 后,将位于(src/main/database)目录下的 weatherDB 目录复制到您的([GlassFish server]/GlassFish/databases)目录,然后启动服务器和 Java DB。Java DB 可以使用以下命令启动:

> asadmin start-database

启动服务器和 Java DB 后,您可以在 GlassFish 服务器中部署天气应用并开始使用它。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意天气应用是一个 Maven web 项目,因此为了构建它,您可以使用 mvn clean install 命令,然后在您的 GlassFish 4 服务器中部署输出 war 文件(weather-1.0-SNAPSHOT.war)。请注意,输出 war 文件将位于目标目录下。为了正确运行 Maven 命令,请确保 JAVA_HOME 指向安装在您的操作系统中的 Java 7 目录。

摘要

在本章中,你详细地学习了如何在 Java EE 7 环境中创建一个基本的 JSF 2.2 应用。你了解了如何构建 JSF 2.2 应用。您学习了如何利用 JSF 2.2 Faces 流来处理公共页面之间的流。最后,您了解了如何利用不同的 Java EE 7 技术(CDI、JPA 2.1 和 EJB 3.2)来促进 JSF 应用中的 bean 管理、事务管理和持久性。

十一、JSF2 高级主题

本章收集了 JSF 应用作者在编写现实生活中的 JSF 应用时必须考虑的高级主题。我们还将看看如何使用< f:ajax >标签来 ajax 化并改善用户体验。Ajax 部分之后是使用 JavaScript 开发 JSF JavaScript API 的例子。最后,如果没有彻底的测试,你就无法构建一个真实世界的应用。我们将通过 JBoss 社区研究由 Red Hat 赞助的 Arquillian 测试框架。

JSF 应用的设计考虑因素

本节重点介绍在构建 JSF 应用时应该考虑的设计注意事项。我们将触及最小化会话数据使用的重要性,如何实现安全性,在哪里保存状态视图,以及最后托管 bean 作用域与 CDI 作用域的比较。

最小化会话范围的使用

当开发 JSF 应用时,你应该特别小心使用会话范围的 bean。将对象存储在用户会话中可能很诱人,因为在整个应用中都可以方便地使用它。这种便利的问题是每个用户的内存占用越来越多。您可能会得到大型的会话对象,直到用户结束会话时才被收集。这将限制并发用户的数量,因为访问系统的用户越多,应用服务器需要的物理内存就越多,从而使应用不可伸缩。会话范围的对象应该只用于存储应该从用户会话开始到结束都有效的数据。贯穿整个用户会话的公共数据可以是用户名、人名、登录时间和首选项。您应该避免使用会话范围的对象来存储有关主-详细信息中选定对象的数据,或者存储可能非常大的二进制对象,如用户配置文件图片。根据经验,在您的 JSF 应用中应该只有一个会话范围的受管 bean。如果您觉得需要多个会话范围的 beans,请认真考虑您的应用的架构。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示对于开发人员来说,直到软件开发生命周期结束时才解决应用性能问题是很常见的。然而,当您在 JSF 中使用会话对象时,您必须在开发过程的早期和整个过程中注意应用的性能测试和概要分析。在模拟多个用户会话时分析内存占用尤其重要。有许多可用的 Java 概要文件。我们建议您使用与您的开发环境很好地集成的分析器,以便在开发过程中尽可能容易地分析您的应用。如果 profiler 使用起来很慢而且很麻烦,您可能会避免使用它,并且您也不会尽早发现可伸缩性问题。一些 ide 有内置的分析器,可以很容易地分析内存和 CPU 消耗,如图 11-1 所示。要模拟多个用户会话,您可以使用像 Apache jMeter(jmeter.apache.org)这样的开源工具,在这里您可以构建一个测试计划,通过在一段时间内生成多个线程来模拟多个用户。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-1 。具有内置探查器的 IDE (NetBeans)示例

当在集群环境中操作时,使用会话范围的 beans 还会增加会话复制的复杂性。在集群环境中,流量通常会在集群中的可用节点之间保持平衡。即使群集试图确保单个用户会话从启动它的节点提供服务,您仍然会遇到这样的情况:一个节点出现故障,流量必须重定向到另一个节点。为了避免用户会话在节点故障期间丢失,必须将会话设置为在节点之间复制。幸运的是,这由应用服务器负责,但是这是在使用会话范围的 beans 时必须解决的另一个问题。

容器管理的安全性

许多应用需要使用用户名和密码或通过客户端证书来保护部分或全部功能。Java EE 规范的创建者定义了一个容器管理的安全框架 ,使得应用开发人员更容易保护他们的应用。或者,应用开发人员将实现一个自定义的安全模型,也称为应用管理的安全性。实现应用管理的安全性需要大量的工作和技能,最终它可能无法提供通过容器管理的安全性无法实现的任何功能或保护。

容器管理的安全性基于一种模型,其中资源(URL)由定义的用户角色保护。登录后,用户被分配到用户角色,剩下的工作由应用服务器负责。应用开发人员唯一需要关心的是定义哪些资源受哪些用户角色的保护。当容器检测到用户未被授权访问所请求的资源时,它会自动将用户定向到登录机制。登录机制可以是基本身份验证、表单身份验证或客户端证书身份验证。基本身份验证将通过浏览器中的本地用户名和密码对话框提示用户输入用户名和密码。表单认证允许应用开发人员提供自己的登录表单,该表单至少必须包含用户名和密码的输入字段。最后,客户端证书身份验证使用 X.509 证书来执行公钥身份验证。安全基础设施的其余部分对应用开发人员来说是完全隐藏的。应用开发人员通过指定安全领域与应用服务器进行通信。安全领域是在应用外部的应用服务器中配置的。安全领域可以指定用户位于 SQL 数据库、LDAP 目录甚至纯文本文件中。应用服务器负责提供安全领域。应用服务器通常为开发人员提供接口来实现他们自己的安全领域,以防您对用户应该如何登录有特殊要求。例如,您可以实现一个安全领域,通过在线服务(如 Google 或 Yahoo)进行身份验证。该实现是应用服务器定制的,但是抽象出了如何处理身份验证。这使得基于容器的安全性非常灵活,并降低了应用开发人员的复杂性。应用开发人员在/WEB-INFO/web.xml 中配置基于容器的安全性。所有符合 JEE 标准的 web 应用服务器都支持容器管理安全性的概念,因此是可移植的。

清单 11-1。 基于容器的安全性,为具有几个受保护资源的简单应用配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" FontName">http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
<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>
    <welcome-file-list>
        <welcome-file>faces/index.xhtml</welcome-file>
    </welcome-file-list>

    <!--
        Security Contraints (protection) for the CUSTOMER role.
    -->
    <security-constraint>
        <display-name>Customer Constraints</display-name>
        <web-resource-collection>
            <web-resource-name>MyAccount</web-resource-name>
            <description>Account Pages</description>
            <url-pattern>/myaccount/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <description/>
            <role-name>CUSTOMER</role-name>
        </auth-constraint>

        <!--
            This section switches the transport from HTTP to HTTPS, thereby
            encrypting the traffic between the browser and server.
        -->
        <user-data-constraint>
            <description>Must switch to HTTPS as the page may contain confidential information.</description>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>

    <!--
        Security Contraints (protection) for the ADMINISTRATOR role.
    -->
    <security-constraint>
        <display-name>Administrator Constrains</display-name>
        <web-resource-collection>
            <web-resource-name>AdministratorSection</web-resource-name>
            <description>Administrator pages</description>
            <url-pattern>/admin/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <description/>
            <role-name>ADMINISTRATOR</role-name>
        </auth-constraint>
    </security-constraint>
    <!--
        Specify which Login Mechanism and Security Realm to use. The details of
        the Realm itself is configured on the application server (outside the
        application).
    -->
    <login-config>
        <auth-method>BASIC</auth-method>
        <realm-name>CRMRealm</realm-name>
    </login-config>
    <!--
        Definition of the Security Roles used in the application
    -->
    <security-role>
        <description>A customer accessing the application</description>
        <role-name>CUSTOMER</role-name>
    </security-role>
    <security-role>
        <description>An administrator of the application</description>
        <role-name>ADMINISTRATOR</role-name>
    </security-role>
</web-app>

国家储蓄

正如我们在第五章中提到的,JSF 2.2 引入了无状态视图的概念。你可以通过指定一个视图应该是瞬态的来使一个视图无状态,如清单 5-23 所示。这提高了应用的性能,因为它不必存储请求之间的视图状态。这显然不适用于所有视图,因为我们必须为一些视图保留状态,但是您应该仔细考虑是否您的每个视图都需要保留状态。

在您确实需要状态保存的情况下,您应该在服务器端而不是客户端启用状态保存。当视图状态保存在客户端时,它被序列化为一个字符串,并存储在一个名为 javax.faces.ViewState 的隐藏输入字段中。首先,每次处理视图时,序列化和反序列化视图状态都会产生开销。其次,您将使用更多的带宽在浏览器和服务器之间来回发送状态。使用客户端状态保存的好处是可以最小化应用的内存占用。您应该仔细考虑什么对您的应用最重要。清单 11-2 显示了用于在/WEB-INF/web.xml 中配置状态保存的上下文参数

清单 11-2。 启用服务器端视图状态保存示例

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" FontName">http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
    ...
    <context-param>
        <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
        <!-- Replace server with client below to enable client-side state saving -->
        <param-value>server</param-value>
    </context-param>
    ...
</web-app>

上下文和依赖注入(CDI)

当您开发 JSF 2.x 应用时,您可以选择使用内置的托管 bean 作用域(@RequestScoped、@SessionScoped 和@ViewScoped)或使用 JSR 299 中定义的上下文和依赖注入(CDI)服务 。CDI 提供了一个架构,其中所有的 Java EE 组件(Servlets、Enterprise JavaBeans、managed beans)都遵循相同的编程模型和生命周期,并具有定义良好的作用域。它允许 Java EE 组件松散耦合,并在需要的地方注入。CDI 已经被证明是如此成功,以至于内置的托管作用域将在 JSF 的未来版本中被弃用。如果你开始开发一个新的应用,你应该从一开始就使用 CDI 服务。如果您正在处理一个必须继续维护的现有应用,那么您应该开始计划从内置受管 bean 作用域到 CDI 作用域的迁移。

在 JSF 启用 CDI 很简单。创建/WEB-INF/beans.xml,指定 CDI beans 应该如何被发现,如清单 11-3 所示。一旦创建了文件,就可以开始在应用中使用 CDI 作用域。

清单 11-3。 /WEB-INF/beans.xml 在您的 JSF 应用中启用 CDI

<?xml version="1.0" encoding="UTF-8"?>
<beans FontName">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/beans_1_1.xsd"
       bean-discovery-mode="annotated">
</beans>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 警告不要试图混合搭配 CDI 作用域和内置的 JSF 管理的 bean 作用域。当你的 JSF bean 因为活跃在不同的作用域而开始行为不端时,这将会导致混乱。

Ajax 化 JSF 应用

在 JSF 2.0 之前,你必须实现自己对 Ajax 的支持,或者使用第三方库,比如 RichFaces。从 JSF 2.0 开始,Ajax 就已经被使用< f:ajax >标签或 JavaScript API 支持了。

使用 <标签

是在 UIComponents 上注册 Ajax 行为的标签。标签可以作为一个子元素嵌套在 UIComponent 中,或者如果相同的 Ajax 行为应该应用于所有 ui component,它可以包含多个 ui component。

标签为配置 Ajax 行为提供了一个小而强大的属性选择。表 11-1 概述了可用的属性。

表 11-1 。< f:ajax/ >标签的属性

|

属性

|

描述

|
| — | — |
| disabled | 确定是否不应该呈现 Ajax 行为。默认值为 false。 |
| delay | Ajax 请求延迟的毫秒数。如果在延迟期间有多个请求进入,那么只有最后一个请求会被执行。 |
| event | 指定 Ajax 行为应该响应的 DOM 事件的字符串。请务必注意,它只是事件名称(例如,“click”而不是“onclick”)。必须使用标记从 UIComponent 发出该事件;否则 Ajax 行为永远不会被触发。 |
| execute | 当 Ajax 行为被触发时应该执行的以空格分隔的组件列表。 |
| immediate | 确定 Ajax 行为是在应用请求值阶段(true)还是在调用应用阶段(false)被触发。默认值为 false。 |
| listener | 对应该处理由 Ajax 行为触发的 AjaxBehaviorEvent 的侦听器的引用。 |
| onevent | JavaScript 函数的名称,该函数应处理执行 Ajax 请求时发出的事件。 |
| onerror | JavaScript 函数的名称,该函数应处理执行 Ajax 请求时发出的错误。 |
| render | Ajax 请求成功时应该(重新)呈现的以空格分隔的组件列表。 |

清单 11-4 展示了一个使用< f:ajax >标签根据输入文本组件中输入的内容更新输出面板的例子。Ajax 行为与 keyup DOM 事件挂钩(键入一个键)。当接收到事件时,将执行输入文本组件,输入的值存储在 ajaxDemo 托管 bean 的 outputMessage 属性中。当请求返回成功时,将重新呈现输出消息面板,并在面板中显示输入文本中输入的消息。该示例还通过将所有 Ajax 事件传递给一个名为 processInput 的 JavaScript 函数来演示 onevent 属性的用途,该函数在请求开始和成功完成时切换 Ajax 微调器的可见性。

清单 11-4。 使用< f:ajax/ >标签在更新文本字段时注册 ajax 行为的例子

<h:form id="my-message">
    <h:outputLabel value="Your message" for="input-message" />

    <h:inputText id="input-message" value="#{ajaxDemo.outputMessage}">
        <f:ajax event="keyup"
                onevent="function(data) { processInput(data, 'my-message:busy'); }"
                render="output-message" execute="@this" />
    </h:inputText>

    <h:graphicImage id="busy" library="images" name="spinner.gif" style="display: none; float: left;" />

    <h:panelGroup id="output-message">#{ajaxDemo.outputMessage}</h:panelGroup>

</h:form>

<script type="text/javascript">
// Handle for onevent
function processInput(data, id) {
    if (data.status === 'begin') {
        toggle_visibility(id);
    } else if (data.status === 'success') {
        toggle_visibility(id);
    }
}

// Utility function for toggling the visibility of an element
function toggle_visibility(id) {
    var e = document.getElementById(id);
    if (e.style.display == 'block')
        e.style.display = 'none';
    else
        e.style.display = 'block';
}
</script>

您会注意到我们在 execute 属性中使用了一个特殊的值。execute 和 render 属性都支持一些特殊的值。这些特殊值是为了方便起见,这样您就不必为通常受 Ajax 行为影响的组件输入特定的组件标识符。这些值在表 11-2 中列出。

表 11-2 。执行/呈现特殊值

|

关键字

|

描述

|
| — | — |
| @all | 执行或渲染所有组件 |
| @none | 不要执行或呈现任何组件 |
| @this | 执行或呈现触发 Ajax 行为的组件 |
| @form | 以触发 Ajax 行为的组件的形式执行或呈现所有组件 |

使用 JavaScript API

JSF 附带了一个 JavaScript API ,可以一起使用,也可以代替< f:ajax >标签。JavaScript API 在 jsf 名称空间下的所有页面上都是可用的。像< f:ajax >一样,JavaScript API 可以用来发起 ajax 请求。JavaScript API 也可以用于监控 Ajax 请求和处理错误。

使用 JavaScript API 发起 Ajax 请求的方法签名如清单 11-5 所示,表 11-3 解释了该方法的输入参数。

清单 11-5。 使用 JavaScript 发起 Ajax 请求的方法签名

jsf.ajax.request(source, event, {options});

表 11-3 。jsf.ajax.request 的输入参数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

清单 11-6 展示了一个使用 JavaScript API 执行更新面板网格脚本的按钮示例。

清单 11-6。 使用 JavaScript API 更新面板网格

<h:panelGroup id="clicks" layout="block">
    <h:outputLink id="refresh" onclick="refreshClicks(this, event); return false;">
        Refresh:
    </h:outputLink>
    <h:outputText value="#{javaScriptApiDemo.clicks}" />
</h:panelGroup>

<script type="text/javascript">
    function refreshClicks(source, event) {
        jsf.ajax.request(source, event, {render: 'clicks'});
    }
</script>

在清单 11-6 中,我们看到 JavaScript refreshClicks 函数在刷新输出链接的 onclick 事件中被调用。它将自己作为源和 onclick 生成的事件传递给刷新函数。它以返回 false 结束,这样单击链接就不会调用完整的 HTTP 请求。所有神奇的事情都发生在 refreshClicks 函数中。这里使用传递给函数的源和事件来触发 Ajax 请求。Ajax 请求有一个单独的选项,声明从 Ajax 请求返回时,应该呈现带有 ID clicks 的元素。ID 为 clicks 的元素是一个面板组,包含链接和从名为 javaScriptApiDemo 的受管 bean 中检索的值。如果受管 bean 中的点击值增加了,refreshClicks 函数将更新显示并显示当前的点击次数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示在 JSF 应用中修改 Ajax 请求时,你最终会得到一个 httpError,说明“Http 传输返回一个 0 状态码”。这通常是混合 ajax 和完整请求的结果。出于性能和数据完整性的原因,这通常是不希望的”(见图 11-2 )。这条警告消息可能看起来很隐晦,但它只是说,您试图在执行完整的 HTTP 请求时执行 Ajax 请求。如果您忘记在调用 Ajax 请求的 onclick 事件的末尾包含 return false,就会发生这种情况

错: 对:<h:output link onclick = " do-some-Ajax();返回 false"/ >

您还可以通过使用清单 11-7 中所示的执行选项,在选定的组件上执行请求生命周期。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-2 。同时执行 Ajax 和完整 HTTP 请求时显示错误

清单 11-7。 使用执行选项在选定的组件上执行 JSF 请求生命周期

<h:form id="my-name-form">
    <h:outputLink onclick="saveName(this, event); return false;">Save name</h:outputLink>
    <h:inputText id="my-name" value="#{javaScriptApiDemo.myName}" />
    <h:panelGroup id="my-name-display">Your name is: #{javaScriptApiDemo.myName}</h:panelGroup>
</h:form>

<script type="text/javascript">
    function saveName(source, event) {
        jsf.ajax.request(source, event, {
            execute: '@form',
            render: 'my-name-form:my-name-display'
        });
    }
</script>

清单 11-7 中的例子显示了一个输入框,用户可以在其中输入自己的名字。该名称被映射到 javaScriptApiDemo 受管 bean 上一个名为 myName 的属性。当前存储在 myName 属性中的名称显示在面板组中输入字段的下方。当点击“保存名称”链接时,将调用保存名称功能。saveName 函数中的 Ajax 请求将选项 execute 设置为@form,表示发出请求的表单应该执行 JSF 请求生命周期。从请求返回后,my-name-form 表单中的 my-name-display 面板被更新。值得注意的是,清单 11-7 完全等同于清单 11-8 。使用< f:ajax >标签和 JavaScript API 之间的选择取决于您试图解决的特定任务。如果同时控制页面的其他方面,将所有功能收集到逻辑分组的 JavaScript 函数中,那么使用 JavaScript API 可能更有意义。如果页面没有任何其他正在执行的 JavaScript,那么使用 JavaScript API 而不是坚持使用< f:ajax >标签可能是多余的。

清单 11-8。 与清单 11-x 相同的例子,但是使用了< f:ajax >标签来代替 JavaScript API

<h:form id="my-name-form-pure-jsf">
    <h:outputLink>
        <f:ajax render="my-name-display-pure-jsf" execute="my-name-pure-jsf" />
        Save name
    </h:outputLink>
    <h:inputText id="my-name-pure-jsf" value="#{javaScriptApiDemo.myName}" />
    <h:panelGroup id="my-name-display-pure-jsf">
        Your name is: #{javaScriptApiDemo.myName}
    </h:panelGroup>
</h:form>

监控 Ajax 事件

使用标签不可能做到的一件事是监控客户端上执行的所有 Ajax 请求,以及对请求中可能出现的问题的一般错误处理。JavaScript API 提供了两个事件监听器。一个是 Ajax 请求事件的事件监听器(addOnEvent),另一个是服务器错误通知(addOnError)。Ajax 请求事件监听器发出三种事件,如表 11-4 所示。服务器错误事件监听器发出四种事件,如表 11-5 所示。

表 11-4 。从 Ajax 请求事件侦听器发出的事件

|

事件

|

描述

|
| — | — |
| begin | 每当 Ajax 请求开始时发出 |
| complete | 每当 Ajax 请求完成时发出 |
| success | 每当 Ajax 请求成功完成时发出 |

表 11-5 。从服务器错误事件侦听器发出的事件

|

事件

|

描述

|
| — | — |
| httpError | 如果 HTTP 状态不在 2xx 成功范围内,则发出此事件 |
| serverError | 当服务器端发生错误或异常时发出 |
| malformedXML | 当服务器返回不正确的 XML 响应时发出 |
| emptyResponse | 当服务器没有返回响应时发出 |

清单 11-9 是一个挂钩到两个事件处理程序的 JavaScript 的例子。通过使用< h:outputScript/ >标签,JavaScript 可以被任何 Facelets 页面使用,如清单 11-10 所示。

清单 11-9。 JavaScript 挂钩到 JSF JavaScript API 公开的事件监听器

function outputAjaxEvent(data) {
    console.log(data);
}

function outputError(errorData) {
    console.log(errorData.type + " (" + errorData.status + "): " + errorName + ". " + errorDescription);
    // Register error on a remote error logging server
}

function showProgress(data) {
    if (data.status === 'begin') {
        toggle_visibility('in-progress');
    } else if (data.status === 'success') {
        toggle_visibility('in-progress');
    }
}

// Utility function for toggling the visibility of an element
function toggle_visibility(id) {
    var e = document.getElementById(id);
        if (e.style.display == 'block')
            e.style.display = 'none';
        else
            e.style.display = 'block';
}

jsf.ajax.addOnEvent(outputAjaxEvent);
jsf.ajax.addOnEvent(showProgress);
jsf.ajax.addOnError(outputError);

清单 11-10。 使用 JavaScript 文件的 Facelets 页面

<?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 FontName">http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      xmlns:f="http://xmlns.jcp.org/jsf/core">

    <ui:composition template="/base.xhtml">

        <ui:define name="title">
            Chapter 11 - JavaScript API Demo
        </ui:define>

        <ui:define name="top">
            Chapter 11 - JavaScript API Demo
        </ui:define>

        <ui:define name="content">

            <h:outputScript name="events.js" library="js" />

            <h:form id="my-name-form-pure-jsf">
                <h:outputLink>
                    <f:ajax render="my-name-display-pure-jsf" execute="my-name-pure-jsf"  />
                    Save name
                </h:outputLink>
                <h:inputText id="my-name-pure-jsf" value="#{javaScriptApiDemo.myName}" />
                <h:panelGroup id="my-name-display-pure-jsf">
                    Your name is: #{javaScriptApiDemo.myName}
                </h:panelGroup>
            </h:form>

            <!—
                 Hidden by default. This is only shown when an Ajax
                 request begins and hidden when a request completes successfully
            -->
            <h:panelGroup id="in-progress" layout="block" style="display: none;">
                <h:panelGroup style="font-weight: bold;">
                    PLEASE WAIT - THE PAGE IS LOADING
                </h:panelGroup>
            </h:panelGroup>

        </ui:define>
    </ui:composition>

</html>

测试 JSF 应用

任何真实世界的应用都必须经过一定程度的测试。开发 web 应用时,编写单元测试来验证功能背后的单个类通常是不够的。您可以通过使用 JUnit 测试组件和 beans 背后的逻辑,但是当您有一个 JSF 应用时,这通常是不够的。测试 JSF 应用有很多方面,不仅仅是确保后端逻辑正确。在 JSF 应用中,HTTP(客户端-服务器交互)、Ajax 请求和 web 浏览器差异增加了复杂性。因此,真正的测试需要一个框架,能够测试应用的部署版本,发起请求(Ajax 和完整的 HTTP)并在请求后测试应用的状态。有许多流行的功能测试框架可用,如 Selenium、FitNesse 和 Cucumber。这些测试框架可以通过验证应用在用户端的行为是否符合预期来帮助我们进行黑盒测试。理想情况下,我们希望有一个集成测试框架,允许我们验证应用及其组件的行为,就像它们在应用服务器上部署时的行为一样。这将允许更精确的测试。

什么是 Arquillian?

Arquillian 是一个完整的 Java EE 应用容器内测试平台。Arquillian 与测试框架(如 JUnit 和 TestNG)集成在一起,使得编写单元测试的任何人都可以轻松采用。Arquillian 使用您选择的嵌入式应用服务器来部署要测试的类和资源。一旦在嵌入式应用服务器中部署了 JSF 应用,就可以通过调用类和资源并检查它们的响应来测试系统的行为。现成的 Arquillian 能够测试 JEE 组件,如 EJB 和 CDI beans。已经为 Arquillian 构建了几个扩展来支持功能测试。表 11-6 概述了 Arquillian 的流行扩展。

表 11-6 。Arquillian 的流行扩展

|

延长

|

目的

|

成熟度

|
| — | — | — |
| 雄蜂 | Selenium 也使用 WebDriver API 的包装器。这个扩展使得创建简单的功能测试成为可能。
链接: http://arquillian.org/modules/drone-extension/ | 稳定的 |
| 弯曲 | 模拟客户端的交互,同时在 JSF 请求生命周期的不同阶段检查服务器端的状态变化。
链接: http://arquillian.org/modules/warp-extension/ | 希腊字母的第一个字母 |
| 石墨烯 | 通过保护和拦截请求来优雅地支持 Ajax,从而增强了 Drone 扩展。
链接: http://arquillian.org/modules/graphene-extension/ | 稳定的 |
| 坚持 | 验证应用的持久层。允许使用 XML、XLS、YAML、JSON 和 SQL 等常见数据格式植入数据库。
链接: http://arquillian.org/modules/persistence-extension/ | 希腊字母的第一个字母 |
| 表演 | 验证测试在给定的时间范围内执行。将在回归测试中发现性能问题。
链接: http://arquillian.org/modules/performance-extension/ | 贝塔 |
| 蒸汽 2 | 允许测试 Seam 库和注入点。
链接: http://arquillian.org/modules/seam2-extension/ | 贝塔 |

在这一节中,我们将探索如何使用 Arquillian 和 Drone 扩展对 JSF 应用进行黑盒测试。首先,我们将看看如何为 Maven 项目设置 Arquillian 和 Drone ,然后看看如何使用 Drone 扩展编写合理的 JUnit 测试。

注意 Arquillian Warp Extension 是 JSFUnit 项目的官方替代品,不再被维护。在撰写本文时,Warp 扩展仍处于 Alpha 状态,没有可用的产品示例。

设置阿奎利亚人和无人机

在这一节中,我们将看看如何在 Maven 项目中设置 Arquillian 和 Drone 。如果你不使用 Maven,你可以在 Arquillian 网站(【http://www.arquillian.org】??)上找到在你的项目中包含必要依赖项的指南。

清单 11-11 显示了 Maven 项目对象模型(POM) ,用于在您的项目中包含 Arquillian 和 Drone。

清单 11-11。 pom.xml 包含使用 Arquillian 和 Drone 所需的依赖项

<?xml version="1.0" encoding="UTF-8"?>
<project FontName">http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.apress.projsf2html5</groupId>
    <artifactId>chapter11</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>chapter11</name>

    ...

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jboss.arquillian</groupId>
                <artifactId>arquillian-bom</artifactId>
                <version>1.1.1.Final</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
            <dependency>
                <groupId>org.jboss.arquillian.extension</groupId>
                <artifactId>arquillian-drone-bom</artifactId>
                <version>1.2.0.CR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.glassfish.main.extras</groupId>
            <artifactId>glassfish-embedded-all</artifactId>
            <version>4.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.arquillian.junit</groupId>
            <artifactId>arquillian-junit-container</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.arquillian.container</groupId>
            <artifactId>arquillian-glassfish-embedded-3.1</artifactId>
            <version>1.0.0.CR4</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.arquillian.extension</groupId>
            <artifactId>arquillian-drone-impl</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.shrinkwrap.descriptors</groupId>
            <artifactId>shrinkwrap-descriptors-api-javaee</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.shrinkwrap.descriptors</groupId>
            <artifactId>shrinkwrap-descriptors-impl-javaee</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.shrinkwrap</groupId>
            <artifactId>shrinkwrap-api</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.arquillian.extension</groupId>
            <artifactId>arquillian-drone-webdriver-depchain</artifactId>
            <type>pom</type>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.arquillian.extension</groupId>
            <artifactId>arquillian-drone-selenium</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.arquillian.extension</groupId>
            <artifactId>arquillian-drone-selenium-server</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-server</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.mortbay.jetty</groupId>
                    <artifactId>servlet-api-2.5</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.6.4</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>

    ...

</project>

使用 Arquillian 和 Drone 编写测试

我们将使用 Arquillian 来模拟 web 应用到 web 容器的部署。一旦 web 应用被部署在嵌入式容器中,我们将开始使用无人机扩展运行功能测试,方法是对部署的应用执行请求,并验证它是否以预期的输出进行响应。作为一个例子,我们将测试一个简单的应用,它要求用户输入他的名字,然后问候用户。用户界面的实体模型如图图 11-3 所示。基于实体模型,我们需要一个 Facelets 文件向用户显示 UI 和一个 CDI bean 来存储名称,如图图 11-4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-3 。测试应用的 UI 模型

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-4 。包含用于测试应用的 CDI Bean 和 Facelets 页面的类图

基于实体模型和类图,我们可以使用 Gherkin 格式编写几个伪测试,稍后我们将使用 Drone 实现这些测试。

| **场景:**输入我的名字 |
| 假设我在页面上输入我的名字“离合器电源” |
| 当我按下提交按钮时 |
| 然后应用会向我问候“你好,离合器电源” |

| **场景:**我进入页面 |
| 假定我进入页面 |
| 当我无所事事时 |
| 然后将不会显示任何问候语 |

CDI bean 的实际实现可以在清单 11-12 中看到。它是一个简单的请求范围 bean,只有一个名为 name 的属性。清单 11-13 显示了 Facelets 文件,它向用户显示输入字段,包括一个提交按钮。注意,我们有一个面板组,仅当 CDI bean 的 name 属性不为空时才显示。

清单 11-12。 简单请求作用域 CDI Bean 公开一个名称属性

package com.apress.projsf2html5.chapter11.jsf;

import java.io.Serializable;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;

@Named(value = "helloYou")
@RequestScoped
public class HelloYou implements Serializable {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

清单 11-13。 Facelets 文件向用户显示输入框和提交按钮

<?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 FontName">http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets">

    <ui:composition template="/base.xhtml">

        <ui:define name="title">
            Chapter 11 - Testing - Hello You
        </ui:define>

        <ui:define name="top">
            Chapter 11 - Testing - Hello You
        </ui:define>

        <ui:define name="content">

            <h:form id="hello-form">
                <h:outputLabel value="What's your name?" for="input-name" />
                <h:inputText id="input-name" value="#{helloYou.name}" />
                <h:commandButton id="submit" value="Submit" />

                <h:panelGroup id="output-message" rendered="#{not empty helloYou.name}">
                    Hello #{helloYou.name}
                </h:panelGroup>

            </h:form>

        </ui:define>
    </ui:composition>
</html>

CDI bean 和 Facelet 页面的结果可以在图 11-5 中看到。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-5 。你好应用

我们现在准备测试应用。我们的测试将用@RunWith 注释,告诉 JUnit 我们将使用 Arquillian 作为我们的测试运行程序。我们还使用@RunAsClient 注释告诉 Arquillian 我们不会在服务器端进行测试,而是作为 web 客户端发送请求。这是使用无人机扩展进行功能测试所必需的。接下来,我们将指定我们想要测试的资源和类。这是通过使用包膜 API 创建 WebArchive 对象来完成的。WebArchive 应该包含所有的资源和类,并且只包含那些。目的是隔离被测试的文件,避免增加不必要的复杂性。创建 WebArchive 的方法必须被注释为@Deployment,以便 Arquillian 在执行测试之前检测必须部署的内容。最后,我们将编写实际的测试。完整的测试可以在 清单 11-14 中看到。

清单 11-14。 测试 Hello You 应用的 Arquillian 测试用例

package com.apress.projsf2html5.chapter11;

import com.apress.projsf2html5.chapter11.jsf.HelloYou;
import com.thoughtworks.selenium.DefaultSelenium;
import java.io.File;
import java.net.URL;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;

@RunWith(Arquillian.class)
@RunAsClient
public class HelloYouTest {

    /** This will give us the contextPath where the web application was installed. */
    @ArquillianResource
    URL contextPath;

    /** This will give us access to a Drone that simulates a browser. */
    @Drone
    private DefaultSelenium browser;

    /**
     * The method annotated with Deployment outputs the web archive representing
     * the application.The archive must contain all the resource and classes
     * being tested.
     *
     * @return {@link WebArchive} containing the resources and classes
     * representing the Hello You application
     */
    @Deployment
    public static WebArchive createDeployment() {
        return ShrinkWrap.create(WebArchive.class, "hello-you.war")
                .addClasses(HelloYou.class)
                .addAsWebResource(new File("src/main/webapp/hello-you.xhtml"))
                .addAsWebResource(new File("src/main/webapp/contracts/basic/base.xhtml"), "contracts/basic/base.xhtml")
                .addAsWebResource(new File("src/main/webapp/contracts/basic/cssLayout.css"), "contracts/basic/cssLayout.css")
                .addAsWebResource(new File("src/main/webapp/contracts/basic/default.css"), "contracts/basic/default.css")
                .addAsWebInfResource(new File("src/main/webapp/WEB-INF/web.xml"))
                .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
    }

    /**
     * Scenario: Entering my name.
     * Given that I enter my name 'Clutch Powers' on the page
     * When I press the Submit Button
     * Then I will be greeted 'Hello Clutch Powers' by the application
     */
    @Test
    public void helloyou_EnterName_GreetingFound() {
        String startUrl = contextPath.toString() + "faces/hello-you.xhtml";

        // Open the hello-you page
        browser.open(startUrl);

        // Type name in the input field
        browser.type("id=hello-form:input-name", "Clutch Powers");

        // Click the submit button
        browser.click("id=hello-form:submit");

        // Wait for the page to load (max 5 seconds)
        browser.waitForPageToLoad("5000");

        // Check that the "Hello <name>" element is displayed on screen
        assertTrue(browser.isVisible("id=hello-form:output-message"));

        // Check that the name entered is the one expected
        assertEquals("Welcome message missing",
                browser.getText("id=hello-form:output-message"),
                "Hello Clutch Powers");
    }
    /**
     * Scenario: I enter the page.
     * Given that I enter page
     * When I do nothing
     * Then I there will be no greeting displayed
     */
    @Test
    public void helloyou_OpenPage_GreetingHidden() {
        String startUrl = contextPath.toString() + "faces/hello-you.xhtml";

        // Open the hello-you page
        browser.open(startUrl);

        // Check that the "Hello <name>" element is NOT displayed on the screen
        assertFalse(browser.isVisible("id=hello-form:output-message"));
    }
}

DefaultSelenium 无人机能够模拟多种浏览器交互。有趣的方法示例在 表 11-7 中突出显示。

表 11-7 。用于模拟用户交互的 DefaultSelenium 方法示例

|

方法

|

描述

|
| — | — |
| attachFile(fieldLocator, fileLocation) | 用于在文件输入表单字段中附加文件 |
| Click(locator) | 单击具有指定定位器的给定元素 |
| doubleClick(locator) | 双击具有指定定位器的给定元素 |
| dragAndDrop(locator, movements) | 模拟从源到位置的拖放 |
| getText(locator) | 获取给定元素中的文本 |
| isVisible(locator) | 已确定给定元素在屏幕上是否可见 |
| Open(url) | 打开给定的页面 |
| typeKeys(location, value) | 在给定的输入字段中键入值 |
| waitForPageToLoad(timeout) | 等待页面加载给定的毫秒数 |

摘要

在本章中,我们探讨了开发 JSF 应用的设计考虑因素,如安全性、性能和内存消耗。我们还看了如何使用标签来 Ajax 化 JSF 应用。使用标签可以在单个以及一组 JSF 组件上设置 Ajax 请求。为了补充标签,我们来到幕后探索 JSF JavaScript API。最后,我们看了使用 Aqullian 测试框架结合无人机扩展来测试 JSF 应用,无人机扩展支持 JSF 应用的功能测试。

十二、JSF2 安全性和性能

在本章中,您将学习如何使用 Java EE 容器提供的安全特性来保护您的 JSF 应用。您知道如何在第十章中介绍的天气应用中应用容器管理的认证、授权和数据保护。在这一章中,你还将学习如何调整你的 JSF 应用的性能,以使你的 JSF 页面响应更快。

JSF 应用安全

Web 应用安全 可以分为三个主要方面,我们将在本节详细阐述:

  • 认证 是向系统确认用户身份真实的行为。
  • 授权 定义了用户在执行身份验证后被允许访问系统的哪些部分。
  • 数据保护 是关于确保用户和系统之间的数据不能被未授权方修改或伪造。

在 Java EE 中,您可以依赖 Java EE 容器提供的安全特性,以便在您的 Java EE 应用中实现安全需求(如果您的 Java EE 应用依赖于 Java EE 容器提供的安全特性;这意味着您的 Java EE 应用正在使用“容器管理的安全性”)。除了在容器级别管理安全性,您还可以在应用级别管理安全性(这种方法称为应用管理的安全性)。应用管理的安全性并不意味着从头开始实现所有的应用安全特性;应用管理的安全性通常利用 Java EE 容器提供的安全特性,以便在应用中实现客户需要的自定义安全特性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意如果从客户需求来看,没有理由实现定制的安全解决方案,那么强烈建议 Java EE 应用使用容器管理的安全性。

Java EE 容器管理的安全性提供了容器管理的身份验证、授权和数据保护。在接下来的小节中,我们将详细解释这些术语。

证明

Java EE 容器提供了不同类型的认证机制:

  • HTTP 基本
  • 基于表单
  • 摘要

HTTP 基本身份验证

在 HTTP 基本身份验证中,服务器从 web 客户端请求用户名和密码,并通过与指定或默认领域中授权用户的数据库进行比较来验证用户名和密码是否有效。当您没有在 web 配置文件中指定身份验证机制时,基本身份验证是默认的。

使用基本身份验证时,会发生以下步骤:

  • 客户端请求访问受保护的资源。
  • web 服务器返回一个对话框,要求输入用户名和密码。
  • 客户端向服务器提交用户名和密码。
  • 服务器对指定领域中的用户进行身份验证,如果成功,则返回请求的资源。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意领域是系统的用户和组的存储。

基于表单的身份验证

在基于表单的身份验证中,您可以在应用中开发和自定义登录和错误页面。当在 web 配置文件中声明基于表单的身份验证时,会发生以下步骤:

  • 客户端请求访问受保护的资源。
  • 如果客户端未经身份验证,服务器会将客户端重定向到登录页面。
  • 客户端将登录表单提交给服务器。
  • 服务器尝试对用户进行身份验证。
  • 如果身份验证成功,将检查通过身份验证的用户的主体,以确保其角色有权访问资源(授权)。如果用户得到授权,服务器将使用存储的 URL 路径将客户端重定向到资源。
  • 如果身份验证失败,客户端将被转发或重定向到错误页面。

有关基于表单的身份验证的完整示例,请查看“在天气应用中应用托管安全性”一节

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意需要注意的是,HTTP 基本认证以 Base64 编码的文本形式发送用户名和密码;而基于表单的身份验证是以纯文本的形式发送它们,这意味着它们是不安全的,所以建议使用安全传输机制(如 SSL)。为了配置 SSL,您需要查看应用服务器的文档,因为它是针对每个应用服务器的;例如,为了在 Tomcat 7 中配置 SSL,请检查以下链接:tomcat.apache.org/tomcat-7.0-doc/ssl-howto.html

摘要认证

摘要认证基于用户名和密码认证用户(类似于基本的 HTTP 认证)。但是,与基本身份验证不同,摘要式身份验证不通过网络发送用户密码。相反,客户端发送密码的单向加密哈希。

配置身份验证方法

为了在您的 Java EE web 应用中配置认证方法,您可以使用< login-config >元素,如 web 配置文件(web.xml)中所示:

<login-config>
    <auth-method>FORM</auth-method>
    <realm-name>jdbcRealm</realm-name>
    <form-login-config>
        <form-login-page>/home.xhtml</form-login-page>
        <form-error-page>/error.xhtml</form-error-page>
    </form-login-config>
</login-config>

您可能已经注意到,元素有以下子元素:

  • 元素指定了 web 应用的认证机制。它可以是消化的,基本的或形式的或没有。
  • 元素指定领域名。
  • 元素指定了登录和错误页面。当使用基于表单的登录时,应该使用它。

批准

授权定义了基于角色的访问控制,它决定了用户可以访问系统的哪些部分。在 Java EE 中,为了实现这一点,可以在 web.xml 中使用<安全约束>元素,如清单 12-1 所示。

清单 12-1。 范例<范例>范例

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" FontName">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-app_3_1.xsd">

    ...

    <security-constraint>
        <display-name>securityConstraint</display-name>
        <web-resource-collection>
            <web-resource-name>resources</web-resource-name>
            <url-pattern>/protected/*</url-pattern>
            <http-method>PUT</http-method>
            <http-method>DELETE</http-method>
            <http-method>GET</http-method>
            <http-method>POST</http-method>
        </web-resource-collection>
        <auth-constraint>
            <role-name>weatherUserRole</role-name>
        </auth-constraint>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>
    <login-config>
        <auth-method>FORM</auth-method>
        <realm-name>WeatherRealm</realm-name>
        <form-login-config>
            <form-login-page>/home.xhtml</form-login-page>
            <form-error-page>/error.xhtml</form-error-page>
        </form-login-config>
    </login-config>
    ...

</web-app>

元素用于使用资源的 URL 映射来定义对资源集合的访问权限。它可以包含以下元素:

  • Web 资源集合():描述一组要保护的资源的 URL 模式和 HTTP 操作的列表。
  • 授权约束():指定是否使用身份验证,并指定被授权执行受约束请求的角色。
  • 用户数据约束():指定在客户端和服务器之间传输数据时如何保护数据(将在“数据保护”一节中说明)。

一个 web 资源集合()包含以下元素:

  • (可选)是您用于 web 资源的名称。
  • 是要保护的 URL。
  • 用于指定哪些方法应该被保护。

授权约束 ( <授权约束>)包含<角色名称>元素。您可以根据需要在< auth-constraint >元素中使用任意数量的< role-name >元素。为应用定义的角色必须映射到应用服务器上定义的用户和组(每个应用服务器都有自己的方式向用户和组映射声明此角色;查看“在天气应用中应用托管安全性”一节,了解如何在 GlassFish application server 版上实现这一点。

数据保护

数据保护是指保护在客户端和服务器之间传输的数据。在 Java EE 中,为了进行数据保护,可以使用 web.xml 中的元素,如清单 12-1 所示,并在清单 12-2 中突出显示。

清单 12-2。web.xml 中<安全约束>元素的 <用户数据约束>

<security-constraint>
        <display-name>securityConstraint</display-name>
        <web-resource-collection>
                <web-resource-name>resources</web-resource-name>
                <url-pattern>/protected/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
                <role-name>weatherUserRole</role-name>
        </auth-constraint>
        <user-data-constraint>
                <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
</security-constraint>

如粗体行所示,元素包含元素。元素指定了客户端和服务器之间的通信,它可以有以下值之一:无、整数、机密。INTEGRAL 表示应用要求数据(在客户端和服务器之间发送)的发送方式不能被第三方恶意更改,而 CONFIDENTIAL 表示应用要求防止其他恶意第三方观察传输内容。INTEGRAL 和 CONFIDENTIAL 都意味着 SSL。

在天气应用中应用托管安全性

在第十章中,我们介绍了天气应用,作为基本 JSF 2.2 应用的一个例子。在天气应用中,我们从应用代码中处理认证(应用登录)和授权(访问天气页面)。不建议从应用代码中处理安全性,尤其是当我们讨论没有自定义安全性需求的典型身份验证和授权场景时;因此,让我们将容器管理的安全性(基于表单的身份验证和授权)应用于天气应用。

首先,让我们修改 home.xhtml 以包含基于表单的身份验证的 html 形式,而不是从应用代码中处理登录要求。清单 12-3 显示了 home.xhtml 的更新。

***清单 12-3。***更新至 home.xhtml 页面

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
          xmlns:ui="http://java.sun.com/jsf/facelets"
          xmlns:h="http://xmlns.jcp.org/jsf/html">

<ui:composition template="/WEB-INF/templates/main.xhtml">
    <ui:define name="title">
        #{bundle['application.loginpage.title']}
    </ui:define>
    <ui:define name="content">

        <!-- Form authentication -->
        <form action="j_security_check" method="POST">
           Username:<input type="text" name="j_username"></input><br/>
           Password:<input type="password" name="j_password"></input><br/>
           <input type="submit" value="#{bundle['application.login']}"></input>
        </form>
        <h:link value="#{bundle['application.loginpage.register']}" outcome="registration"/>

    </ui:define>
</ui:composition>

</html>

如粗体行所示,为了使用基于表单的身份验证,按照 servlet 规范,我们必须使用 HTML

标记(而不是标准的 JSF ),将表单操作设置为“j_security_check”,将表单方法设置为“POST”,并将用户名和密码字段的名称设置为“j_username”和“j _ password”;最后,有一个提交按钮来提交表单。清单 12-4 显示了 web.xml 中基于表单的认证配置

清单 12-4。 天气应用的基于表单的认证配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" ...>
   ...
   <security-constraint>
        <display-name>securityConstraint</display-name>
        <web-resource-collection>
            <web-resource-name>resources</web-resource-name>
            <url-pattern>/protected/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>weatherUser</role-name>
        </auth-constraint>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>
    <login-config>
        <auth-method>FORM</auth-method>
        <realm-name>WeatherRealm</realm-name>
        <form-login-config>
            <form-login-page>/home.xhtml</form-login-page>
            <form-error-page>/error.xhtml</form-error-page>
        </form-login-config>
    </login-config>
    <welcome-file-list>
        <welcome-file>protected/weather.xhtml</welcome-file>
    </welcome-file-list>
...
</web-app>

如粗体行所示,在安全约束部分,只有 weatherUser 角色能够访问受保护文件夹(/protected/*)下的资源。在登录配置部分,认证方式设置为 FORM(即基于表单的认证),领域名称设置为 WeatherRealm,最后在表单登录配置中,登录页面设置为 home.xhtml(如清单 12-3 所示),错误页面(用户登录失败时会显示)设置为 error.xhtml,当用户登录成功时,用户会被转发到受保护文件夹下的 weather.xhtml 页面。

为应用定义的 weatherUser 角色必须映射到应用服务器上定义的组。对于 GlassFish,您可以在配置文件(glassfish-web.xml)中定义角色和组之间的映射,如清单 12-5 所示。

清单 12-5。 glassfish-web.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC ...>
<glassfish-web-app error-url="">
  <context-root>/weather</context-root>
  <security-role-mapping>
    <role-name>weatherUser</role-name>
    <group-name>weather_user</group-name>
  </security-role-mapping>
  ...
</glassfish-web-app>

如配置文件所示,角色名(weatherUser)被映射到领域存储库(WeatherRealm)中的实际组名(weather_user)。

WeatherRealm 是天气应用的用户和组的存储;您可能还记得,我们有一个 APP_USER 表,用来存储应用用户。由于 JDBC realm(GlassFish 和其他一些 Java EE 应用服务器支持它),您可以将现有的用户/组数据库变成一个领域;然而,我们需要添加另一个数据库表(APP_GROUP)来定义用户组,如图图 12-1 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-1 。天气应用数据模型的修改

清单 12-6 显示了包含 APP_USER 和 APP_GROUP 属性以及它们之间关系的 SQL 语句。

清单 12-6。 天气应用数据模型的 SQL 语句

CREATE TABLE APP_USER (
    ID VARCHAR(64) PRIMARY KEY,
    FIRST_NAME VARCHAR(32),
    LAST_NAME VARCHAR(32),
    PASSWORD VARCHAR(32),
    PROFESSION VARCHAR(32),
    EMAIL VARCHAR(64),
    ZIP_CODE VARCHAR(32)
);

CREATE TABLE APP_GROUP(userid varchar(64) not null, groupid varchar(64) not null, primary key(userid, groupid));

ALTER TABLE APP_GROUP add constraint FK_USERID foreign key(userid) references APP_USER(id);

最后,为了在 GlassFish 4.0 中创建我们的自定义领域,单击“配置->服务器配置->安全性->领域”,输入合适的领域信息,最后保存领域,如图 12-2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-2 。在 GlassFish 版中定义新领域

表 12-1 显示了 WeatherRealm 的配置属性。

表 12-1 。自定义领域配置属性

|

财产

|

价值

|
| — | — |
| JAAS 背景 | jdbcrealm-JDBC 范围 |
| 命名服务 | jdbc/weatherDB |
| 用户表 | 天气。应用 _ 用户 |
| 用户名列 | 身份 |
| 密码栏 | 密码 |
| 组表 | 天气。APP_GROUP |
| 组表用户名列 | 使用者辩证码 |
| 组名列 | 组名 |
| 密码加密算法 | 没有人 |
| 分配组 | [将此字段留空] |
| 数据库用户 | 天气 |
| 数据库密码 | 密码 |
| 摘要算法 | 没有人 |
| 编码 | [将此字段留空] |
| 字符集 | [将此字段留空] |

现在,我们为天气应用配置了容器管理的安全性,因此我们应该从 faces-config.xml 中删除 AuthorizationListener 类及其引用。清单 12-7 显示了更新后的受天气控制的 bean。

清单 12-7。 更新了防风类

@Named
@RequestScoped
public class WeatherBacking extends BaseBacking {

    @EJB
    private UserManagerLocal userManager;

    @PostConstruct
    public void loadUser(ComponentSystemEvent event) {
        try {
            String userID = getRequest().getUserPrincipal().getName();
            AppUser sourceAppUser = userManager.getUser(userID);
            AppUser targetAppUser = (AppUser) evaluateEL("#{appUser}", AppUser.class);

            targetAppUser.setFirstName(sourceAppUser.getFirstName());
            targetAppUser.setLastName(sourceAppUser.getLastName());
            targetAppUser.setZipCode(sourceAppUser.getZipCode());
        } catch (Exception ex) {
            Logger.getLogger(WeatherBacking.class.getName()).log(Level.SEVERE, null, ex);
            getContext().addMessage(null, new FacesMessage(SYSTEM_ERROR));
        }
    }

    public String logout() {
        try {
            getRequest().logout();

            return "/home.xhtml?faces-redirect=true";
        } catch (ServletException ex) {
            Logger.getLogger(WeatherBacking.class.getName()).log(Level.SEVERE, null, ex);
        }

        return null;
    }
}

如代码所示,loadUser()方法使用其 ID 检索当前用户信息(用户 ID 可以从 java.security.Principal 检索,可以使用 HTTPServletRequest 的 getUserPrincipal() API 获取)。logout()调用 HTTPServletRequest 的 logout()方法,以便将用户从当前已验证的会话中注销。清单 12-8 显示了更新后的 weather.xhtml 页面。

清单 12-8。 更新 weather.xhtml 页面

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:mashup="http://code.google.com/p/mashups4jsf/">

<ui:composition template="/WEB-INF/templates/main.xhtml">
    <ui:define name="title">
        #{bundle['application.weatherpage.title']}
    </ui:define>
    <ui:define name="content">
         <f:event listener="#{weatherBacking.loadUser}" type="preRenderView" />
        <h:form>
            #{bundle['application.welcome']}, #{appUser.firstName} #{appUser.lastName}! <br/><br/>

            #{bundle['application.weatherpage.currentInfo']} for #{appUser.zipCode}:
            <mashup:yahooWeather temperatureType="c" locationCode="#{appUser.zipCode}"/> <br/><br/>

            <h:commandLink value="#{bundle['application.weatherpage.logout']}"
                           action="#{weatherBacking.logout}"></h:commandLink> <br/><br/>
        </h:form>
    </ui:define>
</ui:composition>

</html>

如粗体行所示,为了检索当前用户信息,在 preRenderView 事件中调用 weatherBacking bean 的 loadUser()方法(每次在视图呈现之前调用)。RegistrationBacking 类也必须更新。清单 12-9 显示了更新后的注册支持 bean。

清单 12-9。 更新注册支持 Bean

@Named
@RequestScoped
public class RegistrationBacking extends BaseBacking {

    @EJB
    private UserManagerLocal userManager;

    public String register() {
        FacesContext context = FacesContext.getCurrentInstance();
        Map<Object, Object> flowScope = context.getApplication().getFlowHandler().getCurrentFlowScope();

        AppUser appUser = new AppUser();

        appUser.setId((String) flowScope.get("id"));
        appUser.setPassword((String) flowScope.get("password"));
        appUser.setEmail((String) flowScope.get("email"));

        appUser.setFirstName((String) flowScope.get("fname"));
        appUser.setLastName((String) flowScope.get("lname"));
        appUser.setProfession((String) flowScope.get("profession"));

        appUser.setZipCode((String) flowScope.get("zipCode"));

        //Assign a group to the user ...
        AppGroup appGroup = new AppGroup(appUser.getId(), "weather_user");
        List<AppGroup> appGroups = new ArrayList<>();

        appGroups.add(appGroup);
        appUser.setAppGroupList(appGroups);

        try {
            userManager.registerUser(appUser);
        } catch (UserExistsException ex) {
            Logger.getLogger(RegistrationBacking.class.getName()).log(Level.SEVERE, null, ex);
            context.addMessage(null, new FacesMessage(USERNAME_ALREADY_EXISTS));
            return null;
        } catch (Exception ex) {
            Logger.getLogger(RegistrationBacking.class.getName()).log(Level.SEVERE, null, ex);
            context.addMessage(null, new FacesMessage(SYSTEM_ERROR));
            return null;
        }

        return "flowReturn";
    }

    //...
}

如粗体行所示,在创建 appUser 对象并用用户信息填充后,它被分配给“weather_user”组,该组在清单 12-5 的映射文件中提到。

为了在天气应用中应用容器管理的安全性,我们需要做的就是这些。为了获得天气应用的完整源代码,请从 www.apress.com/9781430250104的图书网站第十二章源代码下载。

JSF 应用性能

调优 JSF 应用的性能是每个 JSF 开发者需要了解的最重要的方面之一。在这一部分中,我们将讨论为了增强 JSF 2.x 应用的性能而可以调整的最重要的方面。

刷新周期

这个时间间隔指定了 Facelets 编译器在检查页面中的更改之前必须等待的时间。在开发过程中,建议将(javax . faces . facelets _ REFRESH _ PERIOD)参数的值设置为较低的值,以便在开发过程中帮助 JSF 开发人员能够在应用运行时编辑页面。在生产中,为了获得更好的性能,建议将(javax . faces . facelets _ REFRESH _ PERIOD)参数的值设置为-1(这意味着您不希望编译器在编译页面后检查更改),如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
    ...
    <context-param>
        <param-name>javax.faces.FACELETS_REFRESH_PERIOD</param-name>
        <param-value>-1</param-value>
    </context-param>
    ...
</web-app>

跳过评论

将(javax.faces.FACELETS _ SKIP _ COMMENTS)参数设置为 true 有助于通过从 FACELETS 页面中删除注释来减少通过网络发送的数据量。这些注释有助于在开发过程中理解代码,但在部署过程中是不必要的,同时,由于允许系统用户查看源代码注释,它们会带来安全风险。由于对安全性和性能都有影响,因此将该参数设置为 true 很重要,如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
    ...
    <context-param>
        <param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
        <param-value>true</param-value>
    </context-param>
    ...
</web-app>

设计阶段

将 javax.faces.PROJECT_STAGE 参数设置为“Development”允许 JSF 环境在页面中打印出调试信息。这在开发过程中很有帮助,但是在部署之后没有任何用处,除非您在测试环境中对错误或问题进行故障排除。在生产中,始终将该参数值设置为“生产”,以提高生产过程中的性能,如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
    ...
    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Production</param-value>
    </context-param>
    ...
</web-app>

状态保存方法

将 javax . faces . state _ SAVING _ METHOD 参数设置为“server”(这是默认值)比将该参数设置为“client”提供了更好的性能。这是因为服务器状态保存不需要状态的序列化。以下是将状态保存方法设置为“服务器”的示例。

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
    ...
    <context-param>
        <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
        <param-value>server</param-value>
    </context-param>
    ...
</web-app>

但是,重要的是要知道,如果服务器中没有足够的内存,可以将状态保存方法设置为“client”。

响应缓冲器

建议增加响应缓冲区大小,以减少渲染时的内存重新分配,这可以通过将 javax . faces . facelets _ BUFFER _ SIZE 参数(如果使用 Mojarra,则为 com . sun . faces . responsebuffersize 参数)设置为适合应用服务器内存容量的适当值来实现,如下例所示。

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
    ...
    <context-param>
        <param-name>javax.faces.FACELETS_BUFFER_SIZE</param-name>
        <param-value>500000</param-value>
    </context-param>
    <context-param>
        <param-name>com.sun.faces.responseBufferSize</param-name>
        <param-value>500000</param-value>
    </context-param>
    ...
</web-app>

如示例所示,javax.faces.FACELETS _ BUFFER _ SIZE 和 com . sun . faces . responsebuffersize 参数都设置为 500000 字节。

会话中的视图数量

会话中的视图数量由 Apache MyFaces 和 Oracle Mojarra 中的两个不同的上下文参数表示:

  • org . Apache . MyFaces . number _ OF _ VIEWS _ IN _ SESSION(在 Apache MyFaces 中)
  • com . sun . faces . numberofviewsinsession(Oracle Mojarra 中)

仅当状态保存方法设置为“服务器”时,这些参数才起作用。它定义了存储在会话中的序列化视图的最大数量。默认情况下,它被设置为 20(在 Apache MyFaces 中)或 15(在 Oracle Mojarra 中)。对于许多应用来说,将此参数设置为 15 或 20 可能不合适,因此,如果您的 JSF 应用不要求会话中具有如此数量的序列化视图,则建议减少此参数以节省服务器内存,如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
        ...
        <context-param>
            <param-name>org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION</param-name>
            <param-value>3</param-value>
        </context-param>
        ...
</web-app>

在 Mojarra 中,还有另一个相关的上下文参数(也适用于服务器端状态保存)可以优化,这就是(com . sun . faces . numberoflogicalviews)参数。此参数表示存储在会话中的应用逻辑视图的数量。默认情况下,它设置为 15。为了节省服务器内存,您可以尽可能减少这个数字,如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
        ...
        <context-param>
                <param-name>com.sun.faces.numberOfLogicalViews</param-name>
                <param-value>3</param-value>
        </context-param>
        ...
</web-app>

理解这两个参数的语义很重要:

  • numberOfLogicalViews 参数是指会话中逻辑视图的数量,您可以通过在不同的浏览器标签中打开您的 JSF 应用来进行试验;每个浏览器标签代表一个逻辑视图(获取视图)。例如,如果 numberOfLogicalViews 参数设置为三,并且您依次打开四个不同的浏览器选项卡,转到第一个选项卡,并提交表单(假设页面包含表单),那么您将得到 ViewExpiredException,因为代表第一个逻辑视图的第一个选项卡已从逻辑视图的 LRU(最近最少使用)映射中删除。这也意味着,如果您依次打开三个不同的浏览器选项卡,并转到其中任何一个选项卡来提交表单,您将不会遇到此异常,因为您没有超过逻辑视图的最大数量,即三个。
  • numberOfViewsInSession 参数指的是会话中帖子的浏览量,您可以通过在页面中多次提交表单来进行试验。例如,如果 numberOfViewsInSession 参数设置为 3,并且您提交了一个页面表单四次,按下浏览器的后退按钮四次,然后重新提交第一个页面表单,您将得到 ViewExpiredException,因为代表第一个视图的第一个页面表单已从帖子视图的 LRU 地图中删除。这也意味着,如果您提交表单三次,然后返回重新提交第一页表单,您将不会遇到此异常,因为您没有超过帖子查看次数的最大值,即三次。

Apache MyFaces 特定调优

如果应用服务器中有足够的内存,并且因为压缩会消耗 CPU 时间,那么可以禁用服务器状态压缩,如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
        ...
        <context-param>
                <param-name>
                        org.apache.myfaces.COMPRESS_STATE_IN_SESSION
                </param-name>
                <param-value>false</param-value>
        </context-param>
        ...
</web-app>

注意,当状态保存方法设置为“server”时,( org . Apache . myfaces . compress _ STATE _ IN _ SESSION)参数起作用。当状态保存方法设置为“server”时,另一个需要注意的重要参数是(org . Apache . myfaces . serialize _ STATE _ IN _ SESSION)。将 org . Apache . myfaces . serialize _ STATE _ IN _ SESSION 参数设置为 false,可以禁止序列化会话中的状态,这也将提供更好的性能,如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
        ...
        <context-param>
                <param-name>
                        org.apache.myfaces.SERIALIZE_STATE_IN_SESSION
                </param-name>
                <param-value>false</param-value>
        </context-param>
        ...
</web-app>

无国籍的 JSF

JSF 2.2 的一个有用特性是创建无状态视图的能力。创建无状态视图有两个主要优点:

  • 无状态视图比默认的有状态视图具有更好的性能,因为没有花费时间来保存和恢复

    中动态组件的状态。
  • 无状态视图比默认的有状态视图(使用

    )消耗更少的内存,因为在中保存动态组件的状态不消耗内存。

虽然对于中小型页面来说,性能和内存的提升相对较小,但是当您的应用中有很多组件的大型页面时,以及当您的 JSF 应用有很多硬件能力有限的并发用户时,这种提升会非常显著;这意味着无状态视图可以使 JSF 应用更具可伸缩性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意如果你想使用 JSF 开发公共网站,无状态视图是最强大的选择之一。

因为无状态视图没有状态,所以认识到它们不能同时用于视图和会话范围的 beans 是很重要的;这意味着您应该知道您的托管 beans 是在请求范围内设置的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意无状态视图可能与 JSF 组件库不兼容,如 PrimeFaces 或 RichFaces。因此,通常,您必须验证您正在使用的 JSF 组件在无状态模式下是否工作良好。

为了将无状态行为应用到您的 JSF 视图,您需要将的瞬态属性设置为 true,如下所示。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<f:view FontName">http://www.w3.org/1999/xhtml"
            xmlns:f="http://xmlns.jcp.org/jsf/core"
            xmlns:h="http://xmlns.jcp.org/jsf/html"
            transient="true">
    <html>
        <h:head>
            <title>Stateless Page</title>
        </h:head>
        <h:body>
                <!-- JSF HTML components -->
        </h:body>
    </html>
</f:view>

为了让你的 JSF 视图无状态,这就是你需要做的一切。

最佳实践

除了上述所有建议之外,您还需要考虑以下几点:

  • 不要在托管 beans 的 getters 中执行 I/O 操作的业务逻辑,因为在请求处理生命周期中可能会多次调用它们,这会降低应用的整体性能。业务逻辑必须转移到 JSF 动作方法或事件侦听器中。
  • 避免复杂的 EL 表达式。如果您有一个复杂的表达式,将其逻辑移到 Java 托管 beans 中。
  • 如果必须显示包含大量记录的数据表,请始终使用分页。
  • 如果可能的话,使用 Ajax ( )只发送您希望服务器处理的页面部分,并且只呈现应该重新呈现的页面部分(而不是整个页面)。
  • 最小化使用会话范围的受管 beans,以便最小化服务器内存的使用,并提高应用的可伸缩性。

摘要

在本章中,您了解了身份验证、授权和数据保护之间的区别。您了解了如何使用 Java EE 容器提供的安全特性来保护您的 JSF 应用。您知道如何在第十章中介绍的天气应用中应用容器管理的认证、授权和数据保护。您还了解了如何通过修改默认的 JSF 上下文参数和应用一组最佳实践来优化 JSF 应用的性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值