使用计时器在JSF托管的Bean中生成线程以进行定时任务

31

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

例如,假设我想创建一个定时任务,每天向每个注册成员发送一批电子邮件。 我尽可能地使用JSF,并且想知道这是否可行(我知道这听起来有点奇怪)。

到目前为止,我已经在ServletContextListener中使用了所有上述内容。(我不想使用任何应用程序服务器或cron job,并且我希望将上述内容保留在我的Web应用程序内。)

是否有一种聪明的JSF方法来完成这个任务,还是我应该坚持旧模式?

1个回答

75

介绍

如果您想通过#{managedBeanName}在视图中引用它,或者在其他托管的bean中使用@ManagedProperty("#{managedBeanName}")引用它,则仅在JSF托管的bean内部生成线程才有意义。 您应该确保实现@PreDestroy以确保所有这些线程在Web应用程序即将关闭时关闭,就像您在ServletContextListenercontextDestroyed()方法中执行的那样(是的,您做到了吗?)。 另请参见 是否安全在JSF托管的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,那么它会在卸载后继续运行。因此,在重新部署后,将创建一个新线程,再次执行相同的任务。等等。现在它已经成为了“fire and forget”,您不能以编程方式取消它。您基本上需要关闭并重新启动整个服务器以清除先前的线程。

  • 如果Timer线程未标记为守护线程,则它将阻止Web应用程序的卸载和服务器的关闭。您基本上需要强制关闭服务器。其主要缺点是Web应用程序将无法通过例如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.
    }

} 

这可以在托管bean中使用@EJB进行必要的调用:

@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();
    }

}

@BalusC 如果需要从调度程序引用另一个CDI bean,应该使用哪种方法最好?-谢谢。 - dertkw
@timbooo:按照通常的方式@Inject它即可。 - BalusC
@BalusC 但是将一个会话作用域的Bean注入到EJB Singleton中是不可能的,因为没有活动的会话上下文。有什么可能的解决方法? - dertkw
@timbooo:应该可以工作。CDI只是通过代理注入(就像@EJB一样),而不是通过引用注入(就像@ManagedProperty一样)。你只需要确保在正确的时刻访问它即可。如果无法保证,请以另一种方式注入它。 - BalusC
@BalusC 谢谢,你帮了我很多。但是我还是转而使用观察者模式来通知其他的“参与方” :-) - dertkw
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接