Vaadin:返回数据后更新用户界面 [英] Vaadin: Update UI after data returned

查看:140
本文介绍了Vaadin:返回数据后更新用户界面的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

当前,当按下按钮时,页面将等待evalSql()从数据库中获取结果,然后再添加新的Label.

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

如何更改此设置,当按下按钮时,立即添加新的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?

推荐答案

好消息是,您希望在Vaadin用户界面中拥有一个小部件,稍后通过服务器后台在后台完成的工作来更新该小部件可以完成UI对用户的响应 .使用Vaadin及其基于Java的后端,可以很好地完成此任务.

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.

希望您的应用在后台执行某项操作并稍后再返回而不阻塞的技术术语是:

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

我们可以使用线程在Java中完成此操作.产生一个线程来运行您的SQL服务代码.当该代码完成数据库工作时,该代码通过调用UI::access(Runnable runnable)发出请求,以使原始用户界面(UI)线程更新Label小部件.

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.

隆德回答中所述,Label小部件的更新需要 Vaadin 8和更高版本对Push都提供了出色的支持,使在应用程序中安装Push变得异常简单.

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.

提示:通常进行推送,尤其是 WebSocket 近年来发展很快.使用最新一代的Servlet容器将改善您的体验.例如,如果使用Tomcat,我建议使用最新版本的Tomcat 8.5或9.

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.

Java对线程有出色的支持. Java内置的Executor框架为您处理了许多必要的工作.

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

如果您不熟悉线程,那么您还需要认真学习.首先研究有关并行的Oracle教程.最终,您需要阅读和重新阅读几本优秀的书 Brian Goetz等人的《 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.

在Vaadin应用程序启动和退出时,您可能希望设置和拆除线程打乱执行程序服务.做到这一点的方法是编写一个与Vaadin servlet类分开的类.此类必须实现ServletContextListener.您可以通过实现两个必需的方法并使用@WebListener注释类来轻松地做到这一点.

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.

确保拆除执行程序服务.否则,它管理的后台线程可能会在关闭Vaadin应用程序后甚至在关闭 Web容器(Tomcat,Jetty等),继续无限期运行.

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.

这项工作中的关键思想:永远不要从任何背景直接访问任何Vaadin UI窗口小部件.不要在后台线程中运行的代码中从任何窗口小部件调用UI窗口小部件上的任何方法,也不要访问任何值. UI小部件不是线程安全的(使用户界面技术线程安全非常困难).您可能会被这样的后台调用弄走,否则在运行时可能会发生可怕的事情.

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.

如果您恰巧使用的是成熟的Jakarta EE(以前称为Java EE)服务器,而不是Web容器(例如Tomcat或Jetty)或Web Profile服务器(例如TomEE),上面与执行程序服务进行了讨论,并且ServletContextListener已为您完成.使用Java EE 7和更高版本中定义的功能: JSR 236:并发实用程序用于JavaTM 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 标签. Spring可能具有帮助完成这项工作的功能.我不知道,因为我不是Spring用户.也许 Spring TaskExecutor ?

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?

如果搜索Stack Overflow,则会发现所有这些主题均已解决.

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

我已经发布了两个完整的示例应用程序,它们通过Push演示了Vaadin:

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.

首先使用由Vaadin Ltd.公司提供的Maven原型vaadin-archetype-application生成的Vaadin 8.4.3应用程序.

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 {
    }
}

如上所述,我们需要将您的SQL服务工作分配为要在后台线程上完成的任务. Java 5和更高版本中的 Executor 框架完成了此类线程工作的所有繁重工作.我们需要建立一个由线程池支持的执行程序服务,以对所有在用户的Web浏览器窗口中添加的所有新标签进行所有更新.问题是我们在哪里设置,存储和拆除该执行服务对象?

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?

我们希望在我们的Web应用程序的整个生命周期中都可以使用执行程序服务.在第一个用户请求到达我们新启动的Web应用程序之前,我们要设置执行程序服务.当我们尝试关闭Web应用程序时,我们需要拆除该执行程序服务,以便终止其支持线程池中的线程.我们如何融入Vaadin Web应用程序的生命周期?

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?

好吧,Vaadin是 Java Servlet 的实现,尽管它是非常大型且复杂的servlet .用Servlet术语,您的Web应用程序称为上下文". Servlet规范要求所有 Servlet容器(例如Tomcat,Jetty等)都必须注意类标记为某些事件的侦听器.为了利用这一点,我们必须在Vaadin应用程序中添加另一个类,该类实现ServletContextListener接口.

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.

如果将新类注释为@WebListener,则Servlet容器将注意到该类,并且在启动Web应用程序时将实例化我们的侦听器对象,然后在适当的时间调用其方法.在已正确初始化servlet之后但在处理任何传入的Web浏览器请求之前,将调用contextInitialized方法.在处理完最后一个Web浏览器请求之后,在将最后一个响应发送回用户之后,调用contextDestroyed方法.

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.

因此,实现ServletContextListener的类是使用后备线程池设置和拆除执行程序服务的理想场所.

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

另一个问题:设置执行程序服务后,当用户添加其Label对象时,我们将在何处存储引用以供以后在Vaadin servlet中使用?一种解决方案是将执行程序服务引用作为属性"存储在上下文"(我们的Web应用程序)中. Servlet规范要求每个Servlet容器为每个上下文(Web应用程序)提供一个简单的键值集合,其中键是String对象,而值是Object对象.我们可以发明一些字符串来标识执行程序服务,然后我们的Vaadin servlet稍后可以进行循环以检索执行程序服务.

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.executorServiceNameForUpdatingLabelAfterSqlService );
        if ( null != executorService ) {
            executorService.shutdown();
        }

    }
}

现在,回到我们的Vaadin应用程序.修改该文件:

Now, back to our Vaadin app. Modify that file:

  • @Push注释Servlet,以使Vaadin能够通过服务器端生成的事件来更新用户界面小部件.
  • 修改每个Label的创建.
    • Label的初始文本更改为包括"Created:"和当前日期时间.
    • 将实例化到自己的行.
    • Annotate the Servlet with @Push to engage Vaadin’s ability to have a server-side generated event update the user-interface widgets.
    • Modify the creation of each Label.
      • Change the initial text for the Label to include "Created: " with current date-time.
      • Move the instantiation to its own line.

      如果您不熟悉线程和并发性,这可能会令人生畏.学习这段代码,并花一些时间.您可以用其他方式构造它,但是出于教学目的,我想在这里简化它.重点不在于代码的结构/安排,而是重点在于:

      If you are new to threading and concurrency, this may be daunting. Study this code, and it some time to sink in. You could structure this in other manners, but I wanted to make it simple here for teaching purposes. Focus not on the structure/arrangement of the code but on the idea of:

      • 用户单击按钮,这是Vaadin UI主线程中的事件.
      • 按钮上的代码将执行任务(Runnable)提交给执行者服务,该任务稍后将在后台线程中运行.
      • 该后台线程最终运行时会调用您的SQL服务以完成一些工作.完成后,我们代表UI发布一个请求(另一个Runnable)来代表我们进行一些与小部件相关的工作(我们的Label文本更新).
      • 当方便使用UI时,当它不太忙于处理用户界面中生成的其他与用户相关的事件时,UI会运行我们的Runnable来实际修改Label的文本不久前添加的.
      • User clicks button, an event in main Vaadin UI thread.
      • Code on the button submits to an executor service a task (a Runnable) to be run later in a background thread.
      • That background thread, when eventually run, calls on your SQL service to get some work done. When done, we post a request (another Runnable) to the UI to do some widget-related work (our Label text updating) on our behalf.
      • When convenient to the UI, when it is not too busy handling other user-related events generated in the user-interface, the UI gets around to running our Runnable to actually modify the text of the Label that was added a while ago.

      这是我们经过修改的Vaadin应用程序.

      Here is our modified Vaadin app.

      package com.basilbourque.example;
      
      import javax.servlet.ServletContext;
      import javax.servlet.annotation.WebServlet;
      
      import com.vaadin.annotations.Push;
      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;
      
      import java.time.Instant;
      import java.time.ZoneId;
      import java.time.ZonedDateTime;
      import java.util.UUID;
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.ThreadLocalRandom;
      import java.util.concurrent.TimeUnit;
      
      /**
       * 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.
       */
      @Push  // This annotation enables the Push Technology built into Vaadin 8.4.
      @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( ( Button.ClickEvent e ) -> {
                  Label label = new Label( "Thanks " + name.getValue() + ", it works!" + " " + ZonedDateTime.now( ZoneId.systemDefault() ) );  // Moved instantiation of `Label` to its own line so that we can get a reference to pass to the executor service.
                  layout.addComponent( label );  // Notes current date-time when this object was created.
      
                  //
                  ServletContext servletContext = VaadinServlet.getCurrent().getServletContext();
                  // The context attribute is stored as `Object`. Cast to `ExecutorService`.
                  ExecutorService executorService = ( ExecutorService ) servletContext.getAttribute( MyServletContextListener.executorServiceNameForUpdatingLabelAfterSqlService );
                  if ( null == executorService ) {
                      System.out.println( "ERROR - Failed to find executor serivce." );
                  } else {
                      executorService.submit( new Runnable() {
                          @Override
                          public void run () {
                              // Pretending to access our SQL service. To fake it, let's sleep this thread for a random number of seconds.
                              int seconds = ThreadLocalRandom.current().nextInt( 4 , 30 + 1 ); // Pass ( min , max + 1 )
                              try {
                                  Thread.sleep( TimeUnit.SECONDS.toMillis( seconds ) );
                              } catch ( InterruptedException e ) {
                                  e.printStackTrace();
                              }
                              // Upon waking, ask that our `Label` be updated.
                              ZonedDateTime zdt = ZonedDateTime.now( ZoneId.systemDefault() );
                              System.out.println( "Updating label at " + zdt );
                              access( new Runnable() {  // Calling `UI::access( Runnable )`, asking that this Runnable be run on the main UI thread rather than on this background thread.
                                  @Override
                                  public void run () {
                                      label.setValue( label.getValue() + " Updated: " + zdt );
                                  }
                              } );
                          }
                      } );
                  }
              } );
      
              layout.addComponents( name , button );
      
              setContent( layout );
          }
      
      
          @WebServlet ( urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true )
          @VaadinServletConfiguration ( ui = MyUI.class, productionMode = false )
          public static class MyUIServlet extends VaadinServlet {
          }
      }
      

      进行此类异步线程工作时,无法预测确切的执行顺序.您不确切知道后台线程何时以及按照什么顺序执行.您不知道UI对象何时会到达我们的请求,以更新Label对象的文本.请注意,在此屏幕截图中,在运行此应用程序时,不同的Label对象在任意时间以任意顺序更新.

      When doing such asynchronous threaded work, the exact order of execution cannot be predicted. You do not know exactly when and in which order the background threads well be executing. You do not know when the UI object will get to our request to update the Label object’s text. Notice in this screenshot that while running this app, different Label objects were updated at different times in an arbitrary order.

      相关问题:

      • Hook for my Vaadin web app starting and stopping?
      • How to access ServletContext from within a Vaadin 7 app?

      这篇关于Vaadin:返回数据后更新用户界面的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆