java.util.Timer 调度程序每天都会出错,因为夏令时

发布于 2025-01-12 23:18:32 字数 799 浏览 5 评论 0 原文

我下面的代码运行良好,该作业每天都会在正确的时间执行,但夏令时更改除外。它发生在十月底和三月。我的服务器有一个轻量级硬件,它位于使用它的区域。 我怎样才能让它工作,使用最少的资源(没有石英或任何类似的)

// init daily scheduler 
final java.util.Timer timer = new java.util.Timer("MyTimer");
final String[] hourMinute = time.split(":");
final String hour = hourMinute[0];
final String minute = hourMinute[1];
String second = "0";
final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hour));
calendar.set(Calendar.MINUTE, Integer.parseInt(minute));
calendar.set(Calendar.SECOND, Integer.parseInt(second));
if(calendar.getTime().before(new Date())){
    calendar.add(Calendar.DATE, 1);
}

timer.schedule(job, calendar.getTime(), 
TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS)); // period: 1 day

My code from below works well, the job is executed daily at the right time with exception of daylight savings hour changes. It happens at the end of october and march. My server has a lightweight hardware and it is located in zone where it is used.
How can I make it work, using minimal resources (without quartz or any similar)

// init daily scheduler 
final java.util.Timer timer = new java.util.Timer("MyTimer");
final String[] hourMinute = time.split(":");
final String hour = hourMinute[0];
final String minute = hourMinute[1];
String second = "0";
final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hour));
calendar.set(Calendar.MINUTE, Integer.parseInt(minute));
calendar.set(Calendar.SECOND, Integer.parseInt(second));
if(calendar.getTime().before(new Date())){
    calendar.add(Calendar.DATE, 1);
}

timer.schedule(job, calendar.getTime(), 
TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS)); // period: 1 day

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

神爱温柔 2025-01-19 23:18:32

tl;dr

仅使用 java.time 类,切勿使用 Date/Calendar

白天的长度各不相同,并不总是 24 小时。因此,在一天中的某个时间跑步意味着确定每天的特定时刻。

public void scheduleNextRun ( LocalDate today )
{
    LocalDate dateOfNextRun = today;
    if ( LocalTime.now( this.zoneId ).isAfter( this.timeToExecute ) )
    {
        dateOfNextRun = dateOfNextRun.plusDays( 1 );
    }
    ZonedDateTime zdt = ZonedDateTime.of( dateOfNextRun , this.timeToExecute , this.zoneId );  // Determine a moment, a point on the timeline.
    Instant instantOfNextTaskExecution = zdt.toInstant(); // Adjust from time zone to offset of zero hours-minutes-seconds from UTC.
    Duration d = Duration.between( Instant.now() , instantOfNextTaskExecution );
    this.ses.schedule( this , d.toNanos() , TimeUnit.NANOSECONDS );
    System.out.println( "DEBUG - Will run this task after duration of " + d + " at " + zdt );
}

过时的类

您正在使用糟糕的日期时间类,这些类在几年前就被 JSR 310 中定义的现代 java.time 类所取代

。 oracle.com/en/java/javase/17/docs/api/java.base/java/util/Timer.html" rel="noreferrer">Timer 被取代的类几年前由执行器框架,如 Javadoc 中所述。

现代 Java

计划执行程序服务

在应用程序的某个位置,初始化计划执行程序服务。

ScheduledExecutorService see = Executors.newSingleThreadScheduledExecutor() ;

保留预定的执行者服务。请务必在应用程序退出之前将其关闭,否则其后备线程池可能会像僵尸一样继续运行

tl;dr

Use only java.time classes, never Date/Calendar.

Days vary in length, not always 24 hours long. So, running at a certain time-of-day means determining a particular moment for each and every day.

public void scheduleNextRun ( LocalDate today )
{
    LocalDate dateOfNextRun = today;
    if ( LocalTime.now( this.zoneId ).isAfter( this.timeToExecute ) )
    {
        dateOfNextRun = dateOfNextRun.plusDays( 1 );
    }
    ZonedDateTime zdt = ZonedDateTime.of( dateOfNextRun , this.timeToExecute , this.zoneId );  // Determine a moment, a point on the timeline.
    Instant instantOfNextTaskExecution = zdt.toInstant(); // Adjust from time zone to offset of zero hours-minutes-seconds from UTC.
    Duration d = Duration.between( Instant.now() , instantOfNextTaskExecution );
    this.ses.schedule( this , d.toNanos() , TimeUnit.NANOSECONDS );
    System.out.println( "DEBUG - Will run this task after duration of " + d + " at " + zdt );
}

Outmoded classes

You are using terrible date-time classes that were years ago supplanted by the modern java.time classes defined in JSR 310.

And you are using the Timer class that was supplanted years ago by the Executors framework, as noted in the Javadoc.

Modern Java

Scheduled executor service

Somewhere in your app, initialize a scheduled executor service.

ScheduledExecutorService see = Executors.newSingleThreadScheduledExecutor() ;

Keep that scheduled executor service around. Be sure to shut it down before your app exits as otherwise its backing pool of threads may continue running like a zombie ????‍♂️.

Runnable task

Define your task as a Runnable or Callable.

Days vary in length

You want your task to run once per day at a designated time-of-day as seen in a particular time zone.

public class DailyTask implements Runnable { … } 

That does not mean every 24 hours. On some dates in some zones, days vary in length. They may be 23 hours, 25 hours, or 23.5 or some other number of hours long.

Task re-schedules itself

To determine when to run the task next, the task itself should make that calculation. The task will then re-schedule itself for its next execution. To do this, our task must keep a reference to the scheduled executor service on which it is executing. And our task must track the time of day at which you want to run. And the task must track the time zone in which we are to perceive this time-of-day. So we need to pass this info to the constructor of our task, and remember the values as private members.

…
private final ScheduledExecutorService ses;
private final LocalTime timeToExecute;
private final ZoneId zoneId;

public DailyTask ( final ScheduledExecutorService ses , final LocalTime timeToExecute , final ZoneId zoneId )
{
    this.ses = ses;
    this.timeToExecute = timeToExecute;
    this.zoneId = zoneId;
}
…

To simulate work, in this example we simply print to console. We do this in the run method promised by the Runnable interface.

@Override
public void run ( )
{
    // Perform the task’s work.
    System.out.println( Thread.currentThread().getId() + " is running at " + Instant.now() );
}

Calculating next run

To that run method we must add the calculation of the next run. First we capture the current date just in case the clock were to run over to the next date while performing our task’s workload.

LocalDate today = LocalDate.now( this.zoneId );

Next we calculate the amount of time until the next run.

LocalDate tomorrow = today.plusDays( 1  );
ZonedDateTime zdt = ZonedDateTime.of( tomorrow, this.timeToExecute , this.zoneId );  // Determine a moment, a point on the timeline.
Instant instantOfNextTaskExecution = zdt.toInstant(); // Adjust from time zone to offset of zero hours-minutes-seconds from UTC.
Duration d = Duration.between( Instant.now() , instantOfNextTaskExecution );

Scheduling task to run on executor service

Lastly we schedule that work.

this.ses.schedule( this , d.toNanos() , TimeUnit.NANOSECONDS );

To get this task running the very first time, the calling app will need to schedule this task this first time. Rather than make the calling app do the duration calculation, let's make the duration calculation code seen here available as a method.

public void scheduleNextRun( LocalDate today ) {
    LocalDate tomorrow = today.plusDays( 1 );
    ZonedDateTime zdt = ZonedDateTime.of( tomorrow , this.timeToExecute , this.zoneId );  // Determine a moment, a point on the timeline.
    Instant instantOfNextTaskExecution = zdt.toInstant(); // Adjust from time zone to offset of zero hours-minutes-seconds from UTC.
    Duration d = Duration.between( Instant.now() , instantOfNextTaskExecution );
    this.ses.schedule( this , d.toNanos() , TimeUnit.NANOSECONDS );
    System.out.println( "DEBUG - Will run this task after duration of " + d + " at " + zdt );
}

We have a subtle bug in that method. We should not assume the next run is for tomorrow. If, on the very first run, the current moment is not yet past the target time-of-day then we should not add a day to the current date. We should only call .plusDays( 1 ) if the time-of-day has passed for the current date.

public void scheduleNextRun ( LocalDate today )
{
    LocalDate dateOfNextRun = today;
    if ( LocalTime.now( this.zoneId ).isAfter( this.timeToExecute ) )
    {
        dateOfNextRun = dateOfNextRun.plusDays( 1 );
    }
    ZonedDateTime zdt = ZonedDateTime.of( dateOfNextRun , this.timeToExecute , this.zoneId );  // Determine a moment, a point on the timeline.
    Instant instantOfNextTaskExecution = zdt.toInstant(); // Adjust from time zone to offset of zero hours-minutes-seconds from UTC.
    Duration d = Duration.between( Instant.now() , instantOfNextTaskExecution );
    this.ses.schedule( this , d.toNanos() , TimeUnit.NANOSECONDS );
    System.out.println( "DEBUG - Will run this task after duration of " + d + " at " + zdt );
}

Final code

Putting all that code together looks like this.

package work.basil.timing;

import java.time.*;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class DailyTask implements Runnable
{
    private final ScheduledExecutorService ses;
    private final LocalTime timeToExecute;
    private final ZoneId zoneId;

    public DailyTask ( final ScheduledExecutorService ses , final LocalTime timeToExecute , final ZoneId zoneId )
    {
        this.ses = ses;
        this.timeToExecute = timeToExecute;
        System.out.println( "timeToExecute = " + timeToExecute );
        this.zoneId = zoneId;
    }

    @Override
    public void run ( )
    {
        // Capture the current moment as seen your desired time zone.
        // Do this *before* your task’s work, in case during that task work the clock runs over to the next day.
        // Tip: Avoid scheduling tasks for midnight, the witching hour.
        LocalDate today = LocalDate.now( this.zoneId );
        // Perform the task’s work.
        System.out.println( "Thread ID: " + Thread.currentThread().getId() + " is running at " + Instant.now() );
        // Schedule the next task.
        this.scheduleNextRun( today );
    }

    public void scheduleNextRun ( LocalDate today )
    {
        LocalDate dateOfNextRun = today;
        if ( LocalTime.now( this.zoneId ).isAfter( this.timeToExecute ) )
        {
            dateOfNextRun = dateOfNextRun.plusDays( 1 );
        }
        ZonedDateTime zdt = ZonedDateTime.of( dateOfNextRun , this.timeToExecute , this.zoneId );  // Determine a moment, a point on the timeline.
        Instant instantOfNextTaskExecution = zdt.toInstant(); // Adjust from time zone to offset of zero hours-minutes-seconds from UTC.
        Duration d = Duration.between( Instant.now() , instantOfNextTaskExecution );
        this.ses.schedule( this , d.toNanos() , TimeUnit.NANOSECONDS );
        System.out.println( "DEBUG - Will run this task after duration of " + d + " at " + zdt );
    }
}

Demo app

Write a little demo app to use it.

To make this demo work without us having to wait an entire day, let's calculate a minute from the current moment as our desired time-of-day. Then we need only wait a minute to see this code work.

Actually, we will wait two minutes, enough time to ensure our principal code has completely finished before we shut down the demo app’s scheduled executor service.

package work.basil.timing;

import java.time.*;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class App
{
    public static void main ( String[] args )
    {
        System.out.println( "INFO - Demo start. " + Instant.now() );

        ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
        ZoneId z = ZoneId.of( "Europe/Bucharest" );
        System.out.println( "Now in " + z + " is " + ZonedDateTime.now( z ) );
        DailyTask task = new DailyTask( ses , LocalTime.now( z ).truncatedTo( ChronoUnit.SECONDS ).plusMinutes( 1 ) , z );
        task.scheduleNextRun( LocalDate.now( z ) );

        try { Thread.sleep( Duration.ofMinutes( 2 ).toMillis() ); } catch ( InterruptedException e ) { throw new RuntimeException( e ); }
        ses.shutdownNow();
        System.out.println( "INFO - Waiting for scheduled executor service to shutdown now. " + Instant.now() );
        try { ses.awaitTermination( 5 , TimeUnit.SECONDS ); } catch ( InterruptedException e ) { throw new RuntimeException( e ); }
        System.out.println( "INFO - Demo end. " + Instant.now() );
    }
}

When run.

INFO - Demo start. 2022-03-08T22:03:14.837676Z
Now in Europe/Bucharest is 2022-03-09T00:03:14.848880+02:00[Europe/Bucharest]
timeToExecute = 00:04:14
DEBUG - Will run this task after duration of PT59.142979S at 2022-03-09T00:04:14+02:00[Europe/Bucharest]
Thread ID: 15 is running at 2022-03-08T22:04:14.007322Z
DEBUG - Will run this task after duration of PT23H59M59.979047S at 2022-03-10T00:04:14+02:00[Europe/Bucharest]
INFO - Waiting for scheduled executor service to shutdown now. 2022-03-08T22:05:14.868608Z
INFO - Demo end. 2022-03-08T22:05:14.871141Z

Not so S.O.L.I.D.

The code seen above is workable. I could imagine putting that into production. However, looking at the bigger picture, that code has a design flaw.

The SOLID principles are generally recognized as a way of designing better software. The S stands for Single-responsibility principle. That means a class should focus on doing one main thing, and doing that one thing well. So different jobs should be handled by different classes.

But in the code seen above we are mixing two different jobs.

We have the original job, the task that needs to be performed routinely. Imagine that task is some business function such as compiling a Sales report, or calculating an accounting roll-up, or syncing inventory tallies. Such functions do not really care about when the run; they care about sales figures, accounting numbers, or inventory levels.

Deciding when to run such a function is a different kind of job. This other job of capturing the current moment, applying time zones, calculating time to elapse, is entirely separate from sales, accounting, or inventory.

Even the name of our Runnable class is a clue to this violation of the Single-Responsibility Principle: DailyTask has two parts, Task referring to the business function to be accomplished, and Daily referring to the scheduling chore.

So ideally, rather than have our task reschedule itself, we should have two different classes. One handles the original job of business work, and the other handles the scheduling of execution.

Implementing this goes far beyond the original Question. So I will leave this as an exercise for the reader. ;-)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文