将 Adobe Flex 和 JavaFX 与 JavaServer Faces 2.0 结合使用


作者:Re Lai

了解如何利用 JSF 2.0 的新增特性,以及如何将 Flex 和 JavaFX 集成到 JSF 应用程序中

2011 年 3 月发布

JavaServer Faces (JSF) 2.0 规范基于过去六年间 JSF 1.0 规范应用的成功和经验教训而构建。它汲取了 Seam 和其他 Web 框架的灵感,整合了流行的敏捷性实践,例如惯例优于配置和批注优于 XML 等。这带来了更简化的框架。重点包括标准化的 AJAX 支持、作为默认视图技术的 Facelets 以及定制组合组件,最终使组件的创作更加直观、更令人愉悦。这里提供了有关 JSF 2.0 的全面概述。

本文探讨如何利用这些新特性来帮助嵌入富客户端应用程序。Adobe Flex 一直以来都是一种流行的富互联网应用程序框架。JavaFX 尽管相对较新,但因基于 Java 平台构建而备受关注。长期以来,人们一直对将富客户端集成到 Java Web 应用程序之中充满兴趣。JSF 2.0 及其对简化开发的关注使得集成比以往任何时候都更加轻松。

我们从一个 Flex 饼图应用程序示例开始,这个应用程序显示了有关冰激凌口味受欢迎程度的调查结果。JSF 组合组件用于封装嵌入。接下来,调查结果不采用硬编码的方法,而是通过一个 JSF 托管 bean 传递到 Flex 应用程序。随后,我们添加服务器往返,提交用户对于喜爱口味的选择,进一步增强示例。最后,在 JavaFX 中重新实现该图表,并展示如何将其嵌入 JSF 应用程序中。

运行示例

可以在这里下载示例应用程序的源代码。

该应用程序使用 Flex SKD 4.1、JSF Mojarra Implementation 2.0.2 和 JavaFX SDK 1.3.1 开发。NetBeans 6.9.1 用作 IDE,它已经绑定了后两个库。三个附加项目分别是 SampleChartFlex、SampleChartFX 和 SampleWeb。

要在 NetBeans 中运行 Web 应用程序,使用 NetBeans 打开这些项目,右键单击项目 SampleWeb 并运行。

要修改和编译 Flex 项目,您需要安装 Flex SKD 4,修改 SampleChartFlex/build.xml,使其指向 Flex SKD 的安装位置:

<!-- Change me to your Flex SDK installation location -->
<property name="FLEX_HOME" value="C:/Programs/Adobe/flex_sdk/4.1"/>


此后,您可以从 NetBeans 中调用 Build 命令来构建这些项目。我们已经定制了 SampleChartFlex 和 SampleChartFX 项目的 Ant 构建文件,使打包的 swt 或 jar 文件可以在 NetBeans 构建这两个项目期间自动复制到 SampleWeb 项目中。

创建 Flex 示例应用程序

首先,我们在 Flex 中创建一个简单的饼图应用程序,显示冰激凌口味的流行程度,如图 1 所示。单击其中的一个图项。随后,消息标签将显示您的选择。

lai-flex-javafx-jsf-f1

图 1 简单的饼图应用程序

Flex 源代码 (SampleChartFlex.mxml):

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx"
               styleName="plain"
               width="500" height="500">
      
   <mx:PieChart x="50" y="40" id="piechart"
       dataProvider="{getChartData()}" itemClick="onItemClick(event)" >
       <mx:series>
           <mx:PieSeries 
               field="rank" 
               nameField="flavor" 
               labelField="flavor"
               labelPosition="callout"
           />
       </mx:series>
   </mx:PieChart>
   
   <s:Label x="64" y="448" id="message" 
       text="Click to choose your favorite flavor."/>
   
   <fx:Script>
     <![CDATA[
       import mx.collections.ArrayCollection;
       import mx.charts.events.ChartItemEvent;

       private function getChartData() : ArrayCollection {
           return new ArrayCollection ([
               {flavor: "Vanilla",    rank: 60},
               {flavor: "Chocolate",  rank: 30},
               {flavor: "Strawberry", rank: 10}  
           ]);
       }
       
      private function onItemClick(event : ChartItemEvent) : void {
           var flavor : String = event.hitData.chartItem.item.flavor;
           message.text = "You chose " + flavor + ".";  
       } 
      ]]>         
   </fx:Script>
</s:Application>


Flex 应用程序包含一个饼图和一个消息标签。饼图数据由 getChartData() 函数提供。用户单击一个图项时,onItemClick 将处理事件并更新消息标签。使用 mxmlc 将源文件编译为 SampleChartFlex.swf。所提供的示例项目 SampleChartFlex 定制了自己的 ant build.xml 文件,构建项目时它将调用 mxmlc。

嵌入 Flex 应用程序

为了将 Flash 对象嵌入到我们的 JSF Web 应用程序中,我们首先将 SampleChartFlex.swf 添加到我们的 JSF 应用程序项目 SampleWeb 的 Web 内容的 resources/demochart 文件夹中。我们创建一个组合组件来封装这个嵌入。

组合组件是 JSF 2.0 中的新工具,它极大简化了定制组件的开发。您不再需要为编码、解码、标记库描述文件 (TLD) 和呈现器而担忧。只需声明一个 Facelets 组合组件文件并使用它,类似于 Grails 中备受赞誉的定制标记支持。下面是我们的定制组件 demo:chart。

JSF 组合组件源代码 (resources/demo/chart.xhtml):

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html">

    <!-- INTERFACE -->
    <cc:interface>
    </cc:interface>

    <!-- IMPLEMENTATION -->
    <cc:implementation>
        <h:outputScript name="swfobject.js" library="demochart" target="head" />
        <script type="text/javascript">
            (function(){
                var swfUrl = "${facesContext.externalContext.requestContextPath}
                     + /resources/demochart/SampleChartFlex.swf";
                var replaceElementId = "${cc.clientId}:chart";
                swfobject.embedSWF(swfUrl, replaceElementId, 
                    "500", "500", "10.0.0");
            })();
        </script>

        <div id="${cc.clientId}:chart" />
    </cc:implementation>
</html>


开源的 SWFObject 用于嵌入 Flash 内容。可在 Flex 4 SDK 安装目录的 templates\swfobject 文件夹下找到 JavaScript 文件 swfobject.js。将其复制到我们的 Web 内容的 resources\demochart 文件夹中。

我们采取了措施缓解了名称冲突:我们的本地变量是在一个匿名函数中定义的,div HTML 元素 ID 以组合组件客户端 ID 作为前缀。

现在,我们已经创建了定制组件,下面就可以像使用任何其他 JSF 标记一样使用 demo:chart 标记。看不出来在实现过程中使用了 Flex。下面是一个例子。

JSF 页面源代码 (index.xhtml):

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:demo="http://java.sun.com/jsf/composite/demo">
    <h:head>
        <title>Spice up your JSF Pages</title>
    </h:head>
    <h:body>
        <f:view>
            <h1> Spice Up Your JSF Pages </h1>
            <demo:chart />
        </f:view>
    </h:body>
</html>


向 Flex 应用程序传递变量

嵌入式 Flex 应用程序多半依赖于动态数据。事实表明,借助 flashVars 向 Flex 应用程序传递变量非常方便。

本节对我们的示例饼图进行扩展,即从 JSF 托管 bean 传递冰激凌口味调查结果。

JSF 托管 bean 源代码 (IceCreamSurvey.java):

package demo.data;

import java.util.HashMap;
import java.util.Map;
import javax.faces.bean.ManagedBean;

@ManagedBean
public class IceCreamSurvey {
    public Map<String, Integer> getResult() {
        Map<String, Integer> result = new HashMap<String, Integer>();
        result.put("Vanilla",    Integer.valueOf(60));
        result.put("Chocolate",  Integer.valueOf(30));
        result.put("Strawberry", Integer.valueOf(10));
        return result;
    }
}


我们使用 JSF 2.0 批注来指定托管 bean。

为了将调查结果从托管 bean 传递给 Flex,我们首先要修改 JSF 组合组件 chart.xhtml,为其 interface 部分添加一个名为 data 的属性以接受调查结果,然后将调查结果作为 flashVar 传递到 Flex。

JSF 组合组件源代码片段 (resources/demo/chart.xhtml):

    <!-- INTERFACE -->
    <cc:interface>
        <cc:attribute name="data" />
    </cc:interface>

    <!-- IMPLEMENTATION -->
    <cc:implementation>
        <h:outputScript name="swfobject.js" library="demochart" target="head" />
        <script type="text/javascript">
           (function(){
                var swfUrl = "${facesContext.externalContext.requestContextPath}
                    + /resources/demochart/SampleChartFlex.swf";
                var replaceElementId = "${cc.clientId}:chart";
                var expressInstall = "";
                var flashVars = {data: "${cc.attrs.data}"};
                swfobject.embedSWF(swfUrl, replaceElementId,
                    "500", "500", "10.0.0", expressInstall, flashVars);
           })();
    </cc:implementation>


现在,在使用方 JSF 页面中,我们只需将冰激凌口味调查结果传递至 demo:chart 标记即可。

JSF 页面源代码片段 (index.xhtml):

<demo:chart data="#{iceCreamSurvey.result}"/>


在 Flex 应用程序一端,我们需要修改 getChartData 函数以获取参数。

Flex 源代码片段 (SampleChartFlex.mxml):

 private function getChartData() : ArrayCollection {
      // Retrieve "data" from flashVars, 
      // Formatted as Map.toString(), e.g., {Strawberry=10, Chocolate=30, 
      //     Vanilla=60}
     var input : String = Application.application.parameters.data;
     var data : Array = input ? input.split(/\W+/) : [];
     var source = [];
     for (var index : int = 1; index < data.length - 1; index += 2) {
     	   source.push( (flavor: data[index], rank: parseInt(data[index+1])} );
     }
     return new ArrayCollection(source);        
 }


在本例中,数据格式非常简单。因此,我们只需使用正则表达式进行分析。在更为复杂的情况下,可以考虑使用 JavaScript 对象表示法 (JSON) 等正式编码。

使用 JSF AJAX 简化客户端与 JSF 服务器会话之间的通信

在本节中,我们将继续介绍更复杂的情况:Flex 与 JSF 服务器会话之间的往返通信。本节介绍一种新颖而实用的方法,利用 JSF 2.0 的 AJAX 特性整合 Flex 和 JSF 两者的优势。

Flex 应用程序可以通过多种方法与服务器通信。

LiveCyle 数据服务是 Adobe 提供的数据解决方案,囊括多种技术,包括基于 Java 服务器的 BlazeDS操作消息格式 (AMF)。尽管 BlazeDS 和 AMF 的开源特点使之可与其他技术共同使用,但如果服务能够使客户端和服务器端均采用完整的 Adobe 解决方案,那么这种服务将具有极大的吸引力。值得注意的是,Spring FlexGrails Flex 集成的最新发展基本都建立在 BlazeDS 和 AMF 的基础之上。

Flex 还提供了与服务器通信的通用数据访问组件,包括 HTTP 和 Web 服务。这使得 Flex 可以与包括 JSF 在内的多种服务器技术进行互操作。

此外,Flex 与 JavaScript 之间也有着很好的集成,这使我们能够在浏览器端实现集成,中继到 AJAX 应用程序以便与服务器通信。

实际上,这些方法均可配合 JSF 使用,而且每种方法都有着自己的优缺点。举例来说,Exadel Fiji 支持利用以上所有方法集成 Flex/JavaFX 与 JSF。

随着 JSF 2.0 的到来,一个有趣的方面就是 AJAX API 已经实现了标准化。在本节中,我们将利用这一特性将 Flex 应用程序与 JSF 相集成。实际上,这种集成是在浏览器端实现的。我们将依靠 JSF AJAX 框架来处理会话和视图状态跟踪。由于 JSF AJAX API 是 2.0 规范的一部分,因而能够保证所有实现均支持这种 API。在服务器端,使用了 Flex 客户端这点是相当透明的。因此,这种方法易于插入现有的 JSF 应用程序中。

可以想象得到,额外的 JSF AJAX 层会增加性能开销。当数据交换量较小时,这不是问题,使用 AJAX 时大多数都属于这种情况。然而,如果交换大量数据,则应考虑使用直接服务器访问,如上文提到的前两种方法。

我们将修改我们的示例,即在用户单击饼图中的一种口味时提交选择。一个 JSF 托管 bean 将处理这个选择并回复一条消息,随后该消息将显示在 Flex 应用程序中。在 Flex 应用程序一端,我们将修改 onItemClick 函数,令其使用 ExternalInterface 调用嵌入式 Web 页面内的 JavaScript 函数 demo.ajax.submit,我们稍后将定义该函数。

Flex 源代码片段 (SampleChartFlex.mxml):

 private function onItemClick(event : ChartItemEvent) : void {
     var flavor : String = event.hitData.chartItem.item.flavor;
     ExternalInterface.call("demo.ajax.submit", flavor);  
 }


添加一个名为 refresh 的回调函数,用以更新消息标签。在 Flex 应用程序初始化过程中,该函数将通过 ExternalInterface.addCallback 公开给 JavaScript,如下所示:

 

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx"
               styleName="plain"
               width="500" height="500"
	       creationComplete="init()" >
...
       private function init() : void {
     	   ExternalInterface.addCallback("refresh", refresh);
       }
       
       private function refresh(feedback : String): void {
     	   message.text = feedback;     	
       }


对我们的 JSF 组合组件,我们将在 interface 部分额外添加一个 response 属性,该属性映射到服务器对我们的异步提交做出的响应。

JSF 组合组件源代码片段 (resources/demo/chart.xhtml):

<cc:interface>
     <cc:attribute name="data" />
     <cc:attribute name="response" />
</cc:interface>


接下来,在 implementation 部分中定义一个隐藏表单,用于向服务器提交并接收来自服务器的响应:

<h:form id="form" style="display:none">
     <h:outputText id="out" value="#{cc.attrs.response}" />
</h:form>


添加以下 JavaScript 代码来处理异步提交和回复:

     
           // namespace: demo
           if (!demo)  var demo = {};
           
           // namespace: demo.ajax
           if (!demo.ajax) demo.ajax = {};

           demo.ajax.submit =  function(arg) {
                var options = {
                    input:   arg,
                    render:  "${cc.clientId}:form:out",
                    onevent: demo.ajax.onevent
                };
                jsf.ajax.request("${cc.clientId}:form", null, options);
           };

           demo.ajax.onevent =  function(event) {
               if (event.status == "success") {
                  var node = document.getElementById("${cc.clientId}:form:out");
                  var response = node.textContent || node.innerText;
                  var chart = document.getElementById("${cc.clientId}:chart");
                  chart.refresh(response);
              }
           };


Flex 函数 onItemClick 调用 demo.ajax.submit 函数将请求提交给服务器。它使用 JSF 2.0 JavaScript 函数 jsf.ajax.request,使用具有以下选项的隐藏表单来提交异步请求:

  • 负载将作为名为 input 的传递请求参数发送。

  • 它指示服务器在表单内呈现名为 out 的子 outputText。

  • 服务器响应将由 demo.ajax.onevent 事件处理程序处理。

demo.ajax.onevent 处理 AJAX 提交事件。操作成功后,它将从 outputText 节点获取响应,并调用 Flash 公开的 refresh 方法。它尝试以不同的方法获取节点文本,从而规避了浏览器差异。

在 JSF 服务器端,为 JSF 托管 bean 添加以下内容来处理提交。

JSF 托管 bean 源代码片段 (IceCreamSurvey.java):

private String selection;

public String getSelection() {
       return selection;
}
    
public void setSelection(String selection) {
       this.selection = selection;
}


public String getSelectionResponse() {
       return reply(selection);
}

  
public String reply(String flavor) {
       return "Good choice! Many people also like " + flavor + "!";
}


在使用方 JSF 页面内,首先在页头添加 jsf.js,以便在该页面中启用 JSF JavaScript。

JSF 页面源代码片段 (index.xhtml):

<h:outputScript library="javax.faces" name="jsf.js" target="head"/>


我们还需要进一步映射请求参数输入以及我们的定制饼图标记公开的 response 属性。执行该操作时,有几种选择。一种方法是利用 JSF 2.0 增强,允许 EL 操作绑定获取变量,如下所示:

<demo:chart data="#{iceCreamSurvey.result}" 
     response="#{iceCreamSurvey.reply(param.input)}" />


另一种方法是利用视图参数。可以通过视图参数将一个请求参数映射到一个 EL 表达式,如下所示:

<f:metadata>
   <f:viewParam name="input" value="#{iceCreamSurvey.selection}"/>
</f:metadata>
<demo:chart data="#{iceCreamSurvey.result}" 
   response="#{iceCreamSurvey.selectionResponse}" />


每种方法都有着自己的有趣之处。第一种方法较为直观,所需配置较少。第二种方法依赖于视图参数,这是一种可编辑的值占位符,因此可获取转换器和验证器。在需要复杂编码的情况下,第二种方法可能更为适用。

集成 JavaFX

类似地,我们可以在 JavaFX 中实现饼图应用程序。

lai-flex-javafx-jsf-f2 

图 2 在 JavaFX 中实现饼图应用程序

JavaFX 源代码 (demo.piechart.Main.FX):

package demo.piechart;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.chart.PieChart;
import javafx.scene.chart.PieChart3D;
import  javafx.stage.AppletStageExtension;

def piechart = PieChart3D {
   layoutX: 0.0
   layoutY: 11.0
   data:    getChartData()
}

def message = javafx.scene.control.Label {
   layoutX: 189.0
   layoutY: 340.0
   text:    "Click to choose your favorite flavor"
}

function getChartData() : PieChart.Data[]  {
   // Retrieve "data" from the FX argument
   // Formatted as Map.toString(), e.g., {Strawberry=10, Chocolate=30, 
   //     Vanilla=60}
   var input = FX.getArgument("data") as String;
   var data = input.split("\\W+");
   for (flavor in data where indexof flavor mod 2 == 1) {
       PieChart.Data {
           label:  flavor
           value:  Integer.parseInt(data[indexof flavor + 1])
           action: function() 
                       {AppletStageExtension.eval("demo.ajax.submit('{flavor}')")
                   }
       }
   }
}

function run(): Void {
   Stage {
       scene: Scene {
           height: 500
           width: 500
           content: [piechart, message]
       }
   }
}

// Call-back function to update the message label with the server response
public function refresh (response : String) : Void {
   message.text = response;
}


这段代码刻意采用了与我们的 Flex 应用程序类似的编写方式。饼图由名为 data 的运行时参数填充。我们使用 AppletStageExtension 调用容器页的 JavaScript 函数 demo.ajax.submit。对于 JavaFX,可以轻松公开回调函数 refresh。在 JavaFX 中,所有脚本级公共函数均自动对 JavaScript 可见。

要嵌入 JavaFX 小程序,请将 SampleChartFX.jar 和 SampleChartFX_browser.jnlp 复制到我们 Web 内容的 resources/demochart 文件夹中。请注意,NetBeans 生成的 jnlp 文件默认指向本地代码库。无论如何,由于我们将在 Web 页面中指定 jar 文件的位置,因此只需从 jnlp 文件中删除 codebase 属性即可。

随后只需对 JSF 组合组件略加修改即可嵌入 JavaFX 小程序。

JSF 组合组件源代码片段 (resources/demo/chart.xhtml):

<script type="text/javascript"  
    src="http://dl.javafx.com/1.3/dtfx.js" target="head"/>
<script type="text/javascript">
   javafx(
         {
            archive: "${facesContext.externalContext.requestContextPath}
                     + /resources/demochart/SampleChartFX.jar",
            draggable: true,
            width:  500,
            height: 500,
            code: "demo.piechart.Main",
            name: "${cc.clientId}:chart",
            id:   "${cc.clientId}:chart"
         },
         {
            data: "${cc.attrs.data}"
         }
     );
...


大多数 JavaScript 代码都将继续为我们的 JavaFX 小程序所用。唯一的变化在于 JavaScript 回调到 JavaFX 的方式。在 demo.ajax.onevent 函数中不再使用 chart.refresh(response),而是使用 chart.script.refresh(response)。为使代码能够同时适用于两种情况,请使用以下代码:

chart.refresh? chart.refresh(response) : chart.script.refresh(response)


就是这样。无需更改使用方 JSF 页面。使用 JSF 还是 JavaFX 来提供饼图都属于实现细节,对使用方页面而言都是完全透明的。

总结

在本文中,我们利用 JSF 2.0 的一项新特性将 Flex 和 JavaFX 集成到我们的 JSF 应用程序中。JSF 中的这些新功能使我们无需再关注衔接编码、解码、视图状态跟踪等事项。具体来说,我们利用组合组件特性创建了一个定制组件来封装 Flex 和 JavaFX 的嵌入。

另请参阅


Re Lai 是 Oracle 融合 CRM 管理软件开发高级经理。他是一名 Sun 认证企业架构师 (SCEA),在软件行业拥有十余年的经验。他目前的兴趣是使用 Java、脚本语言和 SOA 构建企业解决方案。


阅读本文的英文版本

前往 Java 中文社区论坛,发表您对本文的看法。

Left Curve
Java SDK 和工具
Right Curve
Left Curve
Java 资源
Right Curve