文章
Java
了解如何使用 LightView 将 REST 服务转换为可绑定属性集。
下载:
JavaFX 2 基于 Java 技术。JavaFX 2 将随 JDK 一起打包和交付。与第一版不同,JavaFX 2 的目标是作为正式的 Swing 继任者,专注于业务和企业应用程序。本系列文章共分为三部分,关于 JavaFX 的“企业”端,本文在动画、效果和转换方面着墨甚少,重点讲解如何构造表示逻辑和如何集成后台服务。
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 部署
LightFish 附带一个利用 JavaServer Faces 2 实现的基本 Web 界面,用于管理数据捕获时间间隔。LightView 是一个 JavaFX 2 实时可视化程序,它直接集成 Web UI 并通过 REST 和长轮询来访问监视数据。可以将其视为一个“压力测试信息板”。

图 2: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 文件
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.ExecutorService 和 Platform#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.URL 和 JAXBContext,即可删除外部库(两个 JAR 文件:jersey-core-1.9.1.jar 和 jersey-client-1.9.1.jar),但您将不得不再用几行代码重新实现现有 Jersey 的客户端功能。因为整个 HTTP 通信是在单一类 SnapshotFetcher 中实现的,所以稍后即可毫不费力地交换 HTTP 通信。
每次传递新的 Snapshot 实例时,必须重新启动 javafx.concurrent.Service 的子类 SnapshotProvider,并且必须使用监视数据更新视图。SnapshotProvider 需要一个连接 URI 来提取 Snapshot。类 DashboardPresenter 是 SnapshotProvider 与实际 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)中将自己订阅为 SnapshotProvider 的 valueProperty() 中的 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)时,DashboardPresenter 将 Snapshot 转发给 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.SUCCEEDED 或 Worker.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)并在其到达结束状态时重新启动 Service,javafx.concurrent.Service 得以成为实现长轮询通信样式的完美工具。每次执行 HTTP GET 请求时,将完成 Task,传递负载,且 Service 将自身转换为 SUCCEEDED 状态。已注册到 stateProperty 的 ChangeListener 重置并再次启动服务。
DashboardPresenter 将 Snapshot 实体作为 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 刷新。ConnectionPoolBinding 与 DashboardPresenter 无关,也可以模拟成一个类。不需要通过引入其他接口来进一步分离。
来自 DashboardPresenterBindings 的方法 ObservableList<Snapshot> getSnapshots() 发布迄今收集的所有 Snapshot 实例,并使它们可以方便地被 JavaFX javafx.scene.control.TableView 控件使用。
细粒度绑定模型允许清除视图、表示和业务逻辑的分离。SnapshotFetcher 独立于任何 JavaFX 库。其唯一职责是与后端通信并将 InputStream 转换成 Snapshot 实例。SnapshotProvider 将 SnapshotFetcher 与 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。