Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.3k views
in Technique[技术] by (71.8m points)

spring - Vaadin: Update UI after data returned

@SpringUI
public class VaadinUI extends UI {
  ...
  String sql = "SELECT * FROM table1";
  button.addClickListener(e -> layout.addComponent(new Label(service.evalSql(sql))));
  ...

Currently, when button is pressed, the page waits for evalSql() to get a result back from the database before adding a new Label.

How can I change this so, when button is pressed, a new Label is immediately added, set to an initial placeholder string ("Fetching result..") but updated to the result string after the database returns something?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

The good news is that what you want, to have a widget in your Vaadin user-interface later updated by work done in the background on the server without blocking the UI's responsiveness to the user, can be done. It can be done very well with Vaadin and its Java-based backend.

The bad news is that if you are new to concurrency and threading, you have a learning curve to climb.

Asynchronous

The technical term for wanting your app to do something in the background and check back later without blocking is: asynchronous update.

We can accomplish this in Java using threads. Spawn a thread to run your SQL service code. When that code finishes the database work, that code posts a request by calling UI::access(Runnable runnable) to have the original user-interface (UI) thread update the Label widget.

Push technology

As discussed in the Answer by Lund updating of the Label widget requires Push Technology to update the browser from a server-side generated event. Fortunately, Vaadin 8 and later has excellent support for Push and makes instituting Push in your app extraordinarily easy.

Tip: Push in general, and WebSocket especially, has evolved greatly in recent years. Using the latest generation of Servlet container will improve your experience. For example, if using Tomcat I recommend using the latest version of Tomcat 8.5 or 9.

Threads

Java has excellent support for threading. Much of the necessary work is handled for you by the Executor framework built into Java.

If you are new to threading, you have some serious learning ahead of you. Start by studying the Oracle Tutorial on concurrency. Eventually you'll need to read and re-read a few times the excellent book Java Concurrency in Practice by Brian Goetz et al.

ServletContextListener

You will likely want to set-up and tear-down your thread-juggling executor service as your Vaadin app launches and exits. The way to do that is to write a class separate from your Vaadin servlet class. This class must implement the ServletContextListener. You can easily do that by implementing the two required methods, and annotating the class with @WebListener.

Be sure to tear-down the executor service. Otherwise the background threads it manages may survive the shutdown of your Vaadin app and possibly even the shutdown of your web container (Tomcat, Jetty, etc.), continuing to run indefinitely.

Never access widget from background thread

A key idea in this work: Never ever access any Vaadin UI widget directly from any background. Do not call any methods on the UI widgets, nor access any values, from any widget in code running in a background thread. The UI widgets are not thread-safe (making a user-interface technology thread-safe is terribly difficult). You might get away with such a background call, or terrible things may happen at runtime.

Java EE

If you happen to be using a full-blown Jakarta EE (formerly known as Java EE) server rather than either a web container (such as Tomcat or Jetty) or a Web Profile server (such as TomEE), then the work discussed above with the executor service and ServletContextListener is done for you. Use the features defined in Java EE 7 and later: JSR 236: Concurrency Utilities for JavaTM EE

Spring

Your Question is tagged with Spring. Spring may have features to help with this work. I don’t know, as I am not a Spring user. Perhaps Spring TaskExecutor?

Search Stack Overflow

If you search Stack Overflow you will find that all these topics have been addressed.

I have already posted two full example apps demonstrating Vaadin with Push:

  • A contrived minimalist example, just to give you a taste of what is involved. That particular approach should never be used in real work, as noted in that Answer.
  • For real work, see a more complicated example app posted in this Answer.

Complete example

Start with the Vaadin 8.4.3 app generated by the Maven archetype, vaadin-archetype-application provided by the Vaadin Ltd. company.

package com.basilbourque.example;

import javax.servlet.annotation.WebServlet;

import com.vaadin.annotations.Theme;
import com.vaadin.annotations.VaadinServletConfiguration;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinServlet;
import com.vaadin.ui.Button;
import com.vaadin.ui.Label;
import com.vaadin.ui.TextField;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;

/**
 * This UI is the application entry point. A UI may either represent a browser window
 * (or tab) or some part of an HTML page where a Vaadin application is embedded.
 * <p>
 * The UI is initialized using {@link #init(VaadinRequest)}. This method is intended to be
 * overridden to add component to the user interface and initialize non-component functionality.
 */
@Theme ( "mytheme" )
public class MyUI extends UI {

    @Override
    protected void init ( VaadinRequest vaadinRequest ) {
        final VerticalLayout layout = new VerticalLayout();

        final TextField name = new TextField();
        name.setCaption( "Type your name here:" );

        Button button = new Button( "Click Me" );
        button.addClickListener( e -> {
            layout.addComponent( new Label( "Thanks " + name.getValue() + ", it works!" ) );
        } );

        layout.addComponents( name , button );

        setContent( layout );
    }

    @WebServlet ( urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true )
    @VaadinServletConfiguration ( ui = MyUI.class, productionMode = false )
    public static class MyUIServlet extends VaadinServlet {
    }
}

enter image description here

As discussed above, we need to assign your SQL-service work as a task to be done on a background thread. The Executor framework in Java 5 and later does all the heavy-lifting for such thread work. We need to establish an executor service backed by a thread pool to do all the updating of all the new labels being added on all the users’ web browser windows. The question is where do we setup, store, and teardown that executor service object?

We want the executor service to be available for the entire lifecycle of our web app. Before the first user request arrives to our freshly-launched web app, we want to setup the executor service. And when we are trying to shutdown our web app, we need to teardown that executor service, so that the threads in its backing thread pool are terminated. How do we tie into the lifecycle of our Vaadin web app?

Well, Vaadin is an implementation of Java Servlet, albeit a very large and sophisticated servlet. In Servlet terminology, your web app is known as a “context”. The Servlet specification requires that all Servlet containers (such as Tomcat, Jetty, etc.) notice any class marked as a listener to certain events. To take advantage of this we must add another class to our Vaadin app, a class implementing the ServletContextListener interface.

If we annotate our new class as @WebListener, the Servlet container will notice this class, and when launching our web app will instantiate our listener object, and then call its methods at the appropriate times. The contextInitialized method is called after the servlet has been properly initialized but before any incoming web browser requests have been handled. The contextDestroyed method is called after the last web browser request has been handled, after that last response has been sent back to the user.

So our class implementing the ServletContextListener is the perfect place to setup and teardown our executor service with its backing thread pool.

One more problem: After setting up our executor service, where do we store a reference to be found and used later in our Vaadin servlet when the users are adding their Label objects? One solution is to store the executor service reference as an “attribute” in the “context” (our web app). The Servlet spec requires that every Servlet container provide each context (web app) with a simple key-value collection where the key is a String object and the value is an Object object. We can invent some string to identify our executor service, and then our Vaadin servlet can later do a loop-up to retrieve the executor service.

Ironically enough, that discussion above is longer than the actual code!

package com.basilbourque.example;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@WebListener
// Annotate to instruct your web container to notice this class as a context listener and then automatically instantiate as context (your web app) lanuches.
public class MyServletContextListener implements ServletContextListener {
    static final public String executorServiceNameForUpdatingLabelAfterSqlService = "ExecutorService for SQL service update of labels";

    @Override
    public void contextInitialized ( final ServletContextEvent sce ) {
        // Initialize an executor service. Store a reference as a context attribute for use later in our webapp’s Vaadin servlet.
        ExecutorService executorService = Executors.newFixedThreadPool( 7 );  // Choose an implementation and number of threads appropriate to demands of your app and capabilities of your deployment server.
        sce.getServletContext().setAttribute( MyServletContextListener.executorServiceNameForUpdatingLabelAfterSqlService , executorService );
    }

    @Override
    public void contextDestroyed ( final ServletContextEvent sce ) {
        // Always shutdown your ExecutorService, otherwise the threads may survive shutdown of your web app and perhaps even your web container.

        // The context addribute is stored as `Object`. Cast to `ExecutorService`.
        ExecutorService executorService = ( ExecutorService ) sce.getServletContext().getAttribute( MyServletContextListener.executorServiceNameForUpdatingLabelAfterS

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...