Отображение одних и тех же данных с несколькими клиентами с помощью приложения Push в приложении Vaadin 7

Я хочу поделиться одним и тем же набором данных с несколькими клиентами. Мне нужно использовать Push для автоматического обновления их вида на экране.

Я прочитал «Вопрос и ответ», Минимальный пример приложения Push in Vaadin 7 («@Push») . Теперь мне нужен более надежный реалистичный пример. Во-первых, я знаю, что бесконечная тема не является хорошей идеей в среде Servlet.

И я не хочу, чтобы каждый пользователь имел свой собственный Thread, каждый из которых попадал в базу данных самостоятельно. Кажется более логичным, если один stream проверяет только свежие данные в базе данных. При обнаружении этот stream должен публиковать свежие данные во всех пользовательских интерфейсах / макетах пользователей, ожидающих обновления.

Полностью рабочий пример

Ниже вы найдете код для нескольких classов. Вместе они составляют полностью рабочий пример приложения Vaadin 7.3.8 с использованием новых встроенных функций Push для публикации единого набора данных одновременно любому числу пользователей. Мы моделируем проверку базы данных для свежих данных путем случайного генерирования набора значений данных.

Когда вы запускаете это примерное приложение, появляется окно, отображающее текущее время вместе с кнопкой. Время обновляется один раз в секунду в сто раз.

Снимок экрана первого окна в примере приложения

Это обновление времени не является верным примером. Программа обновления времени выполняет две другие задачи:

  • Его простой код проверяет правильность настройки Push в приложении Vaadin, веб-сервере и веб-браузере.
  • Выполняется код примера, приведенный в разделе «Нагрузка сервера» книги «Ваадин» . Наше время-обновление здесь почти точно отменено из этого примера, за исключением того, что, когда они обновляют график каждую минуту, мы обновляем fragment текста.

Чтобы увидеть истинный пример этого приложения, нажмите / коснитесь кнопки «Открыть окно данных». Откроется второе окно, чтобы отобразить три текстовых поля. Каждое поле содержит случайное генерируемое значение, которое мы притворяемся результатом запроса базы данных.

Экран экрана дисплея базы данных с тремя текстовыми полями

Выполнение этого – небольшая работа, требующая нескольких частей. Давайте рассмотрим эти штуки.

диаграмма различных классов и объектов в этом примере приложения

От себя

В текущей версии Vaadin 7.3.8 нет необходимости в плагинах или дополнениях для включения технологии Push . Даже связанный с Push файл .jar связан с Vaadin.

См. Книгу Ваадина за подробностями. Но на самом деле все, что вам нужно сделать, это добавить аннотацию @Push к вашему подclassу пользовательского интерфейса .

Используйте последние версии вашего контейнера сервлетов и веб-сервера. Push является относительно новым, и реализации развиваются, особенно для разнообразия WebSocket . Например, при использовании Tomcat обязательно используйте последние обновления для Tomcat 7 или 8.

Периодическая проверка свежих данных

Мы должны каким-то образом повторно запросить базу данных для свежих данных.

Бесконечный Thread – это не лучший способ сделать это в среде Servlet, так как Thread не закончится, когда веб-приложение не будет развернуто, или когда сервлет содержит отключен. Thread продолжит работать в JVM, тратит ресурсы, вызывая утечку памяти и другие проблемы.

Захваты для запуска / закрытия веб-приложений

В идеале мы хотим получать информацию, когда веб-приложение запускается (развертывается) и когда веб-приложение отключается (или не развертывается). Когда это так информировано, мы можем запустить или прервать этот stream запросов к базе данных. К счастью, есть такой крючок, который входит в каждый контейнер Servlet. Спецификация Servlet требует, чтобы контейнер поддерживал интерфейс ServletContextListener .

Мы можем написать class, реализующий этот интерфейс. Когда наше веб-приложение (наше приложение Vaadin) развернуто, вызывается наш class-слушатель ‘ contextInitialized . Когда undeployed, contextDestroyed метод contextDestroyed .

Исполнительная служба

Из этого крючка мы могли бы запустить Thread. Но есть лучший способ. Java поставляется с ScheduledExecutorService . В этом classе имеется пул streamов, чтобы избежать накладных расходов на создание и запуск streamов. Вы можете назначить одну или несколько задач ( Runnable ) исполнителю, которые будут выполняться периодически.

Слушатель веб-приложений

Вот наш class слушателей веб-приложений, используя синтаксис Lambda, ansible на 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 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 ); } } 

Доступ к базе данных

Этот class DataPublisher использует class 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 упаковывает свежие данные для доставки другим объектам. Мы определяем class DataEvent как этот пакет. В качестве альтернативы, если вам нужно доставить несколько наборов данных или объектов, а не один, поместите коллекцию в свою версию DataHolder. Упакуйте все, что имеет смысл для макета или виджета, который хочет отображать эти свежие данные.

 package com.example.pushvaadinapp; import java.time.Instant; import java.util.UUID; /** * Holds data to be published in the UI. In real life, this could be one object * or could hold a collection of data objects as might be needed by a chart for * example. These objects will be dispatched to subscribers of an MBassador * event bus. * * @author Basil Bourque */ public class DataEvent { // Core data values. UUID uuid = null; Number number = null; Instant updated = null; // Constructor public DataEvent ( UUID uuid , Number number , Instant updated ) { this.uuid = uuid; this.number = number; this.updated = updated; } @Override public String toString () { return "DataEvent{ " + "uuid=" + uuid + " | number=" + number + " | updated=" + updated + " }"; } } 

Распространение данных

Упаковывая свежие данные в DataEvent, DataProvider передает это сообщение DataPublisher. Итак, следующий шаг – получение этих данных заинтересованным макетам или виджетам Vaadin для представления пользователю. Но как мы узнаем, какие макеты / виджеты заинтересованы в этих данных? И как мы доставляем им эти данные?

Одним из возможных способов является шаблон наблюдателя . Мы видим этот шаблон в Java Swing, а также Vaadin, например ClickListener для Button в Vaadin. Эта закономерность означает наблюдателя и наблюдает друг друга. И это означает большую работу по определению и внедрению интерфейсов.

Автобус событий

В нашем случае нам не нужен производитель данных (DataPublisher) и потребителей (макеты / виджеты Vaadin), чтобы знать друг о друге. Все необходимые виджеты – это данные, без необходимости дальнейшего взаимодействия с производителем. Таким образом, мы можем использовать другой подход – шину событий. В случае шины некоторые объекты публикуют объект «событие», когда происходит что-то интересное. Другие объекты регистрируют свою заинтересованность в уведомлении, когда объект события отправляется в автобус. Когда сообщение отправлено, автобус публикует это событие всем зарегистрированным подписчикам, вызывая определенный метод и передавая событие. В нашем случае объект DataEvent будет передан.

Но какой метод на зарегистрированных подписных объектах будет вызываться? Благодаря магии технологий annotations, отражения и интроспекции Java, любой метод может быть помечен как тот, который будет вызываться. Просто пометьте желаемый метод аннотацией, затем пусть автобус найдет этот метод во время выполнения при публикации события.

Не нужно самостоятельно строить какую-либо из этих автобусов. В Java-мире у нас есть выбор реализаций шины событий.

Google Guava EventBus

Наиболее известным является, вероятно, Google Guava EventBus . Google Guava – это набор различных полезных проектов, разработанных на дому в Google, а затем открытый для других пользователей. Пакет EventBus является одним из этих проектов. Мы могли бы использовать Guava EventBus. В самом деле, я изначально построил этот пример, используя эту библиотеку. Но у Guava EventBus есть одно ограничение: он содержит сильные ссылки.

Слабые ссылки

Когда объекты регистрируют свою заинтересованность в получении уведомления, любая шина событий должна хранить список этих подписей, удерживая ссылку на регистрирующий объект. В идеале это должно быть слабой ссылкой , что означает, что, если подписывающий объект достигнет своей полезности и станет кандидатом на garbage collection , этот объект может это сделать. Если шина событий содержит сильную ссылку, объект не может перейти к сборке мусора. Слабая ссылка сообщает JVM, что мы действительно не заботимся об объекте, мы немного заботимся, но недостаточно, чтобы настаивать на сохранении объекта. При слабой ссылке шина событий проверяет нулевую ссылку перед попыткой уведомить абонента о новом событии. Если значение null, шина событий может отбросить этот слот в своей коллекции отслеживания объектов.

Вы могли бы подумать, что в качестве обходного пути для решения проблемы сильных ссылок вы можете иметь зарегистрированные виджеты Vaadin, чтобы переопределить метод detach . Вам будет сообщено, когда этот виджет Vaadin больше не используется, тогда ваш метод будет отменять регистрацию из шины событий. Если абонентский объект выведен из шины событий, то более сильная ссылка и больше проблем не возникает. Но так же, как метод Java Object finalize не всегда называется , так же не всегда называется метод detach Vaadin. См. Публикацию в этой ветке эксперта Ваадина Анри Сара за подробностями. Опора на detach может привести к утечкам памяти и другим проблемам.

MBassador Event Bus

Смотрите мое сообщение в блоге для обсуждения различных реализаций Java библиотек событий. Из тех, кого я выбрал MBassador для использования в этом примере приложения. Его смысл заключается в использовании слабых ссылок.

Классы пользовательского интерфейса

Между streamами

Чтобы действительно обновить значения макетов и виджетов Vaadin, есть один большой улов. Эти виджеты работают в своем streamе обработки пользовательского интерфейса (основной stream сервлета для этого пользователя). Между тем, проверка базы данных и публикация данных и диспетчеризация событий-событий происходят в фоновом streamе, управляемом службой-исполнителем. Никогда не открывайте и не обновляйте виджеты Vaadin из отдельного streamа! Это правило абсолютно критично. Чтобы сделать его еще более сложным, это может действительно работать во время разработки. Но вы окажетесь в мире обид, если вы сделаете это на производстве.

Итак, как мы получаем данные из фоновых streamов для передачи в виджеты, запущенные в основном streamе сервлета? Класс UI предлагает метод только для этой цели: access . Вы передаете Runnable методу access , а Vaadin планирует, что Runnable будет выполняться в основном streamе пользовательского интерфейса. Очень просто.

Остальные classы

Чтобы завершить это приложение, вот остальные classы. Класс «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( "" ); 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 ); } } 
  • Преобразование прозрачного gif / png в jpeg с помощью java
  • Поддерживает ли Swing поддержку файлов в стиле Windows 7?
  • Проект Maven Run
  • Реализация Singleton с Enum (на Java)
  • JAXB Marshalling Unmarshalling с CDATA
  • Как разрешить java.lang.NoClassDefFoundError: javax / xml / bind / JAXBException в Java 9
  • Преобразование 'ArrayList в' String 'в Java
  • Почему объявление переменной требуется внутри цикла for-each в java
  • вызывающий thread.start () внутри своего собственного конструктора
  • Как использовать несколько объектов Scanner в System.in?
  • Как вы возвращаете объект JSON из Java-сервлета
  • Давайте будем гением компьютера.