【问题标题】:Call method at date/time在日期/时间调用方法
【发布时间】:2023-11-13 09:21:01
【问题描述】:

我正在寻找一种现代方式来在给定日期/时间(尤其是ZonedDateTime)执行给定方法。

我知道 Timer 类和 Quartz 库,如下所示(线程包括完整的解决方案):

但是这些线程相当陈旧,从那时起就没有使用新的 Java 特性和库元素。特别是,获得任何类型的 Future 对象将非常方便,因为它们提供了一种简单的机制来取消它们。

所以请不要提出涉及TimerQuartz 的解决方案。另外,我想要一个 vanilla 解决方案,不使用任何外部库。但是,为了问答,您也可以随意提出建议。

【问题讨论】:

    标签: java datetime future java-time


    【解决方案1】:

    ScheduledExecutorService

    您可以使用自 Java 5 起可用的 ScheduledExecutorService (documentation) 类。它将产生一个 ScheduledFuture (documentation) 可用于监视执行并取消它。

    特别是方法:

    ScheduledFuture<?> schedule​(Runnable command, long delay, TimeUnit unit)
    

    哪个

    提交在给定延迟后启用的一次性任务。

    但您也可以查看其他方法,具体取决于实际用例(scheduleAtFixedRate 和接受Callable 而不是Runnable 的版本)。

    自从 Java 8(Streams、Lambdas...)以来,由于旧的TimeUnit 和新的ChronoUnit(用于您的ZonedDateTime)之间的简单转换方法的可用性,这个类变得更加方便,以及提供 Runnable command 作为 lambda 或方法引用的能力(因为它是 FunctionalInterface)。


    示例

    让我们看一个执行您要求的示例:

    // Somewhere before the method, as field for example
    // Use other pool sizes if desired
    ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    
    public static ScheduledFuture<?> scheduleFor(Runnable runnable, ZonedDateTime when) {
        Instant now = Instant.now();
        // Use a different resolution if desired
        long secondsUntil = ChronoUnit.SECONDS.between(now, when.toInstant());
    
        return scheduler.schedule(runnable, secondsUntil, TimeUnit.of(ChronoUnit.SECONDS));
    }
    

    调用很简单:

    ZonedDateTime when = ...
    ScheduledFuture<?> job = scheduleFor(YourClass::yourMethod, when);
    

    然后,您可以使用job 来监控执行并在需要时取消它。示例:

    if (!job.isCancelled()) {
        job.cancel(false);
    }
    

    注意事项

    你可以将方法中的ZonedDateTime参数换成Temporal,那么它也可以接受其他日期/时间格式。

    完成后不要忘记关闭ScheduledExecutorService。否则即使你的主程序已经完成,你也会有一个线程在运行。

    scheduler.shutdown();
    

    请注意,我们使用Instant 而不是ZonedDateTime,因为区域信息与我们无关,只要正确计算时间差即可。 Instant 始终表示 UTC 时间,没有任何 奇怪 现象,如 DST。 (虽然这对这个应用程序来说并不重要,但它更干净)。

    【讨论】:

    • 好答案。为清楚起见,我建议在 scheduleFor 方法中通过调用 toInstant 转换为使用 Instant 而不是 ZonedDateTime。虽然它在这种情况下有效,但在没有明确时区的情况下显示 ZonedDateTime.now() 为人们学习日期时间处理树立了一个坏榜样。不知道时区是编程中常见的麻烦来源。 Instant 始终采用 UTC,因此使用它可以消除这种歧义。
    • @BasilBourque 感谢您的评论,不过我有一个问题。我认为ChronoUnit#between 知道ZonedDateTime 中设置的时区,因此无论now() 中设置了哪个区域(这是系统默认区域),都会正确比较它。 IE。没关系,between 的结果总是正确的。是这样吗,还是我错了?
    • 我相信您的代码正确,并且结果正确。正如我在评论中提到的,在您的代码上下文中,您对 ZonedDateTime 的使用将成功。我担心其他人学习日期时间处理,看到ZonedDateTime.now(),然后在他们自己的工作中调用该方法,sans 可选区域参数,而不了解 JVM 当前的含义默认时区在运行时隐式应用。此外,在您的方法中使用 Instant 可以清楚地表明我们正在计算秒数,与时区问题没有任何具体相关性。