在 Vaadin 7 应用程序中使用推送在多个客户端显示相同数据

Displaying Same Data Aross Multiple Clients Using Push in Vaadin 7 app

我想将同一套数据共享给多个客户端。我需要使用 Push 来自动更新他们在屏幕上的视图。

我已阅读问答,。现在我需要一个更强大的现实示例。一方面,我知道在 Servlet 环境中拥有一个永无止境的 Thread 并不是一个好主意。

而且我不希望每个用户都有自己的线程,每个用户都自己访问数据库。让一个线程单独检查数据库中的新数据似乎更合乎逻辑。找到后,该线程应将新数据发布给所有等待更新的 UI/Layouts 用户。

Fully-Working 例子

您将在下面找到几个 class 的代码。他们一起制作了一个 Vaadin 7.3.8 应用程序的完整示例,使用新的 built-in 推送功能同时向任意数量的用户发布一组数据。我们通过随机生成一组数据值来模拟检查数据库中的新数据。

当您 运行 这个示例应用程序时,会出现 window 显示当前时间以及一个按钮。时间每秒更新一次,共一百次。

这个time-updating是不是真实的例子。 time-updater 还有两个其他目的:

  • 它的简单代码检查 Push 是否在您的 Vaadin 应用程序、Web 服务器和 Web 浏览器中正确配置。
  • 按照Server Push section of The Book Of Vaadin. Our time-updater here is almost exactly lifted from that example except that where they update a chart中给出的示例代码,我们每分钟更新一段文字。

要查看此应用程序的真实预期示例,click/tap "Open data window" 按钮。第二个 window 打开以显示三个文本字段。每个字段都包含一个 randomly-generated 值,我们假装它来自数据库查询。

做这件事有点麻烦,需要好几件。让我们回顾一下这些部分。

推送

在当前版本的 Vaadin 7.3.8 中,不需要插件或 add-ons 来启用 Push technology。甚至 Push-related .jar 文件也与 Vaadin 捆绑在一起。

参见Book Of Vaadin for details. But really all you need to do is add the @Push annotation to your subclass of UI

使用最新版本的 Servlet 容器和 Web 服务器。推送相对较新,实现也在不断发展,尤其是 WebSocket 种类。例如,如果使用 Tomcat,请务必使用 Tomcat 7 或 8 的最新更新。

定期检查新数据

我们必须有一些方法来重复查询数据库以获取新数据。

A never-ending 线程不是在 Servlet 环境中执行此操作的最佳方法,因为线程不会在 Web 应用程序取消部署或 Servlet 包含关闭时结束。 Thread会在JVM中继续运行,浪费资源,造成内存泄露等问题

网络应用程序Startup/Shutdown挂钩

理想情况下,我们希望在网络应用程序启动(部署)和网络应用程序关闭(或取消部署)时得到通知。收到通知后,我们可以启动或中断 database-querying 线程。幸运的是,每个 Servlet 容器都提供了这样一个钩子。 Servlet spec requires a container support the ServletContextListener 界面。

我们可以写一个class实现这个接口。当我们的网络应用程序(我们的 Vaadin 应用程序)被部署时,我们的监听器 class' contextInitialized is called. When undeployed, the contextDestroyed 方法被调用。

执行者服务

从这个钩子我们可以启动一个线程。但是有更好的方法。 Java 配备 ScheduledExecutorService. This class has a pool of Threads at its disposal, to avoid the overhead of instantiating and starting threads. You can assign one or more tasks (Runnable) 执行器,定期 运行。

网络应用监听器

这是我们的 Web 应用程序侦听器 class,使用 Java 8.

中可用的 Lambda 语法
package com.example.pushvaadinapp;

import java.time.Instant;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

/**
 * Reacts to this web app starting/deploying and shutting down.
 *
 * @author Basil Bourque
 */
@WebListener
public class WebAppListener implements ServletContextListener
{

    ScheduledExecutorService scheduledExecutorService;
    ScheduledFuture<?> dataPublishHandle;

    // Constructor.
    public WebAppListener ()
    {
        this.scheduledExecutorService = Executors.newScheduledThreadPool( 7 );
    }

    // Our web app (Vaadin app) is starting up.
    public void contextInitialized ( ServletContextEvent servletContextEvent )
    {
        System.out.println( Instant.now().toString() + " Method WebAppListener::contextInitialized running." );  // DEBUG logging.

        // In this example, we do not need the ServletContex. But FYI, you may find it useful.
        ServletContext ctx = servletContextEvent.getServletContext();
        System.out.println( "Web app context initialized." );   // INFO logging.
        System.out.println( "TRACE Servlet Context Name : " + ctx.getServletContextName() );
        System.out.println( "TRACE Server Info : " + ctx.getServerInfo() );

        // Schedule the periodic publishing of fresh data. Pass an anonymous Runnable using the Lambda syntax of Java 8.
        this.dataPublishHandle = this.scheduledExecutorService.scheduleAtFixedRate( () -> {
            System.out.println( Instant.now().toString() + " Method WebAppListener::contextDestroyed->Runnable running. ------------------------------" ); // DEBUG logging.
            DataPublisher.instance().publishIfReady();
        } , 5 , 5 , TimeUnit.SECONDS );
    }

    // Our web app (Vaadin app) is shutting down.
    public void contextDestroyed ( ServletContextEvent servletContextEvent )
    {
        System.out.println( Instant.now().toString() + " Method WebAppListener::contextDestroyed running." ); // DEBUG logging.

        System.out.println( "Web app context destroyed." );  // INFO logging.
        this.scheduledExecutorService.shutdown();
    }

}

数据发布者

在该代码中,您会看到 DataPublisher 实例被定期调用,要求它检查新数据,如果找到,则将其传递给所有感兴趣的 Vaadin 布局或小部件。

package com.example.pushvaadinapp;

import java.time.Instant;
import net.engio.mbassy.bus.MBassador;
import net.engio.mbassy.bus.common.DeadMessage;
import net.engio.mbassy.bus.config.BusConfiguration;
import net.engio.mbassy.bus.config.Feature;
import net.engio.mbassy.listener.Handler;

/**
 * A singleton to register objects (mostly user-interface components) interested
 * in being periodically notified with fresh data.
 *
 * Works in tandem with a DataProvider singleton which interacts with database
 * to look for fresh data.
 *
 * These two singletons, DataPublisher & DataProvider, could be combined into
 * one. But for testing, it might be handy to keep them separated.
 *
 * @author Basil Bourque
 */
public class DataPublisher
{

    // Statics
    private static final DataPublisher singleton = new DataPublisher();

    // Member vars.
    private final MBassador<DataEvent> eventBus;

    // Constructor. Private, for simple Singleton pattern.
    private DataPublisher ()
    {
        System.out.println( Instant.now().toString() + " Method DataPublisher::constructor running." );  // DEBUG logging.
        BusConfiguration busConfig = new BusConfiguration();
        busConfig.addFeature( Feature.SyncPubSub.Default() );
        busConfig.addFeature( Feature.AsynchronousHandlerInvocation.Default() );
        busConfig.addFeature( Feature.AsynchronousMessageDispatch.Default() );
        this.eventBus = new MBassador<>( busConfig );
        //this.eventBus = new MBassador<>( BusConfiguration.SyncAsync() );
        //this.eventBus.subscribe( this );
    }

    // Singleton accessor.
    public static DataPublisher instance ()
    {
        System.out.println( Instant.now().toString() + " Method DataPublisher::instance running." );   // DEBUG logging.
        return singleton;
    }

    public void register ( Object subscriber )
    {
        System.out.println( Instant.now().toString() + " Method DataPublisher::register running." );   // DEBUG logging.
        this.eventBus.subscribe( subscriber ); // The event bus is thread-safe. So hopefully we need no concurrency managament here.
    }

    public void deregister ( Object subscriber )
    {
        System.out.println( Instant.now().toString() + " Method DataPublisher::deregister running." );   // DEBUG logging.
        // Would be unnecessary to deregister if the event bus held weak references.
        // But it might be a good practice anyways for subscribers to deregister when appropriate.
        this.eventBus.unsubscribe( subscriber ); // The event bus is thread-safe. So hopefully we need no concurrency managament here.
    }

    public void publishIfReady ()
    {
        System.out.println( Instant.now().toString() + " Method DataPublisher::publishIfReady running." );   // DEBUG logging.

        // We expect this method to be called repeatedly by a ScheduledExecutorService.
        DataProvider dataProvider = DataProvider.instance();
        Boolean isFresh = dataProvider.checkForFreshData();
        if ( isFresh ) {
            DataEvent dataEvent = dataProvider.data();
            if ( dataEvent != null ) {
                System.out.println( Instant.now().toString() + " Method DataPublisher::publishIfReady…post running." );   // DEBUG logging.
                this.eventBus.publishAsync( dataEvent ); // Ideally this would be an asynchronous dispatching to bus subscribers.
            }
        }
    }

    @Handler
    public void deadEventHandler ( DeadMessage event )
    {
        // A dead event is an event posted but had no subscribers.
        // You may want to subscribe to DeadEvent as a debugging tool to see if your event is being dispatched successfully.
        System.out.println( Instant.now() + " DeadMessage on MBassador event bus : " + event );
    }

}

正在访问数据库

DataPublisher class 使用 DataProvider class 访问数据库。在我们的例子中,我们没有实际访问数据库,而是简单地生成随机数据值。

package com.example.pushvaadinapp;

import java.time.Instant;
import java.util.Random;
import java.util.UUID;

/**
 * Access database to check for fresh data. If fresh data is found, package for
 * delivery. Actually we generate random data as a way to mock database access.
 *
 * @author Basil Bourque
 */
public class DataProvider
{

    // Statics
    private static final DataProvider singleton = new DataProvider();

    // Member vars.
    private DataEvent cachedDataEvent = null;
    private Instant whenLastChecked = null; // When did we last check for fresh data.

    // Other vars.
    private final Random random = new Random();
    private Integer minimum = Integer.valueOf( 1 ); // Pick a random number between 1 and 999.
    private Integer maximum = Integer.valueOf( 999 );

    // Constructor. Private, for simple Singleton pattern.
    private DataProvider ()
    {
        System.out.println( Instant.now().toString() + " Method DataProvider::constructor running." );   // DEBUG logging.
    }

    // Singleton accessor.
    public static DataProvider instance ()
    {
        System.out.println( Instant.now().toString() + " Method DataProvider::instance running." );   // DEBUG logging.
        return singleton;
    }

    public Boolean checkForFreshData ()
    {
        System.out.println( Instant.now().toString() + " Method DataProvider::checkForFreshData running." );   // DEBUG logging.

        synchronized ( this ) {
            // Record when we last checked for fresh data.
            this.whenLastChecked = Instant.now();

            // Mock database access by generating random data.
            UUID dbUuid = java.util.UUID.randomUUID();
            Number dbNumber = this.random.nextInt( ( this.maximum - this.minimum ) + 1 ) + this.minimum;
            Instant dbUpdated = Instant.now();

            // If we have no previous data (first retrieval from database) OR If the retrieved data is different than previous data --> Fresh.
            Boolean isFreshData = ( ( this.cachedDataEvent == null ) ||  ! this.cachedDataEvent.uuid.equals( dbUuid ) );

            if ( isFreshData ) {
                DataEvent freshDataEvent = new DataEvent( dbUuid , dbNumber , dbUpdated );
                // Post fresh data to event bus.
                this.cachedDataEvent = freshDataEvent; // Remember this fresh data for future comparisons.
            }

            return isFreshData;
        }
    }

    public DataEvent data ()
    {
        System.out.println( Instant.now().toString() + " Method DataProvider::data running." );   // DEBUG logging.

        synchronized ( this ) {
            return this.cachedDataEvent;
        }
    }

}

打包数据

DataProvider 打包新数据以传递给其他对象。我们将 DataEvent class 定义为该包。或者,如果您需要传送多组数据或对象而不是一组,则在您的 DataHolder 版本中放置一个 Collection。打包对要显示此新数据的布局或小部件有意义的任何内容。

package com.example.pushvaadinapp;

import java.time.Instant;
import java.util.UUID;

/**
 * Holds data to be published in the UI. In real life, this could be one object
 * or could hold a collection of data objects as might be needed by a chart for
 * example. These objects will be dispatched to subscribers of an MBassador
 * event bus.
 *
 * @author Basil Bourque
 */
public class DataEvent
{

    // Core data values.
    UUID uuid = null;
    Number number = null;
    Instant updated = null;

    // Constructor
    public DataEvent ( UUID uuid , Number number , Instant updated )
    {
        this.uuid = uuid;
        this.number = number;
        this.updated = updated;
    }

    @Override
    public String toString ()
    {
        return "DataEvent{ " + "uuid=" + uuid + " | number=" + number + " | updated=" + updated + " }";
    }

}

分发数据

将新数据打包到 DataEvent 后,DataProvider 将其交给 DataPublisher。因此,下一步是将该数据获取到感兴趣的 Vaadin 布局或小部件以呈现给用户。但是我们如何知道哪些 layouts/widgets 对这些数据感兴趣?我们如何将这些数据传递给他们?

一种可能的方法是 Vaadin 中的 Observer Pattern. We see this pattern in Java Swing as well as Vaadin, such as a ClickListener for a Button。这种模式意味着观察者和被观察者相互了解。这意味着在定义和实现接口方面需要做更多的工作。

事件总线

在我们的例子中,我们不需要数据的生产者 (DataPublisher) 和消费者 (Vaadin layouts/widgets) 相互了解。小部件需要的只是数据,无需与生产者进一步交互。所以我们可以使用不同的方法,事件总线。在事件总线中,一些对象会在有趣的事情发生时发布一个 "event" 对象。当事件对象发布到总线上时,其他对象会注册它们对收到通知的兴趣。发布后,总线通过调用证书将该事件发布给所有注册订阅者在方法中并传递事件。在我们的例子中,将传递 DataEvent 对象。

但是将调用已注册订阅对象的哪个方法?通过 Java 的注释、反射和内省技术的魔力,任何方法都可以被标记为要调用的方法。只需用注释标记所需的方法,然后让总线在发布事件时运行时间找到该方法。

无需自己构建任何此事件总线。在 Java 世界中,我们可以选择事件总线实现。

Google 番石榴事件总线

最著名的可能是 Google Guava EventBus. Google Guava 是 in-house 在 Google 开发的各种实用项目,然后 open-sourced其他人使用。 EventBus 包就是其中一个项目。我们可以使用 Guava EventBus。事实上,我最初确实使用这个库构建了这个例子。但是 Guava EventBus 有一个限制:它持有强引用。

弱引用

当对象注册它们对被通知的兴趣时,任何事件总线都必须通过持有对注册对象的引用来保留这些订阅的列表。理想情况下,这应该是一个 weak reference, meaning that should the subscribing object reach the end of its usefulness and become a candidate for garbage collection,该对象可以这样做。如果事件总线持有强引用,则对象无法进行垃圾回收。弱引用告诉 JVM 我们并不 真正地 关心对象,我们关心一点但不足以坚持保留对象。对于弱引用,事件总线会在尝试通知订阅者新事件之前检查空引用。如果为空,则事件总线可以将该槽放入其 object-tracking 集合中。

您可能认为,作为持有强引用问题的解决方法,您可以让注册的 Vaadin 小部件覆盖 detach 方法。当不再使用 Vaadin 小部件时,您会收到通知,然后您的方法将从事件总线中注销。如果订阅对象从事件总线中取出,则不再有强引用,也不再有问题。但正如 Java 对象方法 finalize is not always called, so too is the Vaadin detach method not always called. See the posting on this thread by Vaadin expert Henri Sara 的详细信息。依赖 detach 可能会导致内存泄漏和其他问题。

MBassador 事件总线

请参阅 my blog post for a discussion of various Java implementations of event bus libraries. Of those I chose MBassador 以了解在此示例应用程序中的使用。它的 存在理由 是弱引用的使用。

UI类

线程之间

要实际更新 Vaadin 布局和小部件的值,有一个大问题。这些小部件 运行 在它们自己的 user-interface-handling 线程中(该用户的主 Servlet 线程)。同时,您的 database-checking 和 data-publishing 以及 event-bus-dispatching 都发生在由执行程序服务管理的后台线程上。 切勿从单独的线程访问或更新 Vaadin 小部件! 此规则绝对重要。更棘手的是,这样做实际上可能在开发过程中起作用。但是,如果您在生产中这样做,您将身处痛苦的世界。

那么我们如何从后台线程中获取数据以传送到主 Servlet 线程中的小部件 运行ning? UI class offers a method just for this purpose: access. You pass a Runnableaccess 方法,Vaadin 安排 Runnable 在主 user-interface 线程上执行。 Easy-peasy.

剩余类

为了结束这个示例应用程序,这里是剩余的 classes。 "MyUI" class 替换了 new Maven archetype for Vaadin 7.3.7 创建的默认项目中的同名文件。

package com.example.pushvaadinapp;

import com.vaadin.annotations.Push;
import com.vaadin.annotations.Theme;
import com.vaadin.annotations.VaadinServletConfiguration;
import com.vaadin.annotations.Widgetset;
import com.vaadin.server.BrowserWindowOpener;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinServlet;
import com.vaadin.ui.Button;
import com.vaadin.ui.Label;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;
import java.time.Instant;
import javax.servlet.annotation.WebServlet;

/**
 * © 2014 Basil Bourque. This source code may be used freely forever by anyone
 * absolving me of any and all responsibility.
 */
@Push
@Theme ( "mytheme" )
@Widgetset ( "com.example.pushvaadinapp.MyAppWidgetset" )
public class MyUI extends UI
{

    Label label = new Label( "Now : " );
    Button button = null;

    @Override
    protected void init ( VaadinRequest vaadinRequest )
    {
        // Prepare widgets.
        this.button = this.makeOpenWindowButton();

        // Arrange widgets in a layout.
        VerticalLayout layout = new VerticalLayout();
        layout.setMargin( Boolean.TRUE );
        layout.setSpacing( Boolean.TRUE );
        layout.addComponent( this.label );
        layout.addComponent( this.button );

        // Put layout in this UI.
        setContent( layout );

        // Start the data feed thread
        new FeederThread().start();
    }

    @WebServlet ( urlPatterns = "/*" , name = "MyUIServlet" , asyncSupported = true )
    @VaadinServletConfiguration ( ui = MyUI.class , productionMode = false )
    public static class MyUIServlet extends VaadinServlet
    {
    }

    public void tellTime ()
    {
        label.setValue( "Now : " + Instant.now().toString() ); // If before Java 8, use: new java.util.Date(). Or better, Joda-Time.
    }

    class FeederThread extends Thread
    {

        // This Thread class is merely a simple test to verify that Push works.
        // This Thread class is not the intended example.
        // A ScheduledExecutorService is in WebAppListener class is the intended example.
        int count = 0;

        @Override
        public void run ()
        {
            try {
                // Update the data for a while
                while ( count < 100 ) {
                    Thread.sleep( 1000 );

                    access( new Runnable() // Special 'access' method on UI object, for inter-thread communication.
                    {
                        @Override
                        public void run ()
                        {
                            count ++;
                            tellTime();
                        }
                    } );
                }

                // Inform that we have stopped running
                access( new Runnable()
                {
                    @Override
                    public void run ()
                    {
                        label.setValue( "Done. No more telling time." );
                    }
                } );
            } catch ( InterruptedException e ) {
                e.printStackTrace();
            }
        }
    }

    Button makeOpenWindowButton ()
    {
        // Create a button that opens a new browser window.
        BrowserWindowOpener opener = new BrowserWindowOpener( DataUI.class );
        opener.setFeatures( "height=300,width=440,resizable=yes,scrollbars=no" );

        // Attach it to a button
        Button button = new Button( "Open data window" );
        opener.extend( button );

        return button;
    }
}

"DataUI" 和 "DataLayout" 完成此示例 Vaadin 应用程序中的 7 个 .java 文件。

package com.example.pushvaadinapp;

import com.vaadin.annotations.Push;
import com.vaadin.annotations.Theme;
import com.vaadin.annotations.Widgetset;
import com.vaadin.server.VaadinRequest;
import com.vaadin.ui.UI;
import java.time.Instant;
import net.engio.mbassy.listener.Handler;

@Push
@Theme ( "mytheme" )
@Widgetset ( "com.example.pushvaadinapp.MyAppWidgetset" )
public class DataUI extends UI
{

    // Member vars.
    DataLayout layout;

    @Override
    protected void init ( VaadinRequest request )
    {
        System.out.println( Instant.now().toString() + " Method DataUI::init running." );   // DEBUG logging.

        // Initialize window.
        this.getPage().setTitle( "Database Display" );
        // Content.
        this.layout = new DataLayout();
        this.setContent( this.layout );

        DataPublisher.instance().register( this ); // Sign-up for notification of fresh data delivery.
    }

    @Handler
    public void update ( DataEvent event )
    {
        System.out.println( Instant.now().toString() + " Method DataUI::update (@Subscribe) running." );   // DEBUG logging.

        // We expect to be given a DataEvent item.
        // In a real app, we might need to retrieve data (such as a Collection) from within this event object.
        this.access( () -> {
            this.layout.update( event ); // Crucial that go through the UI:access method when updating the user interface (widgets) from another thread.
        } );
    }

}

……和……

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.example.pushvaadinapp;

import com.vaadin.ui.TextField;
import com.vaadin.ui.VerticalLayout;
import java.time.Instant;

/**
 *
 * @author brainydeveloper
 */
public class DataLayout extends VerticalLayout
{

    TextField uuidField;
    TextField numericField;
    TextField updatedField;
    TextField whenCheckedField;

    // Constructor
    public DataLayout ()
    {
        System.out.println( Instant.now().toString() + " Method DataLayout::constructor running." );   // DEBUG logging.

        // Configure layout.
        this.setMargin( Boolean.TRUE );
        this.setSpacing( Boolean.TRUE );

        // Prepare widgets.
        this.uuidField = new TextField( "UUID : " );
        this.uuidField.setWidth( 22 , Unit.EM );
        this.uuidField.setReadOnly( true );

        this.numericField = new TextField( "Number : " );
        this.numericField.setWidth( 22 , Unit.EM );
        this.numericField.setReadOnly( true );

        this.updatedField = new TextField( "Updated : " );
        this.updatedField.setValue( "<Content will update automatically>" );
        this.updatedField.setWidth( 22 , Unit.EM );
        this.updatedField.setReadOnly( true );

        // Arrange widgets.
        this.addComponent( this.uuidField );
        this.addComponent( this.numericField );
        this.addComponent( this.updatedField );
    }

    public void update ( DataEvent dataHolder )
    {
        System.out.println( Instant.now().toString() + " Method DataLayout::update (via @Subscribe on UI) running." );   // DEBUG logging.

        // Stuff data values into fields. For simplicity in this example app, using String directly rather than Vaadin converters.
        this.uuidField.setReadOnly( false );
        this.uuidField.setValue( dataHolder.uuid.toString() );
        this.uuidField.setReadOnly( true );

        this.numericField.setReadOnly( false );
        this.numericField.setValue( dataHolder.number.toString() );
        this.numericField.setReadOnly( true );

        this.updatedField.setReadOnly( false );
        this.updatedField.setValue( dataHolder.updated.toString() );
        this.updatedField.setReadOnly( true );
    }

}