使用计时器在 JSF 托管 bean 中为计划任务生成线程

Posted

技术标签:

【中文标题】使用计时器在 JSF 托管 bean 中为计划任务生成线程【英文标题】:Spawning threads in a JSF managed bean for scheduled tasks using a timer 【发布时间】:2011-11-21 21:47:11 【问题描述】:

我想知道在应用程序范围的 bean 中使用 Timer 是否可以。

例如,假设我想创建一个计时器任务,每天一次向每个注册会员发送一堆电子邮件。我正在尝试尽可能多地使用 JSF,我想知道这是否可以接受(我知道这听起来有点奇怪)。

到目前为止,我已经在 ServletContextListener 中使用了以上所有内容。 (我不想使用任何应用程序服务器或 cron 作业,我想保留 我的网络应用程序中的上述内容。)

有没有一种聪明的 JSF 方法可以做到这一点,还是我应该坚持旧模式?

【问题讨论】:

【参考方案1】:

简介

至于从 JSF 托管 bean 内部生成线程,只有如果您希望能够通过 #managedBeanName 在您的视图中引用它或通过 @ 在其他托管 bean 中引用它,这将是有意义的987654321@。您只应确保实现 @PreDestroy 以确保在 web 应用程序即将关闭时关闭所有这些线程,就像您在 ServletContextListenercontextDestroyed() 方法中所做的那样(是的,您这样做了?)。另见Is it safe to start a new thread in a JSF managed bean?

切勿在 Java EE 中使用 java.util.Timer

至于在 JSF 托管 bean 中使用 java.util.Timer,您应该绝对不要使用老式的 Timer,而应使用现代的 ScheduledExecutorServiceTimer 存在以下主要问题,使其不适合在长时间运行的 Java EE Web 应用程序中使用(引自 Java Concurrency in Practice):

Timer 对系统时钟的变化敏感,ScheduledExecutorService 不敏感。 Timer 只有一个执行线程,所以长时间运行的任务会延迟其他任务。 ScheduledExecutorService 可以配置任意数量的线程。 TimerTask 中引发的任何运行时异常都会杀死那个线程,从而使Timer 死掉,即计划任务将不再运行。 ScheduledThreadExecutor 不仅可以捕获运行时异常,还可以让您根据需要处理它们。抛出异常的任务将被取消,但其他任务将继续运行。

除了书名,我还能想到更多的缺点:

如果您忘记显式 cancel()Timer,则它会在取消部署后继续运行。因此,在重新部署后,创建了一个新线程,再次执行相同的工作。等等。现在它已经变成了“一劳永逸”,你不能再以编程方式取消它了。您基本上需要关闭并重新启动整个服务器以清除以前的线程。

如果Timer线程没有被标记为守护线程,那么它将阻止webapp的取消部署和服务器的关闭。你基本上需要硬杀服务器。主要缺点是 webapp 将无法通过例如执行优雅的清理。 contextDestroyed()@PreDestroy 方法。

EJB 可用吗?使用@Schedule

如果您的目标是 Java EE 6 或更高版本(例如 JBoss AS、GlassFish、TomEE 等,因此不是准系统 JSP/Servlet 容器,例如 Tomcat),那么使用 @Singleton EJB 和一个 @Schedule 方法代替。这样容器就会担心通过ScheduledExecutorService 来池化和销毁线程。那么你只需要以下 EJB:

@Singleton
public class BackgroundJobManager 

    @Schedule(hour="0", minute="0", second="0", persistent=false)
    public void someDailyJob() 
        // Do your job here which should run every start of day.
    

    @Schedule(hour="*/1", minute="0", second="0", persistent=false)
    public void someHourlyJob() 
        // Do your job here which should run every hour of day.
    

    @Schedule(hour="*", minute="*/15", second="0", persistent=false)
    public void someQuarterlyJob() 
        // Do your job here which should run every 15 minute of hour.
    

 

如有必要,@EJB 可以在托管 bean 中使用:

@EJB
private BackgroundJobManager backgroundJobManager;

EJB 不可用?使用ScheduledExecutorService

如果没有 EJB,您需要手动使用 ScheduledExecutorService。应用程序范围的托管 bean 实现看起来像这样:

@ManagedBean(eager=true)
@ApplicationScoped
public class BackgroundJobManager 

    private ScheduledExecutorService scheduler; 

    @PostConstruct
    public void init() 
        scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(new SomeDailyJob(), 0, 1, TimeUnit.DAYS);
    

    @PreDestroy
    public void destroy() 
        scheduler.shutdownNow();
    


SomeDailyJob 看起来像这样:

public class SomeDailyJob implements Runnable 

    @Override
    public void run() 
        // Do your job here.
    


如果您根本不需要在视图或其他托管 bean 中引用它,那么最好使用 ServletContextListener 使其与 JSF 分离。

@WebListener
public class BackgroundJobManager implements ServletContextListener 

    private ScheduledExecutorService scheduler;

    @Override
    public void contextInitialized(ServletContextEvent event) 
        scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(new SomeDailyJob(), 0, 1, TimeUnit.DAYS);
    

    @Override
    public void contextDestroyed(ServletContextEvent event) 
        scheduler.shutdownNow();
    


【讨论】:

我记得使用 contextDestroyed() :P 注意到 ScheduledExecutorService。我只需要它来显示上次计划执行的时间并杀死/重新运行它。再次感谢! 我改进了反对Timer 的论点的答案。我真的会重新考虑使用ScheduledExecutorService @BalusC 如果需要从调度程序引用另一个 CDI bean 怎么办? - 什么方法最好用? - 谢谢。 @timbooo:@Inject 照常使用。 @BalusC 但是不可能将会话范围的 bean 注入到 ejb Singleton 中,因为没有活动的会话上下文。有什么可能的解决方法?

以上是关于使用计时器在 JSF 托管 bean 中为计划任务生成线程的主要内容,如果未能解决你的问题,请参考以下文章

Spring JSF 集成:如何在 JSF 托管 bean 中注入 Spring 组件/服务?

在页面加载时调用 JSF 托管 bean 操作

使用 URL 参数的 JSF 托管 Bean 方法调用

如何以编程方式注册 JSF 托管 bean?

JSF 获取托管 bean 中的当前操作

CDI 托管 bean 和 JSF 托管 bean 可以相互通信吗?