Displaying Same Data Aross Multiple Clients Using

2019-01-09 08:04发布

问题:

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.

回答1:

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

}