首页 > 解决方案 > Vaadin:数据返回后更新 UI

问题描述

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

目前,当按下按钮时,页面会等待 evalSql() 从数据库中返回结果,然后再添加新标签。

我该如何更改它,当按下按钮时,会立即添加一个新标签,设置为初始占位符字符串(“Fetching result..”),但在数据库返回某些内容后更新为结果字符串?

标签: springvaadin

解决方案


好消息是,您希望在 Vaadin 用户界面中拥有一个小部件,稍后通过服务器后台完成的工作进行更新,而不会阻止 UI 对用户的响应。它可以通过 Vaadin 及其基于 Java 的后端很好地完成。

坏消息是,如果您不熟悉并发和线程,则需要攀登学习曲线。

异步

希望您的应用程序在后台执行某些操作并稍后再检查而不会阻塞的技术术语是:异步更新。

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

推动技术

正如Lund 的回答中所讨论的,更新Label小部件需要推送技术从服务器端生成的事件更新浏览器。幸运的是,Vaadin 8 及更高版本对 Push 提供了出色的支持,并使在您的应用程序中建立 Push 变得异常容易。

提示:总体而言,推送,尤其是WebSocket,近年来有了很大的发展。使用最新一代的 Servlet 容器将改善您的体验。例如,如果使用 Tomcat,我建议使用最新版本的 Tomcat 8.5 或 9。

线程

Java 对线程有很好的支持。许多必要的工作都由 Java 内置的 Executor 框架为您处理。

如果您是线程新手,那么您需要认真学习。首先学习Oracle 并发教程。最终,您需要反复阅读 Brian Goetz 等人的优秀书籍Java Concurrency in Practice

ServletContextListener

当您的 Vaadin 应用程序启动和退出时,您可能希望设置和拆除线程杂耍执行器服务。这样做的方法是编写一个与您的 Vaadin servlet 类分开的类。这个类必须实现ServletContextListener. 您可以通过实现两个必需的方法并使用@WebListener.

一定要拆除执行器服务。否则,它管理的后台线程可能会在您的 Vaadin 应用程序关闭甚至您的Web 容器(Tomcat、Jetty 等)关闭后仍然存在,继续无限期地运行。

永远不要从后台线程访问小部件

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

Java EE

如果您碰巧使用的是成熟的 Jakarta EE(以前称为 Java EE)服务器,而不是 Web 容器(如 Tomcat 或 Jetty)或 Web Profile 服务器(如 TomEE),那么上面讨论的工作与执行人服务,ServletContextListener为您完成。使用 Java EE 7 及更高版本中定义的功能:JSR 236:JavaTM EE 的并发实用程序

春天

您的问题带有Spring标记。Spring 可能具有帮助完成这项工作的功能。我不知道,因为我不是 Spring 用户。也许是Spring TaskExecutor

搜索堆栈溢出

如果您搜索 Stack Overflow,您会发现所有这些主题都已解决。

我已经发布了两个完整的示例应用程序,展示了使用 Push 的 Vaadin:

完整示例

vaadin-archetype-application从由 Vaadin Ltd. 公司提供的 Maven 原型生成的 Vaadin 8.4.3 应用程序开始。

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 浏览器窗口上添加的所有新标签。问题是我们在哪里设置、存储和拆卸执行器服务对象?

我们希望执行器服务可用于我们 Web 应用程序的整个生命周期。在第一个用户请求到达我们新推出的 Web 应用程序之前,我们要设置执行器服务。当我们试图关闭我们的 Web 应用程序时,我们需要拆除该执行程序服务,以便终止其支持线程池中的线程。我们如何与 Vaadin Web 应用程序的生命周期联系起来?

好吧,Vaadin 是Java Servlet的一个实现,尽管它是一个非常大且复杂的 servlet。在 Servlet 术语中,您的 Web 应用程序称为“上下文”。Servlet 规范要求所有Servlet 容器(例如 Tomcat、Jetty 等)通知任何标记为特定事件侦听器的类。为了利用这一点,我们必须向我们的 Vaadin 应用程序添加另一个类,一个实现ServletContextListener接口的类。

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

因此,我们实现的类ServletContextListener是设置和拆除我们的执行程序服务及其支持线程池的理想场所。

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

具有讽刺意味的是,上面的讨论比实际代码要长!

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 应用程序。修改那个文件:

  • 注释 Servlet 以@Push利用 Vaadin 让服务器端生成的事件更新用户界面小部件的能力。
  • 修改创建每个Label.
    • 将 的初始文本更改为Label包含带有当前日期时间的“已创建:”。
    • 将实例化移动到它自己的行。
  • 添加行为,以便在实例化一个新的 之后Label,我们从上下文属性集合中检索我们的执行器服务,并提交给它Runnable最终将运行以执行我们的 SQL 服务。为了模拟该 SQL 服务的工作,我们将后台线程随机休眠半分钟以下的秒数。唤醒后,该后台线程要求我们的UI对象代表我们在 Web 浏览器中显示的 Web 应用程序的内容,以安排另一个Runnable最终在其主用户界面线程上运行。如上所述,永远不要从后台线程直接访问 UI 小部件!始终礼貌地要求UI对象在自己的线程中按照自己的时间表安排与小部件相关的工作。

如果您不熟悉线程和并发,这可能会让人望而生畏。研究这段代码,并花一些时间沉浸其中。你可以用其他方式来构建它,但我想在这里简化它以用于教学目的。不关注代码的结构/安排,而是关注以下想法:

  • 用户单击按钮,这是 Vaadin UI 主线程中的一个事件。
  • Runnable按钮上的代码将稍后在后台线程中运行的任务(a)提交给执行程序服务。
  • 该后台线程在最终运行时会调用您的 SQL 服务以完成一些工作。完成后,我们Runnable向 UI 发布一个请求(另一个),以代表我们执行一些与小部件相关的工作(我们的Label文本更新)。
  • 当 UI 方便时,当它不太忙于处理用户界面中生成的其他与用户相关的事件时,UI 会开始运行我们Runnable来实际修改Label前一段时间添加的文本。

这是我们修改后的 Vaadin 应用程序。

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的对象在不同的​​时间以任意顺序更新。

在此处输入图像描述

相关问题:


推荐阅读