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

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

我已经阅读了问题和答案, 推送Vaadin 7应用程序(“@Push”)的最小示例 。 现在我需要一个更健壮的现实例子。 首先,我知道在Servlet环境中拥有一个永无止境的Thread并不是一个好主意。

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

完全工作的例子

您将在下面找到几个类的代码。 他们一起使用新的内置Pushfunction为Vaadin 7.3.8应用程序提供了一个完整的示例,可以同时向任意数量的用户发布一组数据。 我们通过随机生成一组数据值来模拟检查数据库中的新数据。

运行此示例应用程序时,会出现一个窗口,显示当前时间和按钮。 时间每秒更新一次,持续一百次。

示例应用程序中第一个窗口的屏幕截图

这个时间更新不是真实的例子。 时间更新器有两个用途:

  • 其简单的代码检查在Vaadin应用程序,Web服务器和Web浏览器中正确配置了Push。
  • 按照The Book of Vaadin的Server Push部分中给出的示例代码进行操作。 我们的时间更新器几乎完全取消了这个例子,除了他们每分钟更新一个图表 ,我们更新了一段文字。

要查看此应用程序的真实用途示例,请单击/点击“打开数据窗口”按钮。 将打开第二个窗口以显示三个文本字段。 每个字段都包含一个随机生成的值,我们假装它来自数据库查询。

数据库显示窗口的屏幕截图,包含三个文本字段

这样做有点工作,需要几件。 我们来看看那些碎片。

此示例应用程序设计中的各种类和对象的图表

在当前版本的Vaadin 7.3.8中,不需要插件或插件来启用Push技术 。 甚至推送相关的.jar文件也与Vaadin捆绑在一起。

有关详细信息,请参阅Book of Vaadin 。 但实际上您需要做的就是将@Push注释添加到UI的子类中。

使用最新版本的Servlet容器和Web服务器。 Push是相对较新的,并且实现正在发展,特别是对于WebSocket变种。 例如,如果使用Tomcat,请确保使用Tomcat 7或8的最新更新。

定期检查新数据

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

永远不会结束的线程不是在Servlet环境中执行此操作的最佳方式,因为线程不会在取消部署Web应用程序时结束,也不会在Servlet包含关闭时终止。 线程将继续在JVM中运行,浪费资源,导致内存泄漏和其他问题。

Web App启动/关闭挂钩

理想情况下,我们希望在Web应用程序启动(部署)以及Web应用程序关闭(或取消部署)时获得通知。 通知后,我们可以启动或中断该数据库查询线程。 幸运的是,作为每个Servlet容器的一部分提供了这样的钩子。 Servlet规范要求容器支持ServletContextListener接口。

我们可以编写一个实现此接口的类。 当我们的Web应用程序(我们的Vaadin应用程序)被部署时,我们的侦听器类’ contextInitialized被调用。 取消部署时,将contextDestroyed方法。

执行人服务

从这个钩子我们可以启动一个线程。 但还有更好的方法。 Java配备了ScheduledExecutorService 。 这个类有一个Threads池,可以避免实例化和启动线程的开销。 您可以将一个或多个任务( Runnable )分配给执行程序,以便定期运行。

Web App Listener

这是我们的Web应用程序监听器类,使用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

在该代码中,您将看到定期调用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 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类使用DataProvider类来访问数据库。 在我们的例子中,我们不是实际访问数据库,而是简单地生成随机数据值。

 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类定义为该包。 或者,如果您需要提供多组数据或对象而不是单个数据,则将Collection放在您的DataHolder版本中。 打包对想要显示此新数据的布局或窗口小部件有意义的内容。

 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布局或小部件以呈现给用户。 但是我们如何知道哪些布局/小部件对这些数据感兴趣? 我们如何向他们提供这些数据?

一种可能的方法是观察者模式 。 我们在Java Swing以及Vaadin中看到了这种模式,例如Vaadin中ButtonClickListener 。 这种模式意味着观察者和观察者彼此了解。 这意味着更多的工作在定义和实现接口。

活动巴士

在我们的例子中,我们不需要数据生成器(DataPublisher)和消费者(Vaadin布局/小部件)来了解彼此。 所有小部件都需要的是数据,而无需与生产者进一步交互。 所以我们可以使用不同的方法,即事件总线。 在事件总线中,一些对象在发生有趣事件时发布“事件”对象。 当事件对象被发布到总线时,其他对象注册它们被通知的兴趣。 发布时,总线通过调用某种方法并传递该事件,将该事件发布给所有注册用户。 在我们的例子中,将传递DataEvent对象。

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

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

Google Guava EventBus

最着名的可能是Google Guava EventBus 。 Google Guava是一系列在Google内部开发的各种实用程序项目,然后开源供其他人使用。 EventBus包是其中一个项目。 我们可以使用Guava EventBus。 事实上,我最初使用此库构建此示例。 但是Guava EventBus有一个限制:它有很强的参考价值。

弱参考

当对象注册其被通知的兴趣时,任何事件总线必须通过保持对注册对象的引用来保留这些订阅的列表。 理想情况下,这应该是一个弱引用 ,这意味着如果订阅对象达到其有用性的终点并成为垃圾收集的候选者,那么该对象可以这样做。 如果事件总线拥有强引用,则该对象无法进行垃圾回收。 弱引用告诉JVM我们并不真正关心对象,我们关心一点但不足以坚持保留对象。 使用弱引用时,事件总线在尝试通知订户新事件之前检查空引用。 如果为null,则事件总线可以在其对象跟踪集合中删除该插槽。

您可能认为,作为持有强引用问题的解决方法,您可以使用已注册的Vaadin小部件覆盖detach方法。 当Vaadin小部件不再使用时,您将收到通知,然后您的方法将从事件总线取消注册。 如果订阅对象被从事件总线中取出,则没有更强的引用而且没有更多问题。 但正如Java Object方法finalize并不总是被调用一样 ,Vaadin detach方法也不总是被调用。 有关详细信息,请参阅Vaadin专家Henri Sara在此主题上发布的post。 依赖detach可能导致内存泄漏和其他问题。

MB Ambassador活动巴士

有关事件总线库的各种Java实现的讨论,请参阅我的博客文章 。 在那些我选择MBassador在这个示例应用程序中使用。 其存在的理由是使用弱引用。

UI类

线程之间

要实际更新Vaadin布局和小部件的值,有一个很大的问题。 这些小部件在他们自己的用户界面处理线程(该用户的主要Servlet线程)中运行。 同时,您的数据库检查和数据发布以及事件总线调度都发生在由执行程序服务管理的后台线程上。 永远不要从单独的线程访问或更新Vaadin小部件! 这条规则绝对至关重要。 为了使它更加棘手,这样做可能在开发过程中实际工作。 但如果你在生产中这样做,你将处于一个受伤的世界。

那么我们如何将后台线程中的数据传递到主Servlet线程中运行的小部件? UI类为此提供了一种方法: access 。 您将Runnable传递给access方法,并且Vaadin计划在主用户界面线程上执行Runnable。 十分简单。

剩下的课程

要结束此示例应用程序,以下是其余的类。 “MyUI”类替换了由Vaadin 7.3.7的新Maven原型创建的默认项目中的同名文件。

 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( "" ); 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 ); } }