在运行于Tomcat上的Vaadin 8应用程序中,应该有一个用于刷新和更新数据库的后台进程。如果我使用ServletContextListener将其与主UI分开,则Tomcat不会完成启动,直到它完成contextInitialized中的所有指令执行为止,并且由于我想将其保持在调用数据库的单独线程上的无限循环中,然后睡眠5分钟,该应用程序实际上从未启动。正确的方法是什么?
答案 0 :(得分:4)
您的问题被标记为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() ;
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
如果您想重复运行此任务(例如每五分钟一次),请不在您的Runnable
或Thread
中管理该重复。关于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
。
也许您需要一个后台工作人员每五分钟更新一次某些用户的显示。如果是这样,则需要几个部分:
换句话说,只有一个发布者的有限形式的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();
}
}