完整工作示例
下面您将找到几个类的代码。他们共同制作了一个完整的 Vaadin 7.3.8 应用程序示例,使用新的内置推送功能向任意数量的用户同时发布一组数据。我们通过随机生成一组数据值来模拟检查数据库中的新数据。
当您运行此示例应用程序时,会出现一个窗口,其中显示当前时间以及一个按钮。时间每秒更新一次,共一百次。
本次更新时间为not真实的例子。时间更新器还有另外两个用途:
- 它的简单代码会检查您的 Vaadin 应用程序、Web 服务器和 Web 浏览器中是否正确配置了 Push。
- 遵循中给出的示例代码服务器推送部分 https://vaadin.com/book/vaadin7/-/page/advanced.push.html of 瓦丁之书 https://vaadin.com/book。我们这里的时间更新程序几乎完全来自该示例,除了他们更新chart https://vaadin.com/add-ons/charts每分钟,我们都会更新一段文字。
要查看此应用程序的真实示例,请单击/点击“打开数据窗口”按钮。将打开第二个窗口,显示三个文本字段。每个字段都包含一个随机生成的值,我们假设该值来自数据库查询。
这样做需要一些工作,需要几部分。让我们回顾一下这些片段。
Push
在当前版本的 Vaadin 7.3.8 中,无需插件或附加组件即可启用推送技术 https://en.wikipedia.org/wiki/Push_technology。甚至与 Push 相关的 .jar 文件也与 Vaadin 捆绑在一起。
See the 瓦丁之书 https://vaadin.com/book/vaadin7/-/page/advanced.push.html了解详情。但实际上您需要做的就是添加@Push https://vaadin.com/api/com/vaadin/annotations/Push.html您的子类的注释UI https://vaadin.com/api/com/vaadin/ui/UI.html.
使用最新版本的 Servlet 容器和 Web 服务器。推送相对较新,并且实现正在不断发展,特别是对于WebSocket https://en.wikipedia.org/wiki/WebSocket种类。例如,如果使用 Tomcat,请确保使用 Tomcat 7 或 8 的最新更新。
定期检查新数据
我们必须有某种方法来重复查询数据库以获取新数据。
永无止境的线程并不是在 Servlet 环境中执行此操作的最佳方法,因为当 Web 应用程序取消部署或 Servlet 包含关闭时,线程不会结束。 Thread会继续在JVM中运行,浪费资源,导致内存泄漏等问题。
Web 应用程序启动/关闭挂钩
理想情况下,我们希望在 Web 应用程序启动(部署)和 Web 应用程序关闭(或取消部署)时收到通知。当得知这一情况后,我们可以启动或中断该数据库查询线程。幸运的是,每个 Servlet 容器都提供了这样一个钩子。这Servlet 规范 https://www.jcp.org/en/jsr/detail?id=340需要一个容器支持ServletContextListener http://docs.oracle.com/javaee/7/api/javax/servlet/ServletContextListener.html界面。
我们可以编写一个实现这个接口的类。当我们的 Web 应用程序(我们的 Vaadin 应用程序)部署时,我们的监听器类’contextInitialized http://docs.oracle.com/javaee/7/api/javax/servlet/ServletContextListener.html#contextInitialized(javax.servlet.ServletContextEvent)叫做。取消部署时,contextDestroyed http://docs.oracle.com/javaee/7/api/javax/servlet/ServletContextListener.html#contextDestroyed(javax.servlet.ServletContextEvent)方法被调用。
执行人服务
从这个钩子我们可以启动一个线程。但还有更好的方法。 Java 配备了ScheduledExecutorService http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ScheduledExecutorService.html。该类有一个可供使用的线程池,以避免实例化和启动线程的开销。您可以分配一项或多项任务(Runnable http://docs.oracle.com/javase/8/docs/api/java/lang/Runnable.html) 给执行者,定期运行。
网络应用监听器
这是我们的 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 实例,要求它检查新数据,如果找到则将其传送到所有感兴趣的 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 类使用 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 布局或小部件以呈现给用户。但是我们如何知道哪些布局/小部件对此数据感兴趣?我们如何将这些数据传递给他们?
一种可能的方法是观察者模式 https://en.wikipedia.org/wiki/Observer_pattern。我们在 Java Swing 和 Vaadin 中看到了这种模式,例如ClickListener https://vaadin.com/api/com/vaadin/ui/Button.ClickListener.html for a Button https://vaadin.com/api/com/vaadin/ui/Button.html在瓦丁。这种模式意味着观察者和被观察者彼此了解。这意味着在定义和实现接口方面需要做更多的工作。
活动总线
在我们的例子中,我们不需要数据的生产者(DataPublisher)和消费者(Vaadin 布局/小部件)相互了解。所有小部件想要的只是数据,而不需要与生产者进行进一步的交互。因此我们可以使用不同的方法,即事件总线。在事件总线中,当发生有趣的事情时,某些对象会发布“事件”对象。当事件对象被发布到总线时,其他对象注册了它们希望得到通知的兴趣。发布后,总线通过调用特定方法并传递事件来将该事件发布给所有注册的订阅者。在我们的例子中,将传递 DataEvent 对象。
但是注册的订阅对象上的哪个方法将被调用呢?通过 Java 注释、反射和内省技术的魔力,任何方法都可以被标记为要调用的方法。只需用注释标记所需的方法,然后让总线在发布事件时在运行时找到该方法。
无需自己构建任何此事件总线。在 Java 世界中,我们可以选择事件总线实现。
谷歌番石榴EventBus
最知名的可能是 Google GuavaEventBus https://code.google.com/p/guava-libraries/wiki/EventBusExplained. 谷歌番石榴 https://github.com/google/guava是 Google 内部开发的一系列各种实用项目,然后开源供其他人使用。 EventBus 包就是这些项目之一。我们可以使用 Guava EventBus。事实上,我最初确实使用这个库构建了这个示例。但 Guava EventBus 有一个限制:它拥有强引用。
弱引用
当对象注册其对被通知的兴趣时,任何事件总线都必须通过保存对注册对象的引用来保留这些订阅的列表。理想情况下,这应该是一个弱引用 https://en.wikipedia.org/wiki/Weak_reference,这意味着订阅对象应该达到其用处并成为候选者垃圾收集 https://en.wikipedia.org/wiki/Garbage_collection_(computer_science),该对象可能会这样做。如果事件总线持有强引用,则对象无法继续进行垃圾回收。弱引用告诉 JVM 我们不这样做really关心对象,我们关心一点但不足以坚持保留该对象。使用弱引用时,事件总线会在尝试向订阅者通知新事件之前检查是否存在空引用。如果为 null,则事件总线可以将该槽删除到其对象跟踪集合中。
您可能认为,作为持有强引用问题的解决方法,您可以让注册的 Vaadin 小部件覆盖detach
方法。当 Vaadin 小部件不再使用时,您会收到通知,然后您的方法将从事件总线中取消注册。如果将订阅对象从事件总线中取出,则不再有强引用,也不再有问题。但就像Java对象方法一样finalize并不总是被称为 https://stackoverflow.com/q/2506488/642706,瓦丁也是如此detach
方法并不总是被调用。请参阅上的帖子这个线程 https://vaadin.com/forum#!/thread/1409950作者:Vaadin 专家亨利·萨拉 https://vaadin.com/web/hesara/home了解详情。依靠detach
可能会导致内存泄漏和其他问题。
Massador 活动巴士
See 我的博文 http://crafted-software.blogspot.com/2015/01/event-bus-for-java.html有关事件总线库的各种 Java 实现的讨论。我选择的那些大使 https://github.com/bennidi/mbassador用于本示例应用程序。它是存在的理由就是弱引用的使用。
用户界面类
线程之间
要实际更新 Vaadin 布局和小部件的值,有一个大问题。这些小部件在它们自己的用户界面处理线程(该用户的主 Servlet 线程)中运行。同时,数据库检查、数据发布和事件总线调度都发生在由执行器服务管理的后台线程上。切勿从单独的线程访问或更新 Vaadin 小部件!这条规则绝对至关重要。更棘手的是,这样做实际上可能在开发过程中起作用。但如果你在生产中这样做,你将会陷入痛苦的境地。
那么我们如何从后台线程获取数据并传递到主 Servlet 线程中运行的小部件中呢?这UI https://vaadin.com/api/com/vaadin/ui/UI.html类提供了一个专门用于此目的的方法:access https://vaadin.com/api/com/vaadin/ui/UI.html#access(java.lang.Runnable)。你通过一个Runnable https://docs.oracle.com/javase/8/docs/api/java/lang/Runnable.html to the access
方法,Vaadin 安排该 Runnable 在主用户界面线程上执行。十分简单。
剩余课程
为了总结这个示例应用程序,这里是其余的类。 “MyUI”类替换由创建的默认项目中的同名文件Vaadin 7.3.7 的新 Maven 原型 https://vaadin.com/blog/-/blogs/vaadin-7-3-7-and-new-maven-archetypes.
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.
} );
}
}
…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 );
}
}