完全に機能する例
以下に、いくつかのクラスのコードを示します。これらを組み合わせることで、Vaadin 7.3.8 アプリの完全に機能する例が作成され、新しい組み込みのプッシュ機能を使用して単一のデータ セットを任意の数のユーザーに同時に公開できます。データ値のセットをランダムに生成することにより、データベースの新しいデータのチェックをシミュレートします。
このサンプル アプリを実行すると、現在の時刻とボタンを表示するウィンドウが表示されます。時間は 1 秒に 1 回、100 回更新されます。
この時間更新は、真の例ではありません。time-updater には他に 2 つの目的があります。
このアプリの実際の例を表示するには、[データ ウィンドウを開く] ボタンをクリックまたはタップします。2 番目のウィンドウが開き、3 つのテキスト フィールドが表示されます。各フィールドには、ランダムに生成された値が含まれており、データベース クエリから取得したと見なされます。
これを行うには、いくつかの部品が必要で、少し手間がかかります。それらの部分を見てみましょう。
押す
Vaadin 7.3.8 の現在のバージョンでは、 Push テクノロジーを有効にするためのプラグインやアドオンは必要ありません。Push 関連の .jar ファイルも Vaadin にバンドルされています。
詳細については、ブック オブ ヴァーディンを参照してください。しかし、実際に行う必要があるのは、@Push
注釈をUIのサブクラスに追加することだけです。
サーブレット コンテナーと Web サーバーの最新バージョンを使用します。プッシュは比較的新しく、特にWebSocketの種類については実装が進化しています。たとえば、Tomcat を使用している場合は、必ず Tomcat 7 または 8 の最新の更新を使用してください。
新しいデータを定期的にチェックする
データベースに繰り返しクエリを実行して新しいデータを取得する方法が必要です。
終わりのないスレッドは、サーブレット環境でこれを行うための最良の方法ではありません。スレッドは、Web アプリがアンデプロイされたとき、またはサーブレットにシャットダウンが含まれているときに終了しないためです。スレッドは JVM で引き続き実行され、リソースを浪費し、メモリ リークやその他の問題を引き起こします。
Web アプリの起動/シャットダウン フック
理想的には、Web アプリが起動 (展開) されたとき、および Web アプリがシャットダウン (または展開解除) されたときに通知を受ける必要があります。そのように通知された場合、そのデータベース クエリ スレッドを起動または中断することができます。幸いなことに、すべてのサーブレット コンテナーの一部としてこのようなフックが提供されています。サーブレット仕様では、ServletContextListener
インターフェースをサポートするコンテナーが必要です。
このインターフェースを実装するクラスを書くことができます。Web アプリ (Vaadin アプリ) がデプロイされると、リスナー クラスcontextInitialized
が呼び出されます。アンデプロイされると、contextDestroyed
メソッドが呼び出されます。
エグゼキュータ サービス
このフックから、スレッドを開始できます。しかし、もっと良い方法があります。Java にはScheduledExecutorService
. このクラスには、スレッドのインスタンス化と開始のオーバーヘッドを回避するために、自由に使用できるスレッドのプールがあります。1 つ以上のタスク ( Runnable ) をエグゼキューターに割り当てて、定期的に実行することができます。
Web アプリ リスナー
これは、Java 8 で利用可能な Lambda 構文を使用した、Web アプリのリスナー クラスです。
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 クラスを定義します。または、1 つではなく複数のデータまたはオブジェクトのセットを配信する必要がある場合は、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 レイアウトまたはウィジェットにそのデータを取得して、ユーザーに提示することです。しかし、どのレイアウト/ウィジェットがこのデータに関心を持っているかをどうやって知るのでしょうか? そして、このデータをどのように彼らに届けますか?
考えられる方法の 1 つは、オブザーバー パターンです。ClickListener
このパターンは、Vaadin の a for aなど、Vaadin だけでなく Java Swing でも見られButton
ます。このパターンは、観察者と観察者がお互いについて知っていることを意味します。また、インターフェイスの定義と実装の作業が増えることを意味します。
イベントバス
この場合、データのプロデューサー (DataPublisher) とコンシューマー (Vaadin レイアウト/ウィジェット) がお互いを知る必要はありません。ウィジェットが必要とするのはデータだけであり、プロデューサーとのさらなる対話は必要ありません。そのため、別のアプローチであるイベント バスを使用できます。イベント バスでは、何か興味深いことが発生すると、一部のオブジェクトが「イベント」オブジェクトを発行します。他のオブジェクトは、イベント オブジェクトがバスにポストされたときに通知されることに関心を持っています。ポストされると、バスは特定のメソッドを呼び出してイベントを渡すことにより、そのイベントをすべての登録済みサブスクライバーにパブリッシュします。この場合、DataEvent オブジェクトが渡されます。
しかし、登録されたサブスクライブ オブジェクトのどのメソッドが呼び出されるのでしょうか? Java のアノテーション、リフレクション、およびイントロスペクション テクノロジの魔法により、任意のメソッドに、呼び出されるメソッドとしてタグを付けることができます。目的のメソッドに注釈を付けてタグ付けするだけで、バスは実行時にイベントを発行するときにそのメソッドを見つけます。
このイベント バスを自分で構築する必要はありません。Java の世界では、イベント バスの実装を選択できます。
Google Guava EventBus
最もよく知られているのは、おそらく Google Guava EventBusです。Google Guavaは、Google の社内で開発され、他のユーザーが使用できるようにオープンソース化されたさまざまなユーティリティ プロジェクトの集まりです。EventBus パッケージは、それらのプロジェクトの 1 つです。Guava EventBus を使用できます。実際、私はもともとこのライブラリを使用してこの例を作成しました。しかし、Guava EventBus には 1 つの制限があります。強い参照を保持します。
弱参照
オブジェクトが通知を受けることへの関心を登録すると、イベント バスは、登録オブジェクトへの参照を保持することによって、それらのサブスクリプションのリストを保持する必要があります。理想的には、これは弱い参照であるべきです。つまり、サブスクライブしているオブジェクトがその有用性の終わりに達し、ガベージ コレクションの候補になる場合、そのオブジェクトはそうする可能性があります。イベント バスが強い参照を保持している場合、オブジェクトはガベージ コレクションに進むことができません。弱い参照は、JVM に実際にはそうではないことを伝えます。オブジェクトを気にかけますが、オブジェクトを保持することを主張するほどではありません。弱参照の場合、イベント バスはサブスクライバーに新しいイベントを通知する前に null 参照をチェックします。null の場合、イベント バスはオブジェクト トラッキング コレクションにそのスロットをドロップできます。
強い参照を保持する問題の回避策として、登録済みの Vaadin ウィジェットでメソッドをオーバーライドできると考えるかもしれませんdetach
。その Vaadin ウィジェットが使用されなくなったことが通知され、メソッドがイベント バスから登録解除されます。サブスクライブしているオブジェクトがイベント バスから取り出された場合、強い参照や問題はなくなります。しかし、Java Object メソッドfinalize
が常に呼び出されるとは限らないように、Vaadindetach
メソッドも常に呼び出されるとは限りません。詳細については、 Vaadin の専門家であるHenri Saraによるこのスレッドへの投稿を参照してください。に依存すると、メモリ リークやその他の問題が発生する可能性があります。detach
MBバサダーイベントバス
イベント バス ライブラリのさまざまな Java 実装の説明については、私のブログ投稿を参照してください。その中から、このサンプル アプリで使用するMBassadorを選びました。その存在理由は、弱参照の使用です。
UI クラス
スレッド間
Vaadin のレイアウトとウィジェットの値を実際に更新するには、大きな問題が 1 つあります。これらのウィジェットは、独自のユーザー インターフェイス処理スレッド (このユーザーのメイン サーブレット スレッド) で実行されます。その間、データベースのチェック、データの公開、およびイベントバスのディスパッチはすべて、executor サービスによって管理されるバックグラウンド スレッドで行われます。別のスレッドから Vaadin ウィジェットにアクセスしたり更新したりしないでください。このルールは絶対に重要です。さらに厄介なことに、これを行うと、開発中に実際に機能する可能性があります。しかし、本番環境でこれを行うと、大変な事態になります。
では、バックグラウンド スレッドからデータを取得して、メインのサーブレット スレッドで実行されているウィジェットに通信するにはどうすればよいでしょうか。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.
} );
}
}
…そして…</p>
/*
* 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 );
}
}