Vaadin:数据返回后更新UI

时间:2018-06-15 17:51:19

标签: spring vaadin

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

目前,当按下按钮时,页面会等待evalSql()在添加新Label之前从数据库中返回结果。

如何改变这一点,当按下按钮时,会立即添加一个新的Label,设置为初始占位符字符串(“获取结果..”),但在数据库返回后更新为结果字符串?

2 个答案:

答案 0 :(得分:3)

您需要在UI中使用@Push,并在使用UI.access(..)完成查询时更新Label的内容。这里有一些很好的文档,有一些例子:

https://vaadin.com/docs/v8/framework/advanced/advanced-push.html

答案 1 :(得分:2)

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

坏消息是,如果您不熟悉并发和线程,那么您就有了学习曲线。

异步

希望您的应用在后台执行某些操作并稍后无阻塞地进行检查的技术术语是:asynchronous更新。

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

推送技术

正如Label小部件的Answer by Lund更新中所讨论的,需要Push Technology从服务器端生成的事件更新浏览器。幸运的是,Vaadin 8 and later has excellent support for Push并使您的应用程序中的Push非常容易。

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

线程

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

如果您不熟悉线程,那么您将面临一些认真的学习。首先研究Oracle Tutorial on concurrency。最后,您需要阅读并重新阅读Brian Goetz等人的优秀书籍Java Concurrency in Practice几次。

ServletContextListener

您可能希望在Vaadin应用程序启动和退出时设置并拆除您的线程杂耍执行程序服务。这样做的方法是编写一个与Vaadin servlet类分开的类。该类必须实现ServletContextListener。您可以通过实现两个必需的方法并使用@WebListener注释类来轻松完成此操作。

务必拆除遗嘱执行人服务。否则,它管理的后台线程可能会在你的Vaadin应用程序关闭后继续存在,甚至关闭你的web container(Tomcat,Jetty等),继续无限期地运行。

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

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

Java EE

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

弹簧

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

搜索堆栈溢出

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

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

  • A contrived minimalist example,只是为了让您体验所涉及的内容。如答案中所述,这种特殊方法绝不应该用于实际工作中。
  • 对于实际工作,请参阅this Answer中发布的更复杂的示例应用。

完整示例

从由Vaadin Ltd.公司提供的Maven原型vaadin-archetype-application生成的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 {
    }
}

enter image description here

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

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

嗯,Vaadin是Java Servlet的一个实现,虽然是一个非常大而复杂的servlet。在Servlet术语中,您的Web应用程序称为“上下文”。 Servlet规范要求所有Servlet containers(例如Tomcat,Jetty等)都注意到任何标记为某些事件的监听器的类。为了利用这一点,我们必须在我们的Vaadin应用程序中添加另一个类,这是一个实现ServletContextListener接口的类。

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

因此,实现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应用程序。修改该文件:

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

如果您不熟悉线程和并发,这可能会令人生畏。研究这段代码,有一段时间沉入其中。你可以用其他方式构建它,但我想在教学目的这里简单一点。不是关注代码的结构/安排,而是关注:

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

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

Label

执行此类异步线程工作时,无法预测执行的确切顺序。您不确切知道后台线程何时以及以何种顺序执行。您不知道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个对象在不同时间以任意顺序更新。

enter image description here

相关问题: