Vaadin 8 应用程序中的后台进程

Eig*_*ice 1 tomcat vaadin vaadin8

在我在 Tomcat 上运行的 Vaadin 8 应用程序中,应该有一个用于刷新和更新数据库的后台进程。如果我使用 ServletContextListener 将它与主 UI 分开,Tomcat 将不会完成启动,直到它完成执行 contextInitialized 中的所有指令,并且因为我想将它保持在调用数据库的单独线程上的无限循环中,然后睡眠 5 分钟,该应用程序从未真正启动。实现这一点的正确方法是什么?

Bas*_*que 5

如果更新 Vaadin 中的用户界面

您的问题被标记为 Vaadin,但似乎只询问运行后台任务而不考虑 Vaadin 用户界面。如果是这样,您是在问一个通用的Jakarta Servlet问题,而不是 Vaadin 特定的问题。Vaadin 只是一个Servlet,尽管它是一个非常大、复杂的 Servlet。

正如您所指出的,ServletContextListener在 Web 应用程序启动时,在为第一个用户提供服务之前,在contextInitialized方法中编写一个实现 的类是运行代码的地方。这是在您的 Web 应用程序退出时运行代码的地方,在为最后一个用户提供服务后,在contextDestroyed方法中。

编写完侦听器后,必须通知 Servlet 容器(例如 Apache Tomcat 或 Eclipse Jetty)它的存在。最简单的方法是添加@WebListener注释。

package com.example.acme;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

/**
 *
 * @author Basil Bourque
 */
@WebListener
public class AcmeServletContextListener implements ServletContextListener {

    @Override
    public void contextInitialized ( ServletContextEvent sce ) {
        System.out.println ( "Acme Vaadin web app is starting. " );
    }

    @Override
    public void contextDestroyed ( ServletContextEvent sce ) {
        System.out.println ( "Acme Vaadin web app is exiting." );
    }

}
Run Code Online (Sandbox Code Playgroud)

不要使用Thread类运行后台任务。那是老派。现代方法使用后来添加到 Java的Executor框架。请参阅Oracle 教程

ExecutorService

为了只运行一个后台任务,我们需要一个只有一个线程的线程池。使用Executors该类来实例化线程池。

ExecutorService executorService = Executors.newSingleThreadExecutor() ;
Run Code Online (Sandbox Code Playgroud)

将您的任务定义为RunnableCallable

Runnable runnable = new Runnable ()
{
    @Override
    public void run ( )
    {
        System.out.println ( "INFO - Acme web app doing some work on background thread. " + Instant.now () );
    }
};
Run Code Online (Sandbox Code Playgroud)

Runnable如果你愿意,你可以使用更紧凑的 lambda 语法来定义你的。为了清楚起见,我在这里使用了长语法。

告诉您的执行程序服务运行该 runnable。

executorService.submit ( runnable );
Run Code Online (Sandbox Code Playgroud)

Future返回一个对象,作为您检查任务进度或完成的句柄。你可能不在乎使用它。

Future future = executorService.submit ( runnable );
Run Code Online (Sandbox Code Playgroud)

把所有这些放在一起,加上代码来优雅地关闭我们的线程池(执行程序服务)。我们在控制台消息上添加了一些时间戳Instant.now()

public class AcmeServletContextListener implements ServletContextListener {
    private ExecutorService executorService ; 

    @Override
    public void contextInitialized ( ServletContextEvent sce ) {
        System.out.println ( "Acme Vaadin web app is starting. " + Instant.now () );
        this.executorService = Executors.newSingleThreadExecutor() ;
        Runnable runnable = new Runnable ()
        {
            @Override
            public void run ( )
            {
                System.out.println ( "INFO - Acme web app doing some work on background thread. " + Instant.now () );
            }
        };
        Future future = this.executorService.submit ( runnable );
    }

    @Override
    public void contextDestroyed ( ServletContextEvent sce ) {
        System.out.println ( "Acme Vaadin web app is exiting. " + Instant.now () );
        if ( Objects.nonNull ( executorService ) )
        {
            this.executorService.shutdown ();
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

ScheduledExecutorService

如果您想重复运行此任务,例如每五分钟运行一次,请不要在您的RunnableThread. 关于关注点分离,我们意识到您的后台任务应该只关注其主要任务,例如更新数据库。安排什么时候应该发生,应该多久发生一次,是一项不同的工作,需要在其他地方处理。

在哪里处理调度?在专为这项工作而构建的执行程序服务中。ScheduledExecutorServicehas 方法的实现可以运行一次任务,有或没有延迟(等待期)。或者您可以调用方法来安排重复的任务,比如每五分钟一次。

与上面看到的类似的代码。我们更改ExecutorServiceScheduledExecutorService. 我们Executors.newSingleThreadExecutor()改为Executors.newSingleThreadScheduledExecutor(). 我们使用TimeUnit枚举指定初始延迟和重复周期。这里我们使用TimeUnit.MINUTES,初始延迟为 2(在第一次运行前等待两分钟),每五分钟间隔一次。如果您想使用Future,它现在是一种ScheduledFuture类型。

public class AcmeServletContextListener implements ServletContextListener {
    private ScheduledExecutorService executorService ; 

    @Override
    public void contextInitialized ( ServletContextEvent sce ) {
        System.out.println ( "Acme Vaadin web app is starting. " + Instant.now () );
        // Instantiate a thread pool and scheduler.
        this.executorService = Executors.newSingleThreadScheduledExecutor() ;
        // Define the task to be done.
        Runnable runnable = new Runnable ()
        {
            @Override
            public void run ( )
            {
                System.out.println ( "INFO - Acme web app doing some work on background thread. " + Instant.now () );
            }
        };
        // Tell the scheduler to run the task repeatedly at regular intervals, after an initial delay. 
        ScheduledFuture future = this.executorService.scheduleAtFixedRate? ( runnable , 2 , 5 , TimeUnit.MINUTES );
    }

    @Override
    public void contextDestroyed ( ServletContextEvent sce ) {
        System.out.println ( "Acme Vaadin web app is exiting. " + Instant.now () );
        if ( Objects.nonNull ( executorService ) )
        {
            this.executorService.shutdown ();
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

重要提示: 将您的工作包裹在 Runnable 中的 try-catch 中,以捕获任何Exception可能冒泡的内容。如果异常(或Error)到达您计划的执行程序服务,该服务将停止所有进一步的执行。您的后台任务停止工作,默默地,神秘地。如果保持后台任务运行对您很重要,最好捕获所有意外异常(可能还有错误,这是有争议的)并向您的系统管理员报告。

雅加达并发

如果您要部署到支持Jakarta Concurrency实用程序(最初是JSR 236)的应用程序服务器,则这项工作会容易得多。你不必这样写ServletContextListener。您可以使用注释让应用服务器自动运行您的Runnable.

如果在 Vaadin 中更新用户界面

也许您想要一个后台工作人员,每五分钟更新一些用户的显示。如果是这样,您需要几个部分:

  • 后台线程做一些周期工作,
  • 要更新的用户视图注册表,
  • 许多有兴趣更新的视图实例(Vaadin 布局或小部件)。

换句话说,一种有限形式的 Pub-Sub、发布者和订阅者模式,只有一个发布者。

一个ServletContextListener执行是执行,当你Vaadin Web应用程序正在启动(服务的任何用户之前),当Web应用程序正在退出(服务的最后一个用户后)工作的一种方式。这是启动和关闭发布-订阅发布者和注册中心的好地方。

您可以在发送到您的ServletContextListener实现的 Servlet 上下文中全局保留对您的注册表对象的引用。使用“属性”功能,一键收藏价值通过访问setAttribute/ getAttribute/removeAttribute方法。

如果您的后台工作人员以间断时间运行而不是连续运行,请了解 executors 框架,特别是ScheduledExecutorService. 一定要优雅地关闭任何这样的执行器服务,因为它可以比你的 web 应用程序甚至你的 Serlet 容器更长寿!如果使用成熟的Jakarta EE服务器(例如 Glassfish/Payara、WildFly 等)而不是单纯的 Servlet 容器(例如 Apache Tomcat 或 Eclipse Jetty),您可能能够更轻松地使用并发实用程序功能运行具有自动启动/关闭功能的托管计划执行程序。

当您在 Vaadin 用户界面中实例化要更新的视图时,让该布局或小部件在注册表中注册为对获取更新感兴趣。ServletContext获取 Servlet 上下文的不同方式中所述,从 Web 应用程序检索注册表。

我建议您的注册表保留对感兴趣的视图的弱引用。随着用户关闭他们的 Web 浏览器窗口/选项卡,这些视图最终都会消失。作为其生命周期的一部分,您可以对已注册的小部件进行编程,使其从您的注册表中优雅地注销。但我怀疑使用弱引用将有助于确保万无一失。一种可能性是使用WeakHashMap只有键,没有值的 ,其中每个键都是对从后台线程注册更新的小部件/布局实例的弱引用。

要让后台线程更新 Vaadin Web 应用程序的用户界面,切勿从后台线程访问 Vaadin 小部件。这样做起初似乎可行,但最终您会遇到并发冲突,并且可能会发生非常糟糕的事情。相反,了解 Vaadin 如何access通过传递Runnable. 您将想要了解Push 技术,以及Vaadin如何让 Push 变得非常简单。请注意该页面上的“向其他用户广播”部分,它的描述与此答案大致相同。在此过程中,您可能会了解WebSockets的优点和局限性,可以由 Vaadin 利用的Atmosphere库自动使用以实现推送。

在所有这些过程中,您必须非常注意并发问题和实践,可能还有volatile关键字。根据定义,Java Servlet 容器首先是一个高度线程化的环境,现在您将对这些线程进行自己的编排。因此,您需要阅读、重读并努力学习Brian Goetz 等人所著的优秀书籍Java Concurrency in Practice

在写完所有这些之后,我现在意识到你的问题对于 Stack Overflow 来说真的太宽泛了。但希望这个答案能帮助你找到方向。您可以通过搜索 Stack Overflow 来了解有关每个拼图的更多信息。特别是,如果您搜索 Stack Overflow,您会在 Vaadin 8 中找到我就这些主题撰写的一些很长的帖子,其中包含大量示例代码。也可以参考Vaadin 论坛。如果这是一个需要资金的重要项目,请考虑聘请Vaadin Ltd 公司提供的培训和咨询服务。你的项目可行的;我自己也按照我在这里描述的方式完成了这样一个项目。这并不简单,但它是可能的,而且是一项非常有趣的工作。