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

时间:2019-09-20 19:13:24

标签: tomcat vaadin vaadin8

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

3 个答案:

答案 0 :(得分:4)

如果 not 在Vaadin中更新用户界面

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

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

编写侦听器后,必须将其存在状态通知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." );
    }

}

请勿使用Thread类运行后台任务。那是老派。现代方法使用了后来添加到Java中的 Executor 框架。参见Oracle Tutorial

ExecutorService

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

ExecutorService executorService = Executors.newSingleThreadExecutor() ;

将任务定义为RunnableCallable

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

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

告诉您的执行服务运行该可运行文件。

executorService.submit ( runnable );

返回Future对象,作为您检查任务进度或完成情况的句柄。您可能并不在意使用它。

Future future = executorService.submit ( runnable );

将所有内容加在一起,加上代码以正常关闭我们的线程池(执行程序服务)。然后,我们在控制台消息上使用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 ();
        }
    }

}

ScheduledExecutorService

如果您想重复运行此任务(例如每五分钟一次),请在您的RunnableThread中管理该重复。关于separation of concerns,我们意识到您的后台任务应仅专注于其主要任务,例如更新数据库。计划何时应该发生以及应该何时发生是另一项工作,需要在其他地方进行处理。

在哪里处理调度?在为该杂项专门构建的执行程序服务中。 ScheduledExecutorService的实现具有可以一次运行任务的方法,可以有或没有延迟(等待时间)。或者,您可以调用方法来安排任务重复执行,例如每五分钟一次。

与以上代码相似。我们将ExecutorService更改为ScheduledExecutorService。然后我们将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 ();
        }
    }

}

重要提示: 在尝试捕获中将您的工作包装在Runnable中,以捕获可能冒出的所有Exception。如果您的预定执行程序服务遇到异常(或Error),则该服务将停止所有进一步的执行。您的后台任务会无声地,神秘地停止工作。如果对保持后台任务的持续性很重要,则最好捕获所有意外异常(可能还有错误,这值得商))并报告给系统管理员。

雅加达并发

如果您要部署到支持Jakarta Concurrency实用程序(最初是JSR 236)的应用服务器上,那么这项工作将变得非常简单。您无需写那个ServletContextListener。您可以使用注释使应用服务器自动运行Runnable

如果在Vaadin中更新用户界面

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

  • 后台线程正在做一段时间的工作,
  • 要更新的用户视图注册表,
  • 对视图感兴趣的视图的许多实例(Vaadin布局或小部件)。

换句话说,只有一个发布者的有限形式的Pub-Sub publisher and subscriber pattern

ServletContextListener实现是在Vaadin Web应用程序启动时(服务于任何用户之前)和Web App退出时(服务于最后一个用户之后)执行工作的一种方式。这是启动和关闭发布订阅发布者和注册表的好地方。

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

如果您的后台工作人员是在标点时间而不是连续时间运行,请了解执行程序框架,特别是ScheduledExecutorService。请确保正常关闭任何此类执行程序服务,因为它可能会超过您的Web应用程序甚至Serlet容器的寿命!如果使用功能强大的Jakarta EE服务器(例如Glassfish / Payara,WildFly等),而不是仅使用Servlet容器(例如Apache Tomcat或Eclipse Jetty),则可以使用{{3 }},可通过自动启动/关闭功能更轻松地运行托管的计划执行程序。

当您在Vaadin用户界面中实例化要更新的视图时,请让该布局或小部件在注册表中注册为对获取更新感兴趣。如Concurrency Utilities feature中所述,从网络应用的ServletContext中检索注册表。

我建议您的注册表保持Different ways to get Servlet Context感兴趣的视图。随着用户关闭其Web浏览器窗口/选项卡,这些视图最终将消失。您可以可以对已注册的窗口小部件进行编程,以在其生命周期内正常地从注册表中注销。但我怀疑使用弱引用将有助于确保万无一失。一种可能是使用仅具有键而没有值的WeakHashMap,其中每个键都是对为从后台线程更新而注册的小部件/布局实例的弱引用。

要让后台线程更新Vaadin Web应用程序的用户界面,请从不从后台线程访问Vaadin小部件。这样做乍看起来似乎可行,但最终您会遇到weak references冲突,并且可能会发生非常糟糕的事情。相反,了解Vaadin如何通过传递concurrency来通过access方法发布更新请求。而且您将想了解Runnable,以及如何Push technology非常容易。请注意该页面上的部分Vaadin makes Push,其描述与此答案基本相同。在此过程中,您可能会了解Broadcasting to Other Users的优点和局限性,Vaadin利用WebSockets库可以自动使用它们来实现Push。

在所有这些方面,您必须非常了解并发问题和实践,并且可能要注意volatile关键字。从定义上来说,Java Servlet容器是一个高度线程化的环境,现在您将自己编排这些线程。因此,您需要阅读,重读和认真学习Brian Goetz等着的优秀著作Atmosphere

写完所有这些之后,我意识到您的问题对于Stack Overflow来说确实太宽泛了。但是希望这个答案可以帮助您适应方向。您可以通过搜索堆栈溢出来了解有关每个难题的更多信息。尤其是,如果您搜索Stack Overflow,您会在Vaadin 8中找到一些关于这些主题的非常长的帖子,其中包含许多示例代码,并请查阅Java Concurrency in Practice。如果这是一项需要资金的至关重要的项目,请考虑从Vaadin Ltd公司租用培训和Vaadin Forums。您的项目 是可行的;我自己按照与我在此描述的大致相同的方式完成了这样的项目。这并不简单,但是有可能,而且是非常有趣的工作。

答案 1 :(得分:0)

Tomcat等待进程完成的原因是因为我使用的是thread.run()而不是thread.start()。

答案 2 :(得分:0)

@WebListener
public class Application implements ServletContextListener {

// threads
private static ThreadPoolExecutor onDemandExecutor;
private static ScheduledThreadPoolExecutor scheduledExecutorForDisablingAttendanceUpdation;

// thread pools
@Override
public void contextInitialized(ServletContextEvent sce) {
    // TODO Auto-generated method stub
    System.out.println("[INFO] Application starting up...");
    // thread pools
    onDemandExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(3);
    System.out.println("[INFO] Application up");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
    // TODO Auto-generated method stub
    System.out.println("Application shutting down...");

    // shutdown threads
    scheduledExecutorForDisablingAttendanceUpdation.shutdown();
    onDemandExecutor.shutdown();

    // shutdown database connection pool
    connPool.dispose();

    System.out.println("Application down.");
}

public static ThreadPoolExecutor getExecutor() {
    return onDemandExecutor;
}

public static Connection getConnection() throws IOException, SQLException {
    //      connect();
    return connPool.getConnection();
}
}