Отображение одинаковых данных на нескольких клиентах с помощью приложения Push in Vaadin 7
Я хочу поделиться одним и тем же набором данных для нескольких клиентов. Мне нужно использовать Push, чтобы автоматически обновлять их вид на экране.
Я прочитал вопрос и ответ, минимальный пример приложения Push in Vaadin 7 ("@Push"). Теперь мне нужен более надежный реалистичный пример. Во-первых, я знаю, что иметь бесконечный поток не очень хорошая идея в среде сервлетов.
И я не хочу, чтобы у каждого пользователя был свой собственный поток, каждый из которых попадал в базу данных самостоятельно. Кажется более логичным, чтобы один поток проверял свежие данные в базе данных. При обнаружении этот поток должен публиковать свежие данные во всех пользовательских интерфейсах / макетах, ожидающих обновления.
1 ответ
Полностью рабочий пример
Ниже вы найдете код для нескольких классов. Вместе они представляют собой полностью рабочий пример приложения Vaadin 7.3.8, использующего новые встроенные функции Push для одновременной публикации единого набора данных для любого количества пользователей. Мы моделируем проверку базы данных на наличие свежих данных путем случайной генерации набора значений данных.
При запуске этого примера приложения появляется окно, отображающее текущее время вместе с кнопкой. Время обновляется один раз в секунду в течение ста раз.
Это обновление времени не является истинным примером. Средство обновления времени служит двум другим целям:
- Его простой код проверяет, что Push правильно настроен в вашем приложении Vaadin, веб-сервере и веб-браузере.
- Следует примеру кода, приведенному в разделе " Серверная рассылка" Книги Ваадина. Наш обновитель времени здесь почти точно взят из этого примера, за исключением того, что, когда они обновляют график каждую минуту, мы обновляем фрагмент текста.
Чтобы увидеть истинный предполагаемый пример этого приложения, нажмите / нажмите кнопку "Открыть окно данных". Откроется второе окно с тремя текстовыми полями. Каждое поле содержит случайно сгенерированное значение, которое мы притворяемся полученным из запроса к базе данных.
Делать это немного работы, требуя несколько штук. Давайте пройдемся по этим частям.
От себя
В текущей версии Vaadin 7.3.8 нет необходимости в подключаемых модулях или дополнениях для включения технологии Push. Даже связанный с Push файл.jar поставляется вместе с Vaadin.
Смотрите книгу Ваадин для деталей. Но на самом деле все, что вам нужно сделать, это добавить @Push
аннотация к вашему подклассу пользовательского интерфейса.
Используйте последние версии вашего контейнера сервлета и веб-сервера. Push является относительно новым, и реализации развиваются, особенно для разнообразия WebSocket. Например, при использовании Tomcat обязательно используйте последние обновления для Tomcat 7 или 8.
Периодическая проверка свежих данных
У нас должен быть способ повторно запрашивать в базе данных свежие данные.
Бесконечный поток - не лучший способ сделать это в среде сервлета, так как поток не завершится, когда веб-приложение не будет развернуто или когда сервлет будет содержать выключения. Поток будет продолжать работать в JVM, тратить ресурсы, вызывая утечку памяти и другие проблемы.
Крюки запуска / завершения работы веб-приложения
В идеале мы хотим получать информацию, когда веб-приложение запускается (разворачивается) и когда веб-приложение закрывается (или не развертывается). При получении такой информации мы можем запустить или прервать этот поток запросов к базе данных. К счастью, в каждом контейнере сервлета есть такой крючок. Спецификация Servlet требует поддержки контейнера ServletContextListener
интерфейс.
Мы можем написать класс, который реализует этот интерфейс. Когда наше веб-приложение (наше приложение Vaadin) развернуто, наш класс слушателей ' contextInitialized
называется. Когда не работает, contextDestroyed
метод называется.
Исполнитель Сервис
С этого крючка мы можем запустить поток. Но есть и лучший способ. Java поставляется с ScheduledExecutorService
, Этот класс имеет пул потоков в своем распоряжении, чтобы избежать накладных расходов на создание и запуск потоков. Вы можете назначить исполнителю одну или несколько задач ( Runnable) для периодического запуска.
Слушатель веб-приложений
Вот наш класс слушателя веб-приложения, использующий синтаксис Lambda, доступный в 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
В этом коде вы увидите, что экземпляр 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 упаковывает свежие данные для доставки другим объектам. Мы определяем класс DataEven t, чтобы быть этим пакетом. В качестве альтернативы, если вам нужно доставить несколько наборов данных или объектов, а не один, поместите коллекцию в свою версию 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 + " }";
}
}
Распространение данных
Собрав свежие данные в DataEven t, DataProvider передает их DataPublisher. Поэтому следующим шагом является передача этих данных заинтересованным макетам или виджетам Vaadin для представления пользователю. Но как мы узнаем, какие макеты / виджеты интересуют эти данные? И как мы передаем эти данные им?
Одним из возможных способов является Шаблон наблюдателя. Мы видим этот паттерн в Java Swing, а также в Vaadin, например ClickListener
для Button
в Ваадине. Эта картина означает, что наблюдатель и наблюдатель знают друг о друге. А это означает больше работы по определению и реализации интерфейсов.
Even t Bus
В нашем случае нам не нужно, чтобы производитель данных (DataPublisher) и потребители (макеты / виджеты Vaadin) знали друг о друге. Все, что нужно виджетам - это данные, без необходимости дальнейшего взаимодействия с производителем. Таким образом, мы можем использовать другой подход, шину событий. В шине событий некоторые объекты публикуют объект "событие", когда происходит что-то интересное. Другие объекты регистрируют свою заинтересованность в получении уведомлений, когда объект события публикуется в шине. После публикации шина публикует это событие всем зарегистрированным подписчикам, вызывая определенный метод и передавая событие. В нашем случае объект DataEven t будет передан.
Но какой метод для зарегистрированных объектов подписки будет вызван? С помощью магии Java-технологий аннотации, отражения и самоанализа любой метод может быть помечен как вызываемый. Просто пометьте нужный метод аннотацией, а затем позвольте шине найти этот метод во время выполнения при публикации события.
Не нужно строить какие-либо автобусы этого события самостоятельно. В мире Java у нас есть выбор реализаций шины событий.
Google Guava Even tBus
Наиболее известным является, вероятно, Google Guava Even tBus. Google Guava - это набор различных утилитарных проектов, разработанных собственными силами в Google, а затем открытых для других пользователей. Пакет Even tBus является одним из таких проектов. Мы могли бы использовать Guava Even tBus. Действительно, я изначально построил этот пример, используя эту библиотеку. Но у Guava Even tBus есть одно ограничение: он содержит сильные ссылки.
Слабые ссылки
Когда объекты регистрируют свою заинтересованность в получении уведомлений, любая шина событий должна хранить список этих подписок, сохраняя ссылку на регистрируемый объект. В идеале это должна быть слабая ссылка, то есть если объект подписки достигнет конца своей полезности и станет кандидатом на сборку мусора, этот объект может это сделать. Если шина событий содержит надежную ссылку, объект не может перейти к сборке мусора. Слабая ссылка говорит JVM, что мы на самом деле не заботимся об объекте, мы заботимся немного, но недостаточно, чтобы настаивать на сохранении объекта. При слабой ссылке шина событий проверяет наличие нулевой ссылки, прежде чем пытаться уведомить подписчика о новом событии. Если ноль, шина событий может удалить этот слот в своей коллекции отслеживания объектов.
Вы можете подумать, что в качестве обходного пути для решения проблемы удержания сильных ссылок, вы могли бы иметь свои зарегистрированные виджеты Vaadin переопределить detach
метод. Вы будете проинформированы, когда этот виджет Vaadin больше не используется, тогда ваш метод будет отменен из шины событий. Если подписавшийся объект удален из шины событий, то нет более сильной ссылки и больше нет проблем. Но так же, как метод Java Object finalize
не всегда называется, так же как и Ваадин detach
метод не всегда вызывается. См. Сообщение в этой теме эксперта Vaadin Анри Сара для деталей. Надеется detach
может привести к утечке памяти и другим проблемам.
MB Ambassador Event Bus
См. Мой пост в блоге для обсуждения различных реализаций Java библиотек шины событий. Из тех, что я выбрал MB Ambassador для использования в этом примере приложения. Его смысл - использование слабых ссылок.
UI Classes
Между потоками
Для фактического обновления значений макетов и виджетов Vaadin есть одна большая хитрость. Эти виджеты работают в своем собственном потоке обработки пользовательского интерфейса (основной поток сервлета для этого пользователя). Между тем, проверка базы данных, публикация данных и диспетчеризация шины событий происходят в фоновом потоке, управляемом службой исполнителя. Никогда не открывайте и не обновляйте виджеты Vaadin из отдельной ветки! Это правило абсолютно критично. Чтобы сделать это еще сложнее, это может сработать во время разработки. Но вы окажетесь в мире боли, если вы будете делать это на производстве.
Итак, как нам получить данные из фоновых потоков для передачи в виджеты, работающие в основном потоке сервлетов? Класс UI предлагает метод как раз для этой цели: access
, Вы передаете Runnable в access
метод, и Vaadin планирует, что Runnable будет выполняться в главном потоке пользовательского интерфейса. Очень просто.
Оставшиеся классы
Чтобы завершить этот пример приложения, вот остальные классы. Класс "MyUI" заменяет этот файл с тем же именем в проекте по умолчанию, созданном новым архетипом Maven для Vaadin 7.3.7.
package com.example.pushvaadinapp;
import com.vaadin.annotations.Push;
import com.vaadin.annotations.Theme;
import com.vaadin.annotations.VaadinServletConfiguration;
import com.vaadin.annotations.Widgetset;
import com.vaadin.server.BrowserWindowOpener;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinServlet;
import com.vaadin.ui.Button;
import com.vaadin.ui.Label;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;
import java.time.Instant;
import javax.servlet.annotation.WebServlet;
/**
* © 2014 Basil Bourque. This source code may be used freely forever by anyone
* absolving me of any and all responsibility.
*/
@Push
@Theme ( "mytheme" )
@Widgetset ( "com.example.pushvaadinapp.MyAppWidgetset" )
public class MyUI extends UI
{
Label label = new Label( "Now : " );
Button button = null;
@Override
protected void init ( VaadinRequest vaadinRequest )
{
// Prepare widgets.
this.button = this.makeOpenWindowButton();
// Arrange widgets in a layout.
VerticalLayout layout = new VerticalLayout();
layout.setMargin( Boolean.TRUE );
layout.setSpacing( Boolean.TRUE );
layout.addComponent( this.label );
layout.addComponent( this.button );
// Put layout in this UI.
setContent( layout );
// Start the data feed thread
new FeederThread().start();
}
@WebServlet ( urlPatterns = "/*" , name = "MyUIServlet" , asyncSupported = true )
@VaadinServletConfiguration ( ui = MyUI.class , productionMode = false )
public static class MyUIServlet extends VaadinServlet
{
}
public void tellTime ()
{
label.setValue( "Now : " + Instant.now().toString() ); // If before Java 8, use: new java.util.Date(). Or better, Joda-Time.
}
class FeederThread extends Thread
{
// This Thread class is merely a simple test to verify that Push works.
// This Thread class is not the intended example.
// A ScheduledExecutorService is in WebAppListener class is the intended example.
int count = 0;
@Override
public void run ()
{
try {
// Update the data for a while
while ( count < 100 ) {
Thread.sleep( 1000 );
access( new Runnable() // Special 'access' method on UI object, for inter-thread communication.
{
@Override
public void run ()
{
count ++;
tellTime();
}
} );
}
// Inform that we have stopped running
access( new Runnable()
{
@Override
public void run ()
{
label.setValue( "Done. No more telling time." );
}
} );
} catch ( InterruptedException e ) {
e.printStackTrace();
}
}
}
Button makeOpenWindowButton ()
{
// Create a button that opens a new browser window.
BrowserWindowOpener opener = new BrowserWindowOpener( DataUI.class );
opener.setFeatures( "height=300,width=440,resizable=yes,scrollbars=no" );
// Attach it to a button
Button button = new Button( "Open data window" );
opener.extend( button );
return button;
}
}
"DataUI" и "DataLayout" завершают 7 файлов.java в этом примере приложения Vaadin.
package com.example.pushvaadinapp;
import com.vaadin.annotations.Push;
import com.vaadin.annotations.Theme;
import com.vaadin.annotations.Widgetset;
import com.vaadin.server.VaadinRequest;
import com.vaadin.ui.UI;
import java.time.Instant;
import net.engio.mbassy.listener.Handler;
@Push
@Theme ( "mytheme" )
@Widgetset ( "com.example.pushvaadinapp.MyAppWidgetset" )
public class DataUI extends UI
{
// Member vars.
DataLayout layout;
@Override
protected void init ( VaadinRequest request )
{
System.out.println( Instant.now().toString() + " Method DataUI::init running." ); // DEBUG logging.
// Initialize window.
this.getPage().setTitle( "Database Display" );
// Content.
this.layout = new DataLayout();
this.setContent( this.layout );
DataPublisher.instance().register( this ); // Sign-up for notification of fresh data delivery.
}
@Handler
public void update ( DataEvent event )
{
System.out.println( Instant.now().toString() + " Method DataUI::update (@Subscribe) running." ); // DEBUG logging.
// We expect to be given a DataEvent item.
// In a real app, we might need to retrieve data (such as a Collection) from within this event object.
this.access( () -> {
this.layout.update( event ); // Crucial that go through the UI:access method when updating the user interface (widgets) from another thread.
} );
}
}
…а также…
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.example.pushvaadinapp;
import com.vaadin.ui.TextField;
import com.vaadin.ui.VerticalLayout;
import java.time.Instant;
/**
*
* @author brainydeveloper
*/
public class DataLayout extends VerticalLayout
{
TextField uuidField;
TextField numericField;
TextField updatedField;
TextField whenCheckedField;
// Constructor
public DataLayout ()
{
System.out.println( Instant.now().toString() + " Method DataLayout::constructor running." ); // DEBUG logging.
// Configure layout.
this.setMargin( Boolean.TRUE );
this.setSpacing( Boolean.TRUE );
// Prepare widgets.
this.uuidField = new TextField( "UUID : " );
this.uuidField.setWidth( 22 , Unit.EM );
this.uuidField.setReadOnly( true );
this.numericField = new TextField( "Number : " );
this.numericField.setWidth( 22 , Unit.EM );
this.numericField.setReadOnly( true );
this.updatedField = new TextField( "Updated : " );
this.updatedField.setValue( "<Content will update automatically>" );
this.updatedField.setWidth( 22 , Unit.EM );
this.updatedField.setReadOnly( true );
// Arrange widgets.
this.addComponent( this.uuidField );
this.addComponent( this.numericField );
this.addComponent( this.updatedField );
}
public void update ( DataEvent dataHolder )
{
System.out.println( Instant.now().toString() + " Method DataLayout::update (via @Subscribe on UI) running." ); // DEBUG logging.
// Stuff data values into fields. For simplicity in this example app, using String directly rather than Vaadin converters.
this.uuidField.setReadOnly( false );
this.uuidField.setValue( dataHolder.uuid.toString() );
this.uuidField.setReadOnly( true );
this.numericField.setReadOnly( false );
this.numericField.setValue( dataHolder.number.toString() );
this.numericField.setReadOnly( true );
this.updatedField.setReadOnly( false );
this.updatedField.setValue( dataHolder.updated.toString() );
this.updatedField.setReadOnly( true );
}
}