Vaadin:数据返回后更新 UI

Ray*_*ang 2 spring vaadin vaadin8

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

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

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

Bas*_*que 6

好消息是,您想要的 Vaadin 用户界面中的小部件稍后可以通过服务器后台完成的工作进行更新,而不会阻止 UI 对用户的响应,这是可以实现的。使用 Vaadin 及其基于 Java 的后端可以很好地完成此任务。

\n\n

坏消息是,如果您不熟悉并发和线程,那么您需要爬上一个学习曲线。

\n\n

异步

\n\n

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

\n\n

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

\n\n

推送技术

\n\n

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

\n\n

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

\n\n

线程数

\n\n

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

\n\n

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

\n\n

ServletContextListener

\n\n

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

\n\n

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

\n\n

切勿从后台线程访问小部件

\n\n

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

\n\n

JavaEE

\n\n

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

\n\n

春天

\n\n

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

\n\n

搜索堆栈溢出

\n\n

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

\n\n

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

\n\n\n\n

完整示例

\n\n

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

\n\n
package com.basilbourque.example;\n\nimport javax.servlet.annotation.WebServlet;\n\nimport com.vaadin.annotations.Theme;\nimport com.vaadin.annotations.VaadinServletConfiguration;\nimport com.vaadin.server.VaadinRequest;\nimport com.vaadin.server.VaadinServlet;\nimport com.vaadin.ui.Button;\nimport com.vaadin.ui.Label;\nimport com.vaadin.ui.TextField;\nimport com.vaadin.ui.UI;\nimport com.vaadin.ui.VerticalLayout;\n\n/**\n * This UI is the application entry point. A UI may either represent a browser window\n * (or tab) or some part of an HTML page where a Vaadin application is embedded.\n * <p>\n * The UI is initialized using {@link #init(VaadinRequest)}. This method is intended to be\n * overridden to add component to the user interface and initialize non-component functionality.\n */\n@Theme ( "mytheme" )\npublic class MyUI extends UI {\n\n    @Override\n    protected void init ( VaadinRequest vaadinRequest ) {\n        final VerticalLayout layout = new VerticalLayout();\n\n        final TextField name = new TextField();\n        name.setCaption( "Type your name here:" );\n\n        Button button = new Button( "Click Me" );\n        button.addClickListener( e -> {\n            layout.addComponent( new Label( "Thanks " + name.getValue() + ", it works!" ) );\n        } );\n\n        layout.addComponents( name , button );\n\n        setContent( layout );\n    }\n\n    @WebServlet ( urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true )\n    @VaadinServletConfiguration ( ui = MyUI.class, productionMode = false )\n    public static class MyUIServlet extends VaadinServlet {\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

在此输入图像描述

\n\n

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

\n\n

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

\n\n

Vaadin 是Java Servlet的一个实现,尽管它是一个非常大且复杂的 servlet。在 Servlet 术语中,您的 Web 应用程序称为 \xe2\x80\x9ccontext\xe2\x80\x9d。Servlet 规范要求所有Servlet 容器(例如 Tomcat、Jetty 等)注意任何标记为某些事件侦听器的类。为了利用这一点,我们必须向 Vaadin 应用程序添加另一个类,即实现该ServletContextListener接口的类。

\n\n

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

\n\n

因此,我们实现的类ServletContextListener是使用其后备线程池设置和拆除执行程序服务的完美位置。

\n\n

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

\n\n

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

\n\n
package com.basilbourque.example;\n\nimport javax.servlet.ServletContextEvent;\nimport javax.servlet.ServletContextListener;\nimport javax.servlet.annotation.WebListener;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\n@WebListener\n// Annotate to instruct your web container to notice this class as a context listener and then automatically instantiate as context (your web app) lanuches.\npublic class MyServletContextListener implements ServletContextListener {\n    static final public String executorServiceNameForUpdatingLabelAfterSqlService = "ExecutorService for SQL service update of labels";\n\n    @Override\n    public void contextInitialized ( final ServletContextEvent sce ) {\n        // Initialize an executor service. Store a reference as a context attribute for use later in our webapp\xe2\x80\x99s Vaadin servlet.\n        ExecutorService executorService = Executors.newFixedThreadPool( 7 );  // Choose an implementation and number of threads appropriate to demands of your app and capabilities of your deployment server.\n        sce.getServletContext().setAttribute( MyServletContextListener.executorServiceNameForUpdatingLabelAfterSqlService , executorService );\n    }\n\n    @Override\n    public void contextDestroyed ( final ServletContextEvent sce ) {\n        // Always shutdown your ExecutorService, otherwise the threads may survive shutdown of your web app and perhaps even your web container.\n\n        // The context addribute is stored as `Object`. Cast to `ExecutorService`.\n        ExecutorService executorService = ( ExecutorService ) sce.getServletContext().getAttribute( MyServletContextListener.executorServiceNameForUpdatingLabelAfterSqlService );\n        if ( null != executorService ) {\n            executorService.shutdown();\n        }\n\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

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

如果您不熟悉线程和并发,这可能会令人望而生畏。研究这段代码,需要一些时间来理解。您可以用其他方式构建它,但出于教学目的,我想让它变得简单。不要关注代码的结构/安排,而是关注以下想法:

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

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

\n\n
package com.basilbourque.example;\n\nimport javax.servlet.ServletContext;\nimport javax.servlet.annotation.WebServlet;\n\nimport com.vaadin.annotations.Push;\nimport com.vaadin.annotations.Theme;\nimport com.vaadin.annotations.VaadinServletConfiguration;\nimport com.vaadin.server.VaadinRequest;\nimport com.vaadin.server.VaadinServlet;\nimport com.vaadin.ui.Button;\nimport com.vaadin.ui.Label;\nimport com.vaadin.ui.TextField;\nimport com.vaadin.ui.UI;\nimport com.vaadin.ui.VerticalLayout;\n\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.util.UUID;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * This UI is the application entry point. A UI may either represent a browser window\n * (or tab) or some part of an HTML page where a Vaadin application is embedded.\n * <p>\n * The UI is initialized using {@link #init(VaadinRequest)}. This method is intended to be\n * overridden to add component to the user interface and initialize non-component functionality.\n */\n@Push  // This annotation enables the Push Technology built into Vaadin 8.4.\n@Theme ( "mytheme" )\npublic class MyUI extends UI {\n\n    @Override\n    protected void init ( VaadinRequest vaadinRequest ) {\n        final VerticalLayout layout = new VerticalLayout();\n\n        final TextField name = new TextField();\n        name.setCaption( "Type your name here:" );\n\n        Button button = new Button( "Click Me" );\n        button.addClickListener( ( Button.ClickEvent e ) -> {\n            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.\n            layout.addComponent( label );  // Notes current date-time when this object was created.\n\n            //\n            ServletContext servletContext = VaadinServlet.getCurrent().getServletContext();\n            // The context attribute is stored as `Object`. Cast to `ExecutorService`.\n            ExecutorService executorService = ( ExecutorService ) servletContext.getAttribute( MyServletContextListener.executorServiceNameForUpdatingLabelAfterSqlService );\n            if ( null == executorService ) {\n                System.out.println( "ERROR - Failed to find executor serivce." );\n            } else {\n                executorService.submit( new Runnable() {\n                    @Override\n                    public void run () {\n                        // Pretending to access our SQL service. To fake it, let\'s sleep this thread for a random number of seconds.\n                        int seconds = ThreadLocalRandom.current().nextInt( 4 , 30 + 1 ); // Pass ( min , max + 1 )\n                        try {\n                            Thread.sleep( TimeUnit.SECONDS.toMillis( seconds ) );\n                        } catch ( InterruptedException e ) {\n                            e.printStackTrace();\n                        }\n                        // Upon waking, ask that our `Label` be updated.\n                        ZonedDateTime zdt = ZonedDateTime.now( ZoneId.systemDefault() );\n                        System.out.println( "Updating label at " + zdt );\n                        access( new Runnable() {  // Calling `UI::access( Runnable )`, asking that this Runnable be run on the main UI thread rather than on this background thread.\n                            @Override\n                            public void run () {\n                                label.setValue( label.getValue() + " Updated: " + zdt );\n                            }\n                        } );\n                    }\n                } );\n            }\n        } );\n\n        layout.addComponents( name , button );\n\n        setContent( layout );\n    }\n\n\n    @WebServlet ( urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true )\n    @VaadinServletConfiguration ( ui = MyUI.class, productionMode = false )\n    public static class MyUIServlet extends VaadinServlet {\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

在执行此类异步线程工作时,无法预测确切的执行顺序。您不知道后台线程执行的确切时间和顺序。你不知道该UI对象什么时候会收到我们更新Labelobject\xe2\x80\x99s文本的请求。请注意,在此屏幕截图中,运行此应用程序时,不同的Label对象在不同时间以任意顺序更新。

\n\n

在此输入图像描述

\n\n

相关问题:

\n\n\n


归档时间:

查看次数:

2655 次

最近记录:

3 年,5 月 前