使用Push in Vaadin 7应用程序在多个客户端中显示相同的数据 [英] Displaying Same Data Aross Multiple Clients Using Push in Vaadin 7 app

查看:162
本文介绍了使用Push in Vaadin 7应用程序在多个客户端中显示相同的数据的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



我已阅读问答,的nofollow noreferrer>服务器推送部分。除了每次更新。甚至推送相关的.jar文件也与Vaadin捆绑在一起。



参见 Book of Vaadin 了解详情。但实际上你需要做的就是添加 @Push UI <的子类的 注释/ a>。



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



定期检查新数据



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



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



Web App启动/关闭挂钩



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



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



执行者服务



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



Web App Listener



这是我们的网络应用程序监听器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;

/ **
*对此Web应用程序启动/部署和关闭做出反应。
*
* @author Basil Bourque
* /
@WebListener
公共类WebAppListener实现ServletContextListener
{

ScheduledExecutorService scheduledExecutorService ;
ScheduledFuture<?> dataPublishHandle;

//构造函数。
public WebAppListener()
{
this.scheduledExecutorService = Executors.newScheduledThreadPool(7);
}

//我们的网络应用程序(Vaadin应用程序)正在启动。
public void contextInitialized(ServletContextEvent servletContextEvent)
{
System.out.println(Instant.now()。toString()+Method WebAppListener :: contextInitialized running。); // 调试日志。

//在这个例子中,我们不需要ServletContex。但是,仅供参考,您可能会发现它很有用。
ServletContext ctx = servletContextEvent.getServletContext();
System.out.println(初始化Web应用程序上下文。); // INFO日志记录
System.out.println(TRACE Servlet上下文名称:+ ctx.getServletContextName());
System.out.println(TRACE服务器信息:+ ctx.getServerInfo());

//安排定期发布新数据。使用Java 8的Lambda语法传递匿名Runnable。
this.dataPublishHandle = this.scheduledExecutorService.scheduleAtFixedRate(() - > {
System.out.println(Instant.now()。toString( )+方法WebAppListener :: contextDestroyed-> Runnable running。------------------------------); // DEBUG logging。
DataPublisher.instance()。publishIfReady();
},5,5,TimeUnit.SECONDS);
}

//我们的网络应用程序(Vaadin应用程序)正在关闭。
public void contextDestroyed(ServletContextEvent servletContextEvent)
{
System.out.println(Instant.now()。toString()+Method WebAppListener :: contextDestroyed running。); // 调试日志。

System.out.println(Web应用程序上下文被破坏。); // INFO日志记录
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;

/ **
*用于注册对象(主要是用户界面组件)的单身人士感兴趣
*定期通知最新数据。
*
*与DataProvider单例串联工作,该单例与数据库
*交互以查找新数据。
*
*这两个单身人士,DataPublisher& DataProvider可以组合成
* one。但是为了进行测试,将它们分开可能很方便。
*
* @author Basil Bourque
* /
公共类DataPublisher
{

//静态
私有静态最终DataPublisher singleton = new DataPublisher();

//会员vars。
私人最终大使< DataEvent> eventBus;

//构造函数。私有,简单的单身人士模式。
private DataPublisher()
{
System.out.println(Instant.now()。toString()+Method DataPublisher :: constructor running。); // 调试日志。
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);
}

//单身访问者。
public static DataPublisher instance()
{
System.out.println(Instant.now()。toString()+Method DataPublisher :: instance running。); // 调试日志。
返回单身人士;
}

public void register(Object subscriber)
{
System.out.println(Instant.now()。toString()+Method DataPublisher ::注册运行。); // 调试日志。
this.eventBus.subscribe(subscriber); //事件总线是线程安全的。所以希望我们这里不需要并发管理。
}

public void deregister(Object subscriber)
{
System.out.println(Instant.now()。toString()+Method DataPublisher ::取消注册运行。); // 调试日志。
//如果事件总线保持弱引用,则无需取消注册。
//但是,对于订阅者来说,在适当的时候取消注册可能是一种很好的做法。
this.eventBus.unsubscribe(subscriber); //事件总线是线程安全的。所以希望我们这里不需要并发管理。
}

public void publishIfReady()
{
System.out.println(Instant.now()。toString()+Method DataPublisher :: publishIfReady running 。); // 调试日志。

//我们希望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。); // 调试日志。
this.eventBus.publishAsync(dataEvent); //理想情况下,这将是对总线订户的异步调度。
}
}
}

@Handler
public void deadEventHandler(DeadMessage事件)
{
//死亡事件是一个发布但没有订阅者的活动。
//您可能希望订阅DeadEvent作为调试工具,以查看您的事件是否正在成功调度。
System.out.println(Instant.now()+MB Ambassador事件总线上的DeadMessage:+事件);
}

}



访问数据库



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

  package com.example.pushvaadinapp; 

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

/ **
*访问数据库以检查新数据。如果找到新鲜数据,请输入
*包裹。实际上,我们生成随机数据作为模拟数据库访问的方式。
*
* @author Basil Bourque
* /
公共类DataProvider
{

//静态
私有静态最终DataProvider singleton = new DataProvider();

//会员vars。
private DataEvent cachedDataEvent = null;
private Instant WhenLastChecked = null; //我们什么时候检查新数据

//其他变量。
private final随机随机= new Random();
private Integer minimum = Integer.valueOf(1); //选择1到999之间的随机数。
private Integer maximum = Integer.valueOf(999);

//构造函数。私有,简单的单身人士模式。
private DataProvider()
{
System.out.println(Instant.now()。toString()+Method DataProvider :: constructor running。); // 调试日志。
}

//单身访问者。
public static DataProvider instance()
{
System.out.println(Instant.now()。toString()+Method DataProvider :: instance running。); // 调试日志。
返回单身人士;
}

public Boolean checkForFreshData()
{
System.out.println(Instant.now()。toString()+Method DataProvider :: checkForFreshData running 。); // 调试日志。

synchronized(this){
//我们上次检查新数据时的记录。
this.whenLastChecked = Instant.now();

//通过生成随机数据来模拟数据库访问。
UUID dbUuid = java.util.UUID.randomUUID();
数字dbNumber = this.random.nextInt((this.maximum - this.minimum)+ 1)+ this.minimum;
即时dbUpdated = Instant.now();

//如果我们没有先前的数据(首先从数据库中检索)或者如果检索的数据与先前的数据不同 - >新鲜。
布尔值isFreshData =((this.cachedDataEvent == null)||!this.cachedDataEvent.uuid.equals(dbUuid));

if(isFreshData){
DataEvent freshDataEvent = new DataEvent(dbUuid,dbNumber,dbUpdated);
//将新数据发布到活动总线。
this.cachedDataEvent = freshDataEvent; //记住这些新数据,以便将来进行比较。
}

返回isFreshData;
}
}

public DataEvent data()
{
System.out.println(Instant.now()。toString()+Method DataProvider :: data running。); // 调试日志。

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

}



包装数据



DataProvider打包新数据以便传送到其他对象。我们将DataEvent类定义为该包。或者,如果您需要提供多组数据或对象而不是单个数据,则将Collection放在您的DataHolder版本中。打包想要显示这些新数据的布局或小部件。

  package com.example.pushvaadinapp; 

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

/ **
*保留要在UI中发布的数据。在现实生活中,这可能是一个对象
*或者可以保存一个数据对象的集合,如
*示例的图表可能需要的那样。这些对象将被发送给MBassador
*活动总线的订户。
*
* @author Basil Bourque
* /
公共类DataEvent
{

//核心数据值。
UUID uuid = null;
Number number = null;
即时更新= null;

//构造函数
public DataEvent(UUID uuid,编号,即时更新)
{
this.uuid = uuid;
this.number = number;
this.updated =已更新;
}

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

}



分发数据



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



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



事件总线



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



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



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



Google Guava EventBus



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



弱引用



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



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



MB Ambassador活动总线



请参阅我的博文讨论事件总线库的各种Java实现。我选择 MBassador 用于此示例应用程序。它的 raison d'être是使用弱引用。



UI类



线程之间



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



那么我们如何将后台线程中的数据传递到运行的小部件中主要的Servlet线程? UI 类提供了一种方法,用于此目的: access 。您将 Runnable 传递给访问方法,Vaadin安排Runnable在主用户界面线程上执行。 Easy-peasy。



剩余课程



要结束此示例应用程序,以下是其余课程。 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。这个源代码可以被任何人永久免费使用
*免除我的任何和所有责任。
* /
@Push
@Theme(mytheme)
@Widgetset(com.example.pushvaadinapp.MyAppWidgetset)
公共类MyUI扩展UI
{

标签标签=新标签(现在:);
Button button = null;

@Override
protected void init(VaadinRequest vaadinRequest)
{
//准备小部件。
this.button = this.makeOpenWindowButton();

//在布局中排列小部件。
VerticalLayout layout = new VerticalLayout();
layout.setMargin(Boolean.TRUE);
layout.setSpacing(Boolean.TRUE);
layout.addComponent(this.label);
layout.addComponent(this.button);

//在此UI中放置布局。
setContent(layout);

//启动数据Feed线程
new FeederThread()。start();
}

@WebServlet(urlPatterns =/ *,name =MyUIServlet,asyncSupported = true)
@VaadinServletConfiguration(ui = MyUI.class,productionMode = false)
公共静态类MyUIServlet扩展VaadinServlet
{
}

public void tellTime()
{
label.setValue(Now: + Instant.now()。toString()); //如果在Java 8之前,请使用:new java.util.Date()。或者更好,Joda-Time。
}

类FeederThread扩展线程
{

//这个Thread类只是一个简单的测试来验证Push的工作原理。
//这个Thread类不是预期的例子。
// WebAppListener类中的ScheduledExecutorService是预期的示例。
int count = 0;

@Override
public void run()
{
try {
//更新数据
while(count< ; 100){
Thread.sleep(1000);

access(new Runnable()// UI对象上的特殊'access'方法,用于线程间通信。
{
@Override
public void run( )
{
count ++;
tellTime();
}
});
}

//通知我们已停止运行
访问权限(新的Runnable()
{
@Override
public void run( )
{
label.setValue(完成。不再告诉时间。);
}
});
} catch(InterruptedException e){
e.printStackTrace();
}
}
}

按钮makeOpenWindowButton()
{
//创建一个按钮,打开一个新的浏览器窗口。
BrowserWindowOpener opener = new BrowserWindowOpener(DataUI.class);
opener.setFeatures(height = 300,width = 440,resizable = yes,scrollbars = no);

//将它附加到按钮
按钮按钮=新按钮(打开数据窗口);
opener.extend(按钮);

返回按钮;
}
}

DataUI和DataLayout完成7。此示例中的java文件Vaadin app。

  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)
公共类DataUI扩展UI
{

//会员vars。
DataLayout布局;

@Override
protected void init(VaadinRequest request)
{
System.out.println(Instant.now()。toString()+Method DataUI: :init running。); // 调试日志。

//初始化窗口。
this.getPage()。setTitle(Database Display);
//内容。
this.layout = new DataLayout();
this.setContent(this.layout);

DataPublisher.instance()。register(this); //注册新鲜数据传递通知。
}

@Handler
public void update(DataEvent event)
{
System.out.println(Instant.now()。toString() +方法DataUI :: update(@Subscribe)正在运行。); // 调试日志。

//我们希望获得一个DataEvent项目。
//在真实的应用程序中,我们可能需要从此事件对象中检索数据(例如Collection)。
this.access( () -> {
this.layout.update( event ); // Crucial that go through the UI:access method when updating the user interface (widgets) from another thread.
} );
}

}

…and…

/* 
* 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 );
}

}


I want to share the same set of data to multiple clients. I need to use Push to automatically update their view on screen.

I have read the Question and Answer, Minimal example of Push in Vaadin 7 app ("@Push"). Now I need a more robust realistic example. For one thing, I know having a never-ending Thread is not a good idea in a Servlet environment.

And I don't want each user having their own Thread, each hitting the database on their own. Seems more logical to have one thread alone checking for fresh data in the database. When found, that thread should publish the fresh data to all the users’ UI/Layouts waiting for updates.

解决方案

Fully-Working Example

Below you will find the code for several classes. Together they make a fully working example of a Vaadin 7.3.8 app using the new built-in Push features to publish a single set of data simultaneously to any number of users. We simulate checking the database for fresh data by randomly generating a set of data values.

When you run this example app, a window appears displaying the current time along with a button. The time updates once per second for a hundred times.

This time-updating is not the true example. The time-updater serves two other purposes:

  • Its simple code checks that Push is configured properly in your Vaadin app, web server, and web browser.
  • Follows the example code given in the 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 every minute, we update a piece of text.

To see the true intended example of this app, click/tap the "Open data window" button. A second window opens to show three text fields. Each field contains a randomly-generated value which we pretend came from a database query.

Doing this is a bit of work, requiring several pieces. Let's go over those pieces.

Push

In the current version of Vaadin 7.3.8, there is no need for plugins or add-ons to enable Push technology. Even the Push-related .jar file is bundled with Vaadin.

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

Use recent versions of your Servlet container and web server. Push is relatively new, and implementations are evolving, especially for the WebSocket variety. For example, if using Tomcat be sure to use the latest updates to Tomcat 7 or 8.

Periodically Checking For Fresh Data

We must have some way to repeatedly query the database for fresh data.

A never-ending Thread is not the best way to do that in a Servlet environment, as the Thread will not end when the web app is undeployed nor when the Servlet contain shutsdown. The Thread will continue to run in the JVM, wasting resources, causing a memory leak and other problems.

Web App Startup/Shutdown Hooks

Ideally we want to be informed when the web app starts up (deployed) and when the web app shuts down (or undeployed). When so informed, we could launch or interrupt that database-querying thread. Fortunately, there is such a hook provided as part of every Servlet container. The Servlet spec requires a container support the ServletContextListener interface.

We can write a class that implements this interface. When our web app (our Vaadin app) is deployed, our listener class’ contextInitialized is called. When undeployed, the contextDestroyed method is called.

Executor Service

From this hook we could start up a Thread. But there is a better way. Java comes equipped with the 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) to the executor, to be run periodically.

Web App Listener

Here is our web app listener class, using the Lambda syntax available in Java 8.

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

In that code you’ll see the DataPublisher instance is called periodically, asking it to check for fresh data, and if found deliver to all the interested Vaadin layouts or widgets.

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

}

Accessing Database

That DataPublisher class uses a DataProvider class to access the database. In our case, instead of actually accessing a database we simply generate random data values.

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;
        }
    }

}

Packaging Data

The DataProvider packages fresh data for delivery to other objects. We define a DataEvent class to be that package. Alternatively, if you need to deliver multiple sets of data or objects rather than a single, then place a Collection in your version of DataHolder. Package up whatever makes sense for the layout or widget that wants to display this fresh data.

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 + " }";
    }

}

Distributing Data

Having packaged up fresh data into a DataEvent, the DataProvider hands that off to the DataPublisher. So the next step is getting that data to the interested Vaadin layouts or widgets for presentation to the user. But how do we know which layouts/widgets are interested in this data? And how do we deliver this data to them?

One possible way is the Observer Pattern. We see this pattern in Java Swing as well as Vaadin, such as a ClickListener for a Button in Vaadin. This pattern means the observer and observed know about each other. And it means more work in defining and implementing interfaces.

Event Bus

In our case, we do not need the producer of the data (DataPublisher) and the consumers (the Vaadin layouts/widgets) to know about each other. All the widgets want is the data, without any need for further interaction with the producer. So we can use a different approach, an event bus. In an event bus some objects publish an "event" object when something interesting occurs. Other objects register their interest in being notified when an event object is posted to the bus. When posted, the bus publishes that event to all the registered subscribers by calling a certain method and passing the event. In our case, the DataEvent object will be passed.

But which method on the registered subscribing objects will be invoked? Through the magic of Java’s annotation, reflection, and introspection technologies, any method can be tagged as the one to be called. Merely tag the desired method with an annotation, then let the bus find that method at runtime when publishing an event.

No need to build any of this event bus yourself. In the Java world, we have a choice of event bus implementations.

Google Guava EventBus

The most well known is probably the Google Guava EventBus. Google Guava is a bunch of various utility projects developed in-house at Google and then open-sourced for others to use. The EventBus package is one of those projects. We could use Guava EventBus. Indeed I did originally build this example using this library. But Guava EventBus has one limitation: It holds strong references.

Weak References

When objects register their interest in being notified, any event bus must keep a list of those subscriptions by holding a reference to the registering object. Ideally this should be a weak reference, meaning that should the subscribing object reach the end of its usefulness and become a candidate for garbage collection, that object may do so. If the event bus holds a strong reference, the object cannot proceed to garbage collection. A weak reference tells the JVM that we do not really care about the object, we care a little but not enough to insist the object be retained. With a weak reference, the event bus checks for a null reference before attempting to notify the subscriber of a new event. If null, the event bus can drop that slot in its object-tracking collection.

You might think that as a workaround for the problem of holding strong references you could have your registered Vaadin widgets override the detach method. You would be informed when that Vaadin widget is no longer is use, then your method would deregister from the event bus. If the subscribing object is taken out of the event bus, then no more strong reference and no more problem. But just as the Java Object method 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 for details. Relying on detach could result in memory leaks and other problems.

MBassador Event Bus

See my blog post for a discussion of various Java implementations of event bus libraries. Of those I chose MBassador for use in this example app. Its raison d’être is the use of weak references.

UI Classes

Between Threads

To actually update the values of the Vaadin layouts & widgets, there is one big catch. Those widgets run in their own user-interface-handling thread (the main Servlet thread for this user). Meanwhile, your database-checking and data-publishing and event-bus-dispatching are all happening on a background thread managed by the executor service. Never access or update Vaadin widgets from a separate thread! This rule is absolutely critical. To make it even trickier, doing so might actually work during development. But you will be in a world of hurt if you do so in production.

So how do we get the data from the background threads to be communicated into the widgets running in the main Servlet thread? The UI class offers a method just for this purpose: access. You pass a Runnable to the access method, and Vaadin schedules that Runnable to be executed on the main user-interface thread. Easy-peasy.

Remaining Classes

To wrap up this example app, here are the remaining classes. The "MyUI" class replaces that file of the same name in a default project created by the 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" and "DataLayout" complete the 7 .java files in this example Vaadin app.

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.
        } );
    }

}

…and…

/*
 * 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 );
    }

}

这篇关于使用Push in Vaadin 7应用程序在多个客户端中显示相同的数据的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆