JavaFX 的企业端

作者:Adam Bien

了解如何使用 LightView 将 REST 服务转换为可绑定属性集。

2012 年 6 月发布

下载:

下载Java FX

下载GlassFish

下载LightFish

简介

JavaFX 2 基于 Java 技术。JavaFX 2 将随 JDK 一起打包和交付。与第一版不同,JavaFX 2 的目标是作为正式的 Swing 继任者,专注于业务和企业应用程序。本系列文章共分为三部分,关于 JavaFX 的“企业”端,本文在动画、效果和转换方面着墨甚少,重点讲解如何构造表示逻辑和如何集成后台服务。

LightFish — 实际示例

Oracle GlassFish Server 3.1 是一个 Java Platform, Enterprise Edition (Java EE) 6 应用服务器,它具有大量监视功能,使得其数据可通过多通道进行访问。(GlassFish Server 开源版同样如此。)最易于访问的通道是 REST 服务:只需一个 HTTP 客户端或命令行工具(如 cURL 或 Wget)即可访问监视统计信息。此类数据对于“成功的”压力测试(即显示挂掉的服务器的测试)的事后分析尤为重要。唯一的问题是没有历史记录,只有最近的监视数据。在夜间运行的情况下,您可以使用历史数据来分析应用程序的趋势和整体强健性。

LightFish 是一个开源监视应用程序,该应用程序从“受测 GlassFish”计算机(参见图 1)定期提取并永久保存快照,并通过简化的 REST API 保证这些快照实时可用。
图 1:LightFish 部署

图 1:LightFish 部署

LightFish 附带一个利用 JavaServer Faces 2 实现的基本 Web 界面,用于管理数据捕获时间间隔。LightView 是一个 JavaFX 2 实时可视化程序,它直接集成 Web UI 并通过 REST 和长轮询来访问监视数据。可以将其视为一个“压力测试信息板”。

图 2:LightView 信息板

图 2:LightView 信息板

Maven 和 LightView

我们首先介绍 Java EE 6 和 Maven 3 之间的集成。JavaFX 2 应用程序与任何其他 Java 应用程序的构建方式相同。NetBeans 7 IDE 全面支持 JavaFX 2,因此可以使用 NetBeans 原生创建、构建和部署 JavaFX 应用程序。NetBeans 在后台使用 Ant 来实现此目的。

但是,大多数 Java EE 应用程序一直是使用 Maven 构建工具进行构建和测试的。创建此类项目最为简单的方法是从命令行执行 mvn archetype:generate,并接受建议值(尤其是 maven-archetype-quickstart 值)。mvn archetype:generate 命令将创建 jar 类型的基本项目,如清单 1 所示。

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.lightview</groupId>
    <artifactId>lightview</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>lightview</name>
    <url>http://maven.apache.org</url>
    <dependencies>
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>javafx</artifactId>
            <version>2.0</version>
            <systemPath>${fx.home}/rt/lib/jfxrt.jar</systemPath>
            <scope>system</scope>
        </dependency>
		<!-- remaining dependencies -->
    </dependencies>
        <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
</project>


清单 1:用于构建和运行 JavaFX 2 应用程序的 Maven 3 项目对象模型 (POM)

为使 JavaFX 2 应用程序可编译,还需要在类路径中包含 jfxrt.jar 文件。使用 system 域即可轻松地将引用添加到 POM。为此,请在 <systemPath> 标记中配置 fxrt.jar 的位置(参见清单 1)。

JavaFX SDK 附带 jfxrt jar 文件。为保持 Maven pom.xml 文件环境的独立性,请在 javafx 配置文件内的属性 (fx.home) 中指定实际路径(参见清单 2)。

<settings>
<profiles>
	<profile>
	         <id>javafx</id>
         <activation>
               <activeByDefault>false</activeByDefault>
         </activation>
            <properties>
	        <fx.home>[PATH_TO_JAVAFX]/javafx-sdk2.0.2-beta/</fx.home>
	    </properties>
</profiles>
</settings>


清单 2:Maven 配置文件设置

或者,您也可以使用以下命令将 jfxrt.jar 文件部署到本地 Maven 信息库:

mvn install:install-file -Dfile=jfxrt.jar -DgroupId=com.oracle -DartifactId=javafx -Dpackaging=jar -Dversion=2.0. 

jfxrt.jar 文件安装到本地信息库之后,可以使用 compile 域将该文件作为普通依赖项加以引用(参见清单 3)。

<dependency>
	<groupId>com.oracle</groupId>
	<artifactId>javafx</artifactId>
	<version>2.0</version>
	<scope>compile</scope>
</dependency>


清单 3:引用已安装的 jfxrt.jar 文件

JavaFX 和 REST

LightFish 根据可配置的时间间隔定期公开监视快照。可以通过阻塞 GET 请求(例如 http://localhost:8080/lightfish/live)来访问快照,其格式如下所示:

<snapshot>
	<usedHeapSize>86550272</usedHeapSize>
	<threadCount>94</threadCount>
	<peakThreadCount>94</peakThreadCount>
	<totalErrors>0</totalErrors>
	<currentThreadBusy>1</currentThreadBusy>
	<committedTX>21</committedTX>
	<rolledBackTX>0</rolledBackTX>
	<queuedConnections>0</queuedConnections>
	<pools>
		<jndiName>SamplePool</jndiName>
		<numconnfree>8</numconnfree>
		<waitqueuelength>0</waitqueuelength>
		…
	</pools>
</snapshot>


清单 4:XML 格式的 GlassFish 快照数据

仅当“服务器推送”带来新的快照时才释放连接。通过释放阻塞的 HTTP 连接并将数据推送到客户端来积极等待数据到达的过程称为“长轮询”,这是常见的 AJAX 做法。

与目前绝大多数可用的 UI 工具包类似,JavaFX UI 不是线程安全的。必须从事件队列访问 UI,而不能通过应用程序线程访问。实用工具方法 javafx.application.Platform#runLater(java.lang.Runnable runnable) 被应用程序线程调用,用异步处理的结果更新 UI。可以结合使用 java.util.concurrent.ExecutorServicePlatform#runLater 来异步更新 UI,但 JavaFX 2 提供了更便捷的方法。

javafx.concurrent.Service 类能够执行后台线程并使用其结果从事件队列更新 UI。为此必须使用 javafx.concurrent.Task 封装同步方法。与 javafx.concurrent.Task 不同,Service 可以重复使用。因为我们将轮询服务器,以获取 Snapshot 类的最近实例并重新创建 Task 实例,所以 Service 是一个自然之选。对于一次性使用的情况(如使用 HTTP-POST 方法发送非幂等性请求),独立的 Task 实现就足以应付,如清单 5 所示。

package org.lightview.service;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.lightview.entity.Snapshot;

public class SnapshotProvider extends Service<Snapshot> {
    private final String liveDataURL;
	//accessors omitted
    &Override
    protected Task<Snapshot< createTask() {
        return new Task<Snapshot>() {
            &Override
            protected Snapshot call() throws Exception {
                SnapshotFetcher fetcher = new SnapshotFetcher(liveDataURL);
                return fetcher.getSnapshot();
            }
        };
    }
}


清单 5:异步执行同步服务

在方法 Task#call 内部,系统将实例化 SnapshotFetcher 并同步执行 getSnapshot 方法。SnapshotFetcher#getSnapshot() 的执行受阻,它将一直等待直至下一个快照到达(参见清单 6)。

import com.sun.jersey.api.client.Client;
import javax.ws.rs.core.MediaType;
import org.lightview.entity.Snapshot;

public class SnapshotFetcher {
    private final String url;
    private final Client client;
    //constructor omitted
    public Snapshot getSnapshot() {
         return client.resource(this.url).
			accept(MediaType.APPLICATION_XML).
			get(Snapshot.class);
    }
}


清单 6:通过 HTTP 提取快照

SnapshotFetcher 是一个纯 Java Platform, Standard Edition (Java SE) 类。GET HTTP 请求的实现以及将 XML 流解组到 Snapshot 实体并不重要。对于 HTTP 通信,使用的是 Java API for RESTful Web Services (JAX-RS) 规范 Jersey 实现的 REST 客户端。Jersey 实现与后台的 HTTP 通信并将 XML 流转换为 Snapshot 实例。Jersey 将解组的任务委托给 Java API for XML Binding (JAXB)。要实现 Snapshot 的序列化和反序列化,只需使用 @XmlRootElement@XmlAccessorType JAXB 批注对其进行批注(参见清单 7)。

&XmlRootElement
&XmlAccessorType(XmlAccessType.FIELD)
public class Snapshot {
    private long id;
    private Date monitoringTime;
    private long usedHeapSize;
    public Snapshot() {   }

//other fields and accessors omitted
}


清单 7:带 JAXB 批注的 Snapshot 实体

或者,我们也可以使用 java.net.URL#openStream 方法从服务器获取 Snapshot 实例,并使用 javax.xml.bind.JAXBContext 类反序列化包含 Snapshot 的 XML 表示的流。使用普通 java.net.URLJAXBContext,即可删除外部库(两个 JAR 文件:jersey-core-1.9.1.jarjersey-client-1.9.1.jar),但您将不得不再用几行代码重新实现现有 Jersey 的客户端功能。因为整个 HTTP 通信是在单一类 SnapshotFetcher 中实现的,所以稍后即可毫不费力地交换 HTTP 通信。

REST 与 UI 的边界

每次传递新的 Snapshot 实例时,必须重新启动 javafx.concurrent.Service 的子类 SnapshotProvider,并且必须使用监视数据更新视图。SnapshotProvider 需要一个连接 URI 来提取 Snapshot。类 DashboardPresenterSnapshotProvider 与实际 DashboardView 之间的粘合剂,它向 SnapshotProvider 公开 uri StringProperty,如清单 8 所示。

public class DashboardPresenter implements DashboardPresenterBindings {
    private StringProperty uri;
    private ObservableList<Snapshot> snapshots;
    private ObservableMap<String, ConnectionPoolBindings> pools;
    SnapshotProvider service;
    private LongProperty usedHeapSizeInMB;
   //some property declarations omitted

    public DashboardPresenter() {
        this.snapshots = FXCollections.observableArrayList();
        this.pools = FXCollections.observableHashMap();
        this.uri = new SimpleStringProperty();
        this.usedHeapSizeInMB = new SimpleLongProperty();
        this.threadCount = new SimpleLongProperty();
	
        this.initializeListeners();
    }

    void initializeListeners() {
        this.uri.addListener(new ChangeListener<String>() {
            public void changed(ObservableValue<? extends String> observableValue, 
String s, String newUri) {
                restartService();
            }
        });
    }

    void restartService() {
        if (this.service != null && this.service.isRunning()) {
            this.service.cancel();
            this.service.reset();
        }
        this.startFetching();
    }


    public void setUri(String uri) {
        this.uri.setValue(uri);
    }

    public String getUri() {
        return this.uri.getValue();
    }

    public StringProperty getUriProperty() {
        return this.uri;
    }


    void startFetching() {
        this.service = new SnapshotProvider(getUri());
        service.start();
        service.valueProperty().addListener(
                new ChangeListener<Snapshot>() {

                    public void changed(ObservableValue<? extends Snapshot> 
observable, Snapshot old, Snapshot newValue) {
                        if (newValue != null) {
                            snapshots.add(newValue);
                            onSnapshotArrival(newValue);
                        }
                    }

                });
        registerRestarting();
    }

    void registerRestarting() {
        service.stateProperty().addListener(new ChangeListener<Worker.State>() {
            public void changed(ObservableValue<? extends Worker.State> observable, 
Worker.State oldState, Worker.State newState) {
                if (newState.equals(Worker.State.SUCCEEDED) || 
newState.equals(Worker.State.FAILED)) {
                    service.reset();
                    service.start();
                }
            }

        });
    }

    void onSnapshotArrival(Snapshot snapshot) {
        this.usedHeapSizeInMB.set(snapshot.getUsedHeapSizeInMB());
        this.threadCount.set(snapshot.getThreadCount());
	//some setters omitted
        this.updatePools(snapshot);
    }


    void updatePools(Snapshot snapshot) {
        List<ConnectionPool> connectionPools = snapshot.getPools();
        for (ConnectionPool connectionPool : connectionPools) {
            String jndiName = connectionPool.getJndiName();
            ConnectionPoolBindings bindings = ConnectionPoolBindings.from(connectionPool);
            ConnectionPoolBindings poolBindings = this.pools.get(jndiName);
            if(poolBindings != null){
                poolBindings.update(connectionPool);
            }else{
                this.pools.put(jndiName,bindings);
            }
        }
    }

  public ObservableList<Snapshot> getSnapshots() {
        return snapshots;
    }

    public ObservableMap<String,ConnectionPoolBindings> getPools() {
        return pools;
    }
//getters omitted…
}


清单 8:DashboardPresenter — REST 与 UI 之间的粘合剂

javafx.beans.property.SimpleStringProperty 实现了访问实时监视输入所需的 URI。JavaFX 属性支持类之间的同步。使用下面的一行代码将 javafx.scene.control.TextField txtUri 直接绑定到 URI 属性:

dashboardPresenter.getUriProperty().bind(txtUri.textProperty());

在绑定的 TextField 实例中输入的文本将自动与 StringProperty 同步。JavaFX 属性的状态可以使用 javafx.beans.value.ChangeListener 进行观察。每次状态发生更改时都将调用方法 changed。这些更新通知在此处的作用是:每当用户输入(参见清单 8 中的方法 initializeListeners)和更改 URI 时重新启动 Service

DashboardPresenter 监听 URI 的更改并通过重新配置和重新启动 SnapshotProvider 的方式来对这些更改做出反应。DashboardPresenter 在方法 startFetching(参见清单 8)中将自己订阅为 SnapshotProvidervalueProperty() 中的 ChangeListener,如清单 9 所示。

SnapshotProvider#valueProperty().addListener(new ChangeListener<Snapshot>() {
   public void changed(ObservableValue<? extends Snapshot> observable, Snapshot old, 
   Snapshot newValue) {}
}


清单 9:ChangeListener 注册

javafx.beans.property.ReadOnlyObjectProperty<T> valueProperty()javafx.concurrent.Worker 接口中指定,并在 javafx.concurrent.Service 中实现。

每次调用 changed(清单 9)时,DashboardPresenterSnapshot 转发给 onSnapshotArrival 方法(清单 10)并更新属性的值,有效地通知和更新所有关注的监听器。

    void onSnapshotArrival(Snapshot snapshot) {
        this.usedHeapSizeInMB.set(snapshot.getUsedHeapSizeInMB());
        this.threadCount.set(snapshot.getThreadCount());
        this.peakThreadCount.set(snapshot.getPeakThreadCount());
	     //some updates omitted	
        this.updatePools(snapshot);
    }


清单 10:SnapshotListener — 表示器与 UI 之间的接口

成功执行 Task 之后,无需进一步干预,SnapshotProvider 会将其内部 State 更改为 Worker.State.SUCCEEDED 并退出执行。为接收定期更新,使用 ChangeListener 监听状态更改并在到达结束状态 Worker.State.SUCCEEDEDWorker.State.FAILED 时重新启动服务,如清单 11 所示。

void registerRestarting() {
SnapshotProvider#stateProperty().addListener(new ChangeListener<Worker.State>(){
   public void changed(ObservableValue<? extends Worker.State> observable, 
Worker.State oldState, Worker.State newState) {
                if(newState.equals(Worker.State.SUCCEEDED) || 
newState.equals(Worker.State.FAILED)){
                    service.reset();
                    service.start();
                }
            }
        });
}


清单 11:自动服务重新启动

javafx.concurrent.Task 不同,javafx.concurrent.Service 实现可多次重用。通过对 stateProperty 做出响应(清单 11)并在其到达结束状态时重新启动 Servicejavafx.concurrent.Service 得以成为实现长轮询通信样式的完美工具。每次执行 HTTP GET 请求时,将完成 Task,传递负载,且 Service 将自身转换为 SUCCEEDED 状态。已注册到 statePropertyChangeListener 重置并再次启动服务。

分离绑定

DashboardPresenterSnapshot 实体作为 DashboardPresenterBindings 接口中定义的一组可绑定属性公开给视图(参见清单 12)。

public interface DashboardPresenterBindings {
    StringProperty getUriProperty();
    ReadOnlyLongProperty getId();
    ReadOnlyLongProperty getUsedHeapSizeInMB();
    ReadOnlyLongProperty getThreadCount();
    ReadOnlyIntegerProperty getPeakThreadCount();
    ReadOnlyIntegerProperty getBusyThreads();
    ReadOnlyIntegerProperty getQueuedConnections();
    ReadOnlyIntegerProperty getCommitCount();
    ReadOnlyIntegerProperty getRollbackCount();
    ReadOnlyIntegerProperty getTotalErrors();
    ObservableMap<String, ConnectionPoolBindings> getPools();
    ObservableList<Snapshot> getSnapshots();
}


清单 12:DashboardPresenterBindings — 视图与表示器之间的接口

清单 12 中以 ReadOnly 前缀开头的属性只能用于刷新视图。只有 getUriProperty() 方法公开一个绑定到 TextField 的可写 StringProperty。所有只读属性包含最新 Snapshot 实体的数据。从视图的角度来看,JavaFX 属性是一个最新可绑定属性,因此是一个方便使用的数据类型。每个 DashboardPresenterBindings 属性通过下面一行代码与视图同步:

this.heapView.value().bind(this.dashboardPresenter.getUsedHeapSizeInMB());

可以动态创建和安装 GlassFish JDBC 连接池,由此将其公开为动态和可绑定 ObservableMap。映射的关键字是一个典型字符串,值是 ConnectionPoolBindings 类(参见清单 13)。

public class ConnectionPoolBindings {
    private StringProperty jndiName;
    private IntegerProperty numconnfree;
    private IntegerProperty waitqueuelength;
    private IntegerProperty numpotentialconnleak;
    private IntegerProperty numconnused;

    private ConnectionPoolBindings() {
        this.jndiName = new SimpleStringProperty();
	//…
    }
    public ReadOnlyStringProperty getJndiName() {
        return jndiName;
    }
    public ReadOnlyIntegerProperty getNumconnfree() {
        return numconnfree;
    }
    public void setJndiName(String jndiName) {
        this.jndiName.set(jndiName);
    }
    public void setNumconnfree(int numconnfree) {
        this.numconnfree.set(numconnfree);
    }
  //some accessors omitted
    public static ConnectionPoolBindings from(ConnectionPool connectionPool) {
        ConnectionPoolBindings poolBindings = new ConnectionPoolBindings();
        poolBindings.update(connectionPool);
        return poolBindings;
    }
    void update(ConnectionPool connectionPool) {
        this.setJndiName(connectionPool.getJndiName());
        this.setNumconnfree(connectionPool.getNumconnfree());
        this.setNumpotentialconnleak(connectionPool.getNumpotentialconnleak());
        this.setNumconnused(connectionPool.getNumconnused());
        this.setWaitqueuelength(connectionPool.getWaitqueuelength());
    }
}


清单 13:ConnectionPoolBindings 适配器

ConnectionPool 的运行时统计信息直接公开为 ConnectionPoolBindings 类。ConnectionPoolBinding 类可视为适配器:它将 ConnectionPool 转换为一组 JavaFX 属性。属性的状态只能在内部更改。该类的外部用户只能获得对 getter 方法公开的数据的只读访问。ConnectionPoolBinding 的实例用静态方法 from 创建,并用方法 update 刷新。ConnectionPoolBindingDashboardPresenter 无关,也可以模拟成一个类。不需要通过引入其他接口来进一步分离。

来自 DashboardPresenterBindings 的方法 ObservableList<Snapshot> getSnapshots() 发布迄今收集的所有 Snapshot 实例,并使它们可以方便地被 JavaFX javafx.scene.control.TableView 控件使用。

清除职责分离

细粒度绑定模型允许清除视图、表示和业务逻辑的分离。SnapshotFetcher 独立于任何 JavaFX 库。其唯一职责是与后端通信并将 InputStream 转换成 Snapshot 实例。SnapshotProviderSnapshotFetcher 与 JavaFX 线程模型集成,并在后台实现 Snapshot 实例的定期提取。这两个类都取代经典业务委托模式,并代理 Java EE 后端服务。

通过将 Snapshot 实例转换成细粒度属性,SnapshotPresenter 成为后端服务和视图逻辑之间的粘合剂。DashboardPresenter 完全分离视图与数据源实现,并使视图的实现更方便。为接收监视数据,视图只需将其视图组件绑定到发布的 SnapshotPresenterBindings 接口。

在本系列文章的第 2 部分,我们将从视图角度讨论信息板的结构以及 JavaServer Faces 2 UI 与 WebView 的直接集成。

另请参见

关于作者

顾问兼作者 Adam Bien 是 Java EE 6 和 7、EJB 3.X、JAX-RS、CDI 和 JPA 2.X JSR 专家组成员。他从 JDK 1.0 就开始使用 Java 技术,并在几个大型项目中使用了 servlet/EJB 1.0,目前是 Java SE 和 Java EE 项目的架构师和开发人员。他编辑了多本关于 JavaFX、J2EE 和 Java EE 的图书,并且是《Real World Java EE PatternsRethinking Best Practices》《Real World Java EE Night Hacks—Dissecting the Business Tier》两本书的作者。Adam 还是 Java Champion 和 JavaOne 2009 Rock Star。