JavaFX 集成策略

作者:Adam Bien

借助 Lambda 和对异步通信的支持,JavaFX 为后端服务带来了新的集成可能性。

2013 年 11 月发布

您很难在企业中找到独立的应用程序。企业桌面应用程序呈现和操纵应用服务器公开的一个或多个后端服务的数据。在过去的 Swing 和 J2EE 时代,通信通常是单向、同步的。JavaFX 和 Java EE 6、7 引入了各种新的同步、异步、推送和拉取集成策略。本文重点介绍 JavaFX 应用程序与 Java EE 服务的集成。

JavaFX 即 Java

JavaFX 即 Java。因此,用于 Swing 应用程序的最佳实践也适用于 JavaFX。后端服务的集成与协议和技术无关。

服务涉及 IP 地址、端口和属性文件等各种配置。而 API 的方法往往抛出 java.rmi.RemoteException 等特定于协议的异常,从而用不相关的信息污染表示逻辑。专有服务的瘦包装器封装了实现细节,仅公开一个更有意义的接口。这是一个经典的四人组 (GoF) 适配器模式

业务委托的复兴

J2EE 客户端之前很大程度上依赖于基于互联网 ORB 间协议的远程方法调用 (RMI-IIOP) 通信,后来在后端服务方面又依赖于 Java API for XML-based RPC (JAX-RPC) 和 Java API for XML Web Services (JAX-WS)。这两个 API 大量使用检查到的异常,并且依赖于特定的技术。要将表示逻辑与协议分离,必须使用业务委托模式

使用业务委托减少表示层客户端与业务服务之间的耦合。业务委托隐藏了业务服务底层的实现细节,如 EJB 架构的查找和访问细节。

通常,可以通过在默认情况下创建真正代理、在测试环境中创建模拟对象的工厂来扩展业务委托。使用 Mockito 这样的现代模拟库可以直接模拟业务委托。JavaFX 和 Java EE 环境中的业务委托可以作为适配器传统 Java 对象 (POJO) 实现,封装了实现细节,为 JavaFX 提供一个方便的接口。

图 1

图 1

先请求,后响应

向应用服务器发送一个阻塞请求,然后等待数据到达,这是最简单的后端集成。业务委托变成与后端通信的服务,如清单 1 所示:

import javax.json.JsonObject;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

public class MethodMonitoring {
 
    private Client client;

    public void init() {
        this.client = ClientBuilder.newClient();
    }
    public MethodsStatistics getMethodStatistics(String application, String ejbName) {
        final String uri = getUri();
        WebTarget target = this.client.target(uri);
        Response response = target.
                resolveTemplate("application", application).
                resolveTemplate("ejb", ejbName).
                request(MediaType.APPLICATION_JSON).get(Response.class);
        if (response.getStatus() == 204) {
            return null;
        }
        return new MethodsStatistics(response.readEntity(JsonObject.class));
    }
 }

清单 1

MethodMonitoring 类易于实现和测试,可与表示器集成。由于 getMethodStatistics 方法可能阻塞无限长的时间,因此来自 UI 监听器方法的同步调用可能导致 UI 没有响应。

异步集成

幸运的是,JAX-RS 2.0 API 还支持基于回调的异步通信模型。getMethodStatistics 方法不会使用阻塞请求的方式,而是发起请求并注册一个回调(参见清单 2)。

import javax.json.JsonObject;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.InvocationCallback;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import java.util.function.Consumer;

public class MethodMonitoring {
    private Client client;

    @PostConstruct
    public void init() {
        this.client = ClientBuilder.newClient();
    }

    public void getMethodStatistics(Consumer<MethodsStatistics> consumer, Consumer<Throwable> error, 
    String application, String ejbName) {
        final String uri = getUri();
        WebTarget target = this.client.target(uri);
        target.
                resolveTemplate("application", application).
                resolveTemplate("ejb", ejbName).
                request(MediaType.APPLICATION_JSON).async().get(new InvocationCallback<JsonObject>() {
            @Override
            public void completed(JsonObject jsonObject) {
                consumer.accept(new MethodsStatistics(jsonObject));
            }

            @Override
            public void failed(Throwable throwable) {
                error.accept(throwable);
            }
        });
    }
}

清单 2

回调将传入的 JsonObject 转换成域对象,然后传递给 java.util.function.Consumer 的实现。业务委托的实现仍独立于 JavaFX API,使用 Java 8 java.util.function.Consumer 作为回调。使用 Java 7,任何自定义接口或类均可用作回调。但如果使用 Java 8,JavaFX 表示器的实现可大大简化,如清单 3 所示。

...
      this.methodMonitoring.getMethodStatistics(s -> onArrival(s), 
        t -> System.err.println(t), this.monitoredApplication, ejb);
...
    void onArrival(MethodsStatistics statistics) {
        Platform.runLater(() -> {
            this.methodStatistics.clear();
            this.methodStatistics.addAll(statistics.all());
        }
        );
    }

清单 3

java.util.function.Consumer 可作为 lambda 表达式实现,从而大大减少代码量。JavaFX 是单线程 UI 工具包,因此不能由多个线程异步访问。

java.lang.Runnable 接口的 lambda 实现被传递给 Platform.runLater 方法,同时添加到事件队列供以后执行。根据 Javadoc,“将来某个不确定的时间在 JavaFX 应用程序线程上运行指定的 Runnable。该方法可通过任何线程进行调用,将 Runnable 发布到事件队列,然后立即返回到调用方。Runnable 按发布顺序执行。先执行传递给 runLater 方法的 runnable,然后再执行传递给后续 runLater 调用的 Runnable。

Platform#runLater 方法不适合执行长时间运行的任务;但它非常适合从异步线程更新 JavaFX UI 组件。

实际工作中的任务

Platform.runLater 不是用来执行“繁重任务”的;而是用来快速更新 JavaFX 节点的。异步调用长时间运行的方法需要创建线程,JavaFX 通过 javafx.concurrent.Task 类提供原生支持。Task 实现了 WorkerEventTarget 接口,并且继承了 java.util.concurrent.FutureTask 类,可视为 Java 线程与 JavaFX 事件机制之间的桥梁。Task 可作为普通 Runnable 直接供 Thread 使用,也可作为 Callable 传递给 ExecutorService。无论是哪种情况,均可先用业务委托封装无法异步执行的同步 Java EE API(例如 IIOP):

public class SnapshotFetcher{ 
...
    public Snapshot getSnapshot() {
        return fetchFromServerSynchronously();
    }
...

清单 4

接下来,用 Task 封装阻塞业务委托方法,然后就可以异步执行了。

Task<Snapshot> task = new Task<Snapshot>() {
            @Override
            protected Snapshot call() throws Exception {
                SnapshotFetcher fetcher = new SnapshotFetcher();
                return fetcher.getSnapshot();
        };

清单 5

TaskRunnableFuture 组成,因此可由 Thread 直接执行,也可以提交给 Executor。JavaFX 自带的 javafx.concurrent.Service 类使用可绑定属性将线程与 UI 无缝集成。Service 实际上是一个 Task 工厂:

import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.lightview.model.Snapshot;

public class SnapshotProvider extends Service<Snapshot> {
  @Override
    protected Task<Snapshot> createTask() {
        return new Task<Snapshot>() {
            @Override
            protected Snapshot call() throws Exception {
                SnapshotFetcher fetcher = new SnapshotFetcher();
                return fetcher.getSnapshot();
        };
        };
    }
}

清单 6

Service 的状态和 Task 执行结果可用作 JavaFX 可绑定属性:

        this.service = new SnapshotProvider();
        service.start();
        service.valueProperty().addListener(
                (observable,old,newValue) ->{
                //process new value
                    });

清单 7

无论是异步集成原来的同步资源,还是在客户端执行长时间运行的进程,Task 类都是方便的工具。

异步性的阻塞

对于模拟与浏览器进行基于 HTTP 的推送式通信来说,Comet 和长轮询都是令人讨厌的黑客攻击。HTTP 是一个请求-响应协议,所以响应只能作为请求的答复进行发送。因此,如果没有初始请求,则无法通过 HTTP 向浏览器推送数据。长轮询通信方式很容易实现:浏览器启动被服务器阻止的连接。服务器使用阻塞连接将数据发送给浏览器,浏览器立即关闭连接。浏览器处理数据,然后与服务器建立后续阻塞连接。如果没有数据可返回,服务器向浏览器发送 204 请求。

由于 JavaFX 应用程序作为独立的 Java 应用程序部署在企业中,因此不仅限于 HTTP 通信。但 REST 端点通常也可用于 HTML5 客户端,并且可以直接被 JavaFX 应用程序重用。REST 和 JSON 成为与 HTML5 客户端、Java 应用程序以及甚至低级设备进行通信的新标准。

JavaFX 应用程序可以直接参与长轮询,并且能够和 HTML5 客户端一样得到通知。同步通信与长轮询唯一区别是,长轮询反复发起阻塞调用。可通过 javafx.concurrent.Service 直接实现重复轮询。无论执行失败还是成功,都将重置并重启服务:

javafx.concurrent.Service service = ...;
    void registerRestarting() {
        service.stateProperty().addListener((observable,oldState,newState) -> {
                if (newState.equals(Worker.State.SUCCEEDED) ||
                        newState.equals(Worker.State.FAILED)) {
                    service.reset();
                    service.start();
            }
        });
    }

清单 8

推送集成

推送式通信是一种没有请求方的请求-响应式通信;应用服务器随时都可以推送数据。Java 消息服务 (JMS)、WebSocket 和内存网格有一个触发即忘式的通知机制,可轻松与 JavaFX 集成。

作为 Java EE 7 的一部分,JSR 356 实现了 WebSocket 协议,并且提供了一个 Java 客户端 API。WebSocket 规范引入了一个双向二进制协议,非常适合 UI 客户端的集成。WebSocket 消息兼具二进制和文本的特点,通过 Endpoint 子类来接收,如清单 9 所示:

import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.StringReader;

public class SnapshotEndpoint extends Endpoint {

    private SimpleObjectProperty<Snapshot> snapshot;
    private final Unmarshaller unmarshaller;
    private JAXBContext jaxb;

    public SnapshotEndpoint() {
        try {
            this.jaxb = JAXBContext.newInstance(Snapshot.class);
            this.unmarshaller = jaxb.createUnmarshaller();

        } catch (JAXBException e) {}
        this.snapshot = new SimpleObjectProperty<>();
    }

    @Override
    public void onOpen(Session session, EndpointConfig ec) {
        session.addMessageHandler(new MessageHandler.Whole<String>() {
            @Override
            public void onMessage(String message) {
                final Snapshot current = deserialize(message);
                Platform.runLater(() ->
                        snapshot.set(current));
            }
        });
    }
    Snapshot deserialize(String message) {
        try {
            return (Snapshot) unmarshaller.unmarshal(new StringReader(message));
        } catch (JAXBException e) {}
    }
}

清单 9

SnapshotEndpoint 类接收一个字符串消息,然后使用用于 XML 绑定的 Java 架构 (JAXB) API 进行转换。Snapshot 域对象是带批注的 POJO:

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Snapshot {
 //...arbitrary fields
    private long id;
}

清单 10

JSR 356 API 支持扩展,所以可以将序列化和反序列化单列为一个专用类。而且,我们并不限于 JAXB;我们可以使用任何可用的对象表示法,例如 JSON 或序列化。由于 SnapshotEndpoint 是由一个专用的 WebSocket 线程在客户端上执行的,所以该消息不能只用来更新 UI。使用 Platform.runLater 方法,可将消息从 WebSocket 正确传递给 JavaFX 线程。

Endpoint 只负责实际通信。此外,WebSocket 容器还要求对专用类中实现的内容进行初始配置和初始化:

public class SnapshotSocketListener {
    private SnapshotEndpoint endpoint;
    private Session session;

    public SnapshotSocketListener() {
        this.endpoint = new SnapshotEndpoint();
        WebSocketContainer container = ContainerProvider.getWebSocketContainer();
        ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build();
        String uri = "ws://localhost:8080/lightfish/snapshots/";
        try {
            session = container.
            connectToServer(this.endpoint, config, URI.create(uri));
        } catch (DeploymentException | IOException e) {
            throw new IllegalStateException("Cannot connect to WebSocket: ", e);
        }
    }

    public ReadOnlyObjectProperty<Snapshot> snapshotProperty() {
        return endpoint.snapshotProperty();
    }

    @PreDestroy
    public void disconnect() {
        try {
            session.close();
        } catch (IOException e) {
        }
    }
}

清单 11

到目前为止,我们几乎还没有使用 JavaFX 集成功能;相反,我们一直关注各种集成方式。但 JavaFX 属性让同步和异步集成特别有趣。

JavaFX 方式的集成

javafx.beans.property.ObjectProperty 封装了 Object 实例,并且可以绑定。相关客户端可以注册为监听器或直接绑定到属性,然后在封装的实例被替换时收到通知。(参见 Java Magazine 文章“企业应用程序的 JavaFX 数据绑定”中的“窄接口绑定”一节。)无论哪种通信协议和同步性,响应都携带一个必须由 UI 显示的域对象。通过 ObjectProperty,UI 可以直接绑定到值,这样当数据到达时会自动收到通知。表示器直接绑定到 ObjectProperty,无需其他管理方法:

        this.snapshotSocketListener.snapshotProperty().
        addListener((o, oldValue, newValue) -> {
            snapshots.add(newValue);
            onSnapshotArrival(newValue);
        });

清单 12

可绑定的 ObjectProperty 大大简化了操作,令接口变得异常地“窄”。绑定也适用于出站通信。表示器更改域对象或模型的状态,从而引发服务通知(业务委托)。出站通信不需要与 UI 线程同步。异步操作甚至可以直接从 UI 线程执行,所以长时间运行的操作除了可以用 javafx.concurrent.Service 封装以外,还可以在业务委托内以异步方式执行。但是,并非所有 UI 操作都会更改域对象的状态。“保存”或“刷新”等简单的用户操作可以直接转换为业务委托方法调用。

提高响应速度,简化代码

JavaFX UI 是事件驱动的异步 UI。而且,Java EE 7 API(如 JAX RS 2.0、JMS 或 WebSocket)也具有异步功能。JavaFX 与异步 Java EE 7 API 结合使用可大大简化代码。所有业务委托操作均可异步执行,不会阻塞用户界面,甚至业务委托本身也不受影响。交互模式与通信协议无关,因此所有异步 Java EE 7 API 都可使用一种交互模式。

请求以“触发即忘”的方式发送给服务器。无需等待响应,注册了一个回调方法来处理响应处理。回调接收数据,填充域对象,并使用 Platform.runLater 方法中的 ObjectProperty#set 替换当前域对象。域对象的改动被传播到表示器,表示器进而将改动广播到所有相关视图。

完全异步通信大大减少了所需的代码量。将数据绑定与双方向的触发即忘式方法结合使用,无需在临时模型与服务器端的主状态之间进行数据同步。所有操作直接传递给业务委托,然后来自应用服务器的所有响应直接更新 UI。

完全异步的交互还可极大改善用户体验 (UX);无论服务器端的交互如何耗费资源,UI 永远不会卡顿。

总结

乍一看,JavaFX 与后端服务的集成非常类似于 Swing。Platform#runLater 对应于 javax.swing.SwingUtilities#invokeLaterjavafx.concurrent.Service 的用途与 javax.swing.SwingWorker 类似。

现代 Java EE 7 API、JavaFX 数据绑定(参见“企业应用程序的 JavaFX 数据绑定”),以及 FXML 和 Scene Builder 的“反转控制”功能(参见“将 JavaFX Scene Builder 集成到企业应用程序中”)不仅让我们能够大大简化表示逻辑,还能通过统一的方法实现多视图桌面应用程序。

通过 Java EE 6 和 7 后端,您也可以在服务器端继续使用异步交互方式。

另请参见

关于作者

顾问兼作者 Adam Bien 是 Java EE 6 和 7、EJB 3.X、JAX-RS 和 JPA 2.X JSR 专家组成员。他从 JDK 1.0 就开始使用 Java 技术,并使用了 Servlet/EJB 1.0,目前是 Java SE 和 Java EE 项目的架构师和开发人员。他编辑了多本关于 JavaFX、J2EE 和 Java EE 的图书,并且是《Real World Java EE Patterns—Rethinking Best Practices》《Real World Java EE Night Hacks—Dissecting the Business Tier》两本书的作者。Adam 还是 Java Champion、Top Java Ambassador 2012 和 JavaOne 2009、2011 及 2012 Rock Star。Adam 不定期在慕尼黑机场 组织 Java EE 研讨会 (http://airhacks.com)。

分享交流

请在 FacebookTwitterOracle Java 博客上加入 Java 社区对话!