Как запускать определенную задачу каждый день в определенное время с помощью ScheduledExecutorService?

90

Я пытаюсь выполнять определенную задачу каждый день в 5 утра. Поэтому я решил использовать ScheduledExecutorServiceдля этого, но до сих пор видел примеры, которые показывают, как запускать задачу каждые несколько минут.

И я не могу найти ни одного примера, который показывает, как запускать задачу каждый день в определенное время (5 часов утра) утром, а также учитывая факт перехода на летнее время -

Ниже мой код, который будет запускаться каждые 15 минут -

public class ScheduledTaskExample {
    private final ScheduledExecutorService scheduler = Executors
        .newScheduledThreadPool(1);

    public void startScheduleTask() {
    /**
    * not using the taskHandle returned here, but it can be used to cancel
    * the task, or check if it's done (for recurring tasks, that's not
    * going to be very useful)
    */
    final ScheduledFuture<?> taskHandle = scheduler.scheduleAtFixedRate(
        new Runnable() {
            public void run() {
                try {
                    getDataFromDatabase();
                }catch(Exception ex) {
                    ex.printStackTrace(); //or loggger would be better
                }
            }
        }, 0, 15, TimeUnit.MINUTES);
    }

    private void getDataFromDatabase() {
        System.out.println("getting data...");
    }

    public static void main(String[] args) {
        ScheduledTaskExample ste = new ScheduledTaskExample();
        ste.startScheduleTask();
    }
}

Есть ли способ, чтобы я мог запланировать выполнение задачи каждый день в 5 часов утра, ScheduledExecutorServiceучитывая также факт перехода на летнее время?

А также TimerTaskлучше для этого или ScheduledExecutorService?

AKIWEB
источник
Вместо этого используйте что-нибудь вроде Quartz.
millimoose

Ответы:

113

Как и в нынешнем выпуске Java SE 8 с отличным API даты и времени с java.timeтаким расчетом, можно сделать более простой расчет вместо использования java.util.Calendarи java.util.Date.

Теперь в качестве примера для планирования задачи с вашим вариантом использования:

ZonedDateTime now = ZonedDateTime.now(ZoneId.of("America/Los_Angeles"));
ZonedDateTime nextRun = now.withHour(5).withMinute(0).withSecond(0);
if(now.compareTo(nextRun) > 0)
    nextRun = nextRun.plusDays(1);

Duration duration = Duration.between(now, nextRun);
long initalDelay = duration.getSeconds();

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);            
scheduler.scheduleAtFixedRate(new MyRunnableTask(),
    initalDelay,
    TimeUnit.DAYS.toSeconds(1),
    TimeUnit.SECONDS);

initalDelayВычисляется задать планировщик , чтобы задержать выполнение в TimeUnit.SECONDS. Проблемы с разницей во времени с единицей миллисекунд и ниже кажутся незначительными для этого варианта использования. Но вы все равно можете использовать duration.toMillis()и TimeUnit.MILLISECONDSдля обработки вычислений расписания в миллисекундах.

А еще лучше для этого TimerTask или ScheduledExecutorService?

НЕТ: ScheduledExecutorService вроде бы лучше, чем TimerTask. У StackOverflow уже есть ответ для вас .

От @PaddyD,

У вас все еще есть проблема, из-за которой вам нужно перезапускать это дважды в год, если вы хотите, чтобы он запускался в нужное местное время. scheduleAtFixedRate не сократит его, если вы не будете довольны одним и тем же временем UTC в течение всего года.

Поскольку это правда, и @PaddyD уже дал обходной путь (+1 ему), я предоставляю рабочий пример с API даты и времени Java8 с ScheduledExecutorService. Использование потока демона опасно

class MyTaskExecutor
{
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
    MyTask myTask;
    volatile boolean isStopIssued;

    public MyTaskExecutor(MyTask myTask$) 
    {
        myTask = myTask$;

    }

    public void startExecutionAt(int targetHour, int targetMin, int targetSec)
    {
        Runnable taskWrapper = new Runnable(){

            @Override
            public void run() 
            {
                myTask.execute();
                startExecutionAt(targetHour, targetMin, targetSec);
            }

        };
        long delay = computeNextDelay(targetHour, targetMin, targetSec);
        executorService.schedule(taskWrapper, delay, TimeUnit.SECONDS);
    }

    private long computeNextDelay(int targetHour, int targetMin, int targetSec) 
    {
        LocalDateTime localNow = LocalDateTime.now();
        ZoneId currentZone = ZoneId.systemDefault();
        ZonedDateTime zonedNow = ZonedDateTime.of(localNow, currentZone);
        ZonedDateTime zonedNextTarget = zonedNow.withHour(targetHour).withMinute(targetMin).withSecond(targetSec);
        if(zonedNow.compareTo(zonedNextTarget) > 0)
            zonedNextTarget = zonedNextTarget.plusDays(1);

        Duration duration = Duration.between(zonedNow, zonedNextTarget);
        return duration.getSeconds();
    }

    public void stop()
    {
        executorService.shutdown();
        try {
            executorService.awaitTermination(1, TimeUnit.DAYS);
        } catch (InterruptedException ex) {
            Logger.getLogger(MyTaskExecutor.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

Заметка:

  • MyTaskэто интерфейс с функцией execute.
  • При остановке ScheduledExecutorServiceвсегда использовать awaitTerminationпосле вызова shutdown: всегда есть вероятность, что ваша задача зависнет / заблокируется, и пользователь будет ждать вечно.

Предыдущий пример, который я привел с календарем, был просто идеей, о которой я упоминал, я избегал точного расчета времени и проблем с переходом на летнее время. Обновил решение по жалобе @PaddyD

Шалфей
источник
Спасибо за предложение, не могли бы вы подробно объяснить мне, как это intDelayInHourозначает, что я буду выполнять свою задачу в 5 утра?
AKIWEB 05
Какова цель aDate?
José Andias
Но если вы начнете это с ЧЧ: мм, задача будет запущена в 05: мм, а не в 5 утра? Он также не учитывает летнее время, как запрошено OP. Хорошо, если вы запустите его сразу после часа, или если вас устраивает любое время между 5 и 6, или если вы не возражаете перезапускать приложение посреди ночи два раза в год после изменения часов, я полагаю ...
PaddyD 06
2
У вас все еще есть проблема, из-за которой вам нужно перезапускать это дважды в год, если вы хотите, чтобы он запускался в нужное местное время. scheduleAtFixedRateне будет сокращать его, если вы не будете довольны одним и тем же временем UTC в течение всего года.
PaddyD
3
Почему следующий пример (второй) триггер выполняется n раз или пока не пройдет второй? Разве код не предполагал запускать задачу один раз в день?
krizajb
25

В Java 8:

scheduler = Executors.newScheduledThreadPool(1);

//Change here for the hour you want ----------------------------------.at()       
Long midnight=LocalDateTime.now().until(LocalDate.now().plusDays(1).atStartOfDay(), ChronoUnit.MINUTES);
scheduler.scheduleAtFixedRate(this, midnight, 1440, TimeUnit.MINUTES);
Виктор
источник
14
Для удобочитаемости я бы предложил TimeUnit.DAYS.toMinutes(1)вместо «магического числа» 1440.
filonous
Спасибо, Виктор. Таким образом, нужно перезапускать дважды в год, если я хочу, чтобы он работал в нужное местное время?
invzbl3
фиксированная ставка не должна изменяться при изменении местного времени, после создания она становится примерно такой же.
Виктор
Почему это запускает мою задачу в 23:59:59?
krizajb
Для удобства чтения я бы предложил 1 вместо 1440 или TimeUnit.DAYS.toMinutes (1), а затем использовать единицу времени TimeUnit.DAYS. ;-)
Келли Денехи
7

Если у вас нет возможности использовать Java 8, вам подойдет следующее:

public class DailyRunnerDaemon
{
   private final Runnable dailyTask;
   private final int hour;
   private final int minute;
   private final int second;
   private final String runThreadName;

   public DailyRunnerDaemon(Calendar timeOfDay, Runnable dailyTask, String runThreadName)
   {
      this.dailyTask = dailyTask;
      this.hour = timeOfDay.get(Calendar.HOUR_OF_DAY);
      this.minute = timeOfDay.get(Calendar.MINUTE);
      this.second = timeOfDay.get(Calendar.SECOND);
      this.runThreadName = runThreadName;
   }

   public void start()
   {
      startTimer();
   }

   private void startTimer();
   {
      new Timer(runThreadName, true).schedule(new TimerTask()
      {
         @Override
         public void run()
         {
            dailyTask.run();
            startTimer();
         }
      }, getNextRunTime());
   }


   private Date getNextRunTime()
   {
      Calendar startTime = Calendar.getInstance();
      Calendar now = Calendar.getInstance();
      startTime.set(Calendar.HOUR_OF_DAY, hour);
      startTime.set(Calendar.MINUTE, minute);
      startTime.set(Calendar.SECOND, second);
      startTime.set(Calendar.MILLISECOND, 0);

      if(startTime.before(now) || startTime.equals(now))
      {
         startTime.add(Calendar.DATE, 1);
      }

      return startTime.getTime();
   }
}

Он не требует внешних библиотек и учитывает летнее время. Просто укажите время дня, когда вы хотите запустить задачу как Calendarобъект, а задачу как файл Runnable. Например:

Calendar timeOfDay = Calendar.getInstance();
timeOfDay.set(Calendar.HOUR_OF_DAY, 5);
timeOfDay.set(Calendar.MINUTE, 0);
timeOfDay.set(Calendar.SECOND, 0);

new DailyRunnerDaemon(timeOfDay, new Runnable()
{
   @Override
   public void run()
   {
      try
      {
        // call whatever your daily task is here
        doHousekeeping();
      }
      catch(Exception e)
      {
        logger.error("An error occurred performing daily housekeeping", e);
      }
   }
}, "daily-housekeeping");

NB, задача таймера выполняется в потоке демона, который не рекомендуется для выполнения операций ввода-вывода. Если вам нужно использовать поток пользователя, вам нужно будет добавить еще один метод, который отменяет таймер.

Если вам нужно использовать ScheduledExecutorService, просто измените startTimerметод на следующий:

private void startTimer()
{
   Executors.newSingleThreadExecutor().schedule(new Runnable()
   {
      Thread.currentThread().setName(runThreadName);
      dailyTask.run();
      startTimer();
   }, getNextRunTime().getTime() - System.currentTimeMillis(),
   TimeUnit.MILLISECONDS);
}

Я не уверен в поведении, но вам может понадобиться метод остановки, который вызывается, shutdownNowесли вы идете по ScheduledExecutorServiceмаршруту, иначе ваше приложение может зависнуть, когда вы попытаетесь его остановить.

PaddyD
источник
Японял твою точку зрения. +1 и спасибо. Однако будет лучше, если мы не будем использовать поток демона (т. Е. new Timer(runThreadName, true)).
Sage
@ Шалфей, не беспокойся. Демонический поток прекрасен, если вы не выполняете операций ввода-вывода. Вариант использования, для которого я написал это, был простым классом «запустил и забыл» для запуска некоторых потоков для выполнения некоторых повседневных служебных задач. Я полагаю, если вы выполняете чтение базы данных в потоке задачи таймера, как может указывать запрос OP, тогда вам не следует использовать Daemon и вам понадобится какой-то метод остановки, который вам придется вызвать, чтобы ваше приложение могло завершиться. stackoverflow.com/questions/7067578/…
PaddyD
@PaddyD Была ли последняя часть, то есть та, которая использовала ScheduledExecutorSerive, верна ??? То, как создается анонимный класс, не выглядит правильно с точки зрения синтаксиса. Также newSingleThreadExecutor () не имеет метода расписания правильно ??
FReeze FRancis
6

Думали ли вы об использовании чего-то вроде Quartz Scheduler ? В этой библиотеке есть механизм для планирования задач, которые будут запускаться в установленный период времени каждый день, с использованием выражения, подобного cron (посмотрите CronScheduleBuilder).

Пример кода (не тестировался):

public class GetDatabaseJob implements InterruptableJob
{
    public void execute(JobExecutionContext arg0) throws JobExecutionException
    {
        getFromDatabase();
    }
}

public class Example
{
    public static void main(String[] args)
    {
        JobDetails job = JobBuilder.newJob(GetDatabaseJob.class);

        // Schedule to run at 5 AM every day
        ScheduleBuilder scheduleBuilder = 
                CronScheduleBuilder.cronSchedule("0 0 5 * * ?");
        Trigger trigger = TriggerBuilder.newTrigger().
                withSchedule(scheduleBuilder).build();

        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        scheduler.scheduleJob(job, trigger);

        scheduler.start();
    }
}

Впереди немного больше работы, и вам может потребоваться переписать код выполнения задания, но это должно дать вам больше контроля над тем, как вы хотите, чтобы задание выполнялось. Кроме того, при необходимости будет проще изменить расписание.

Имика
источник
5

Java8:
Моя версия обновления сверху ответ:

  1. Исправлена ​​ситуация, когда сервер веб-приложений не хочет останавливаться из-за пула потоков с незанятым потоком.
  2. Без рекурсии
  3. Запустите задачу с вашим местным временем, в моем случае это Беларусь, Минск


/**
 * Execute {@link AppWork} once per day.
 * <p>
 * Created by aalexeenka on 29.12.2016.
 */
public class OncePerDayAppWorkExecutor {

    private static final Logger LOG = AppLoggerFactory.getScheduleLog(OncePerDayAppWorkExecutor.class);

    private ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

    private final String name;
    private final AppWork appWork;

    private final int targetHour;
    private final int targetMin;
    private final int targetSec;

    private volatile boolean isBusy = false;
    private volatile ScheduledFuture<?> scheduledTask = null;

    private AtomicInteger completedTasks = new AtomicInteger(0);

    public OncePerDayAppWorkExecutor(
            String name,
            AppWork appWork,
            int targetHour,
            int targetMin,
            int targetSec
    ) {
        this.name = "Executor [" + name + "]";
        this.appWork = appWork;

        this.targetHour = targetHour;
        this.targetMin = targetMin;
        this.targetSec = targetSec;
    }

    public void start() {
        scheduleNextTask(doTaskWork());
    }

    private Runnable doTaskWork() {
        return () -> {
            LOG.info(name + " [" + completedTasks.get() + "] start: " + minskDateTime());
            try {
                isBusy = true;
                appWork.doWork();
                LOG.info(name + " finish work in " + minskDateTime());
            } catch (Exception ex) {
                LOG.error(name + " throw exception in " + minskDateTime(), ex);
            } finally {
                isBusy = false;
            }
            scheduleNextTask(doTaskWork());
            LOG.info(name + " [" + completedTasks.get() + "] finish: " + minskDateTime());
            LOG.info(name + " completed tasks: " + completedTasks.incrementAndGet());
        };
    }

    private void scheduleNextTask(Runnable task) {
        LOG.info(name + " make schedule in " + minskDateTime());
        long delay = computeNextDelay(targetHour, targetMin, targetSec);
        LOG.info(name + " has delay in " + delay);
        scheduledTask = executorService.schedule(task, delay, TimeUnit.SECONDS);
    }

    private static long computeNextDelay(int targetHour, int targetMin, int targetSec) {
        ZonedDateTime zonedNow = minskDateTime();
        ZonedDateTime zonedNextTarget = zonedNow.withHour(targetHour).withMinute(targetMin).withSecond(targetSec).withNano(0);

        if (zonedNow.compareTo(zonedNextTarget) > 0) {
            zonedNextTarget = zonedNextTarget.plusDays(1);
        }

        Duration duration = Duration.between(zonedNow, zonedNextTarget);
        return duration.getSeconds();
    }

    public static ZonedDateTime minskDateTime() {
        return ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
    }

    public void stop() {
        LOG.info(name + " is stopping.");
        if (scheduledTask != null) {
            scheduledTask.cancel(false);
        }
        executorService.shutdown();
        LOG.info(name + " stopped.");
        try {
            LOG.info(name + " awaitTermination, start: isBusy [ " + isBusy + "]");
            // wait one minute to termination if busy
            if (isBusy) {
                executorService.awaitTermination(1, TimeUnit.MINUTES);
            }
        } catch (InterruptedException ex) {
            LOG.error(name + " awaitTermination exception", ex);
        } finally {
            LOG.info(name + " awaitTermination, finish");
        }
    }

}
Алексей Алексеенко
источник
1
У меня аналогичное требование. Я должен запланировать задачу на день в заданное время. После завершения запланированной задачи запланируйте задачу на следующий день в указанное время. Это должно продолжаться и дальше. Мой вопрос в том, как узнать, выполнено ли запланированное задание или нет. Как только я знаю, что запланированное задание выполнено, только я могу запланировать его на следующий день.
amitwdh
2

У меня была аналогичная проблема. Мне пришлось запланировать кучу задач, которые нужно было выполнить в течение дня ScheduledExecutorService. Это было решено одной задачей, начинающейся в 3:30 утра, и планированием всех других задач относительно его текущего времени . И перенёс себя на следующий день в 3:30 утра.

В этом сценарии переход на летнее время больше не является проблемой.

user987339
источник
2

Вы можете использовать простой синтаксический анализ даты, если время дня раньше, давайте начнем завтра:

  String timeToStart = "12:17:30";
  SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss");
  SimpleDateFormat formatOnlyDay = new SimpleDateFormat("yyyy-MM-dd");
  Date now = new Date();
  Date dateToStart = format.parse(formatOnlyDay.format(now) + " at " + timeToStart);
  long diff = dateToStart.getTime() - now.getTime();
  if (diff < 0) {
    // tomorrow
    Date tomorrow = new Date();
    Calendar c = Calendar.getInstance();
    c.setTime(tomorrow);
    c.add(Calendar.DATE, 1);
    tomorrow = c.getTime();
    dateToStart = format.parse(formatOnlyDay.format(tomorrow) + " at " + timeToStart);
    diff = dateToStart.getTime() - now.getTime();
  }

  ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);            
  scheduler.scheduleAtFixedRate(new MyRunnableTask(), TimeUnit.MILLISECONDS.toSeconds(diff) ,
                                  24*60*60, TimeUnit.SECONDS);
Люк Шевалье
источник
1

Просто чтобы добавить ответ Виктора .

Я бы порекомендовал добавить проверку, чтобы увидеть, midnightпревышает ли переменная (в его случае long ) 1440. Если это так, я бы пропустил.plusDays(1) , иначе задача будет запущена только послезавтра.

Я сделал это просто так:

Long time;

final Long tempTime = LocalDateTime.now().until(LocalDate.now().plusDays(1).atTime(7, 0), ChronoUnit.MINUTES);
if (tempTime > 1440) {
    time = LocalDateTime.now().until(LocalDate.now().atTime(7, 0), ChronoUnit.MINUTES);
} else {
    time = tempTime;
}
Ниби
источник
Это упростит, если вы воспользуетесьtruncatedTo()
Марк Иеронимус
1

Следующий пример работает для меня

public class DemoScheduler {

    public static void main(String[] args) {

        // Create a calendar instance
        Calendar calendar = Calendar.getInstance();

        // Set time of execution. Here, we have to run every day 4:20 PM; so,
        // setting all parameters.
        calendar.set(Calendar.HOUR, 8);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.AM_PM, Calendar.AM);

        Long currentTime = new Date().getTime();

        // Check if current time is greater than our calendar's time. If So,
        // then change date to one day plus. As the time already pass for
        // execution.
        if (calendar.getTime().getTime() < currentTime) {
            calendar.add(Calendar.DATE, 1);
        }

        // Calendar is scheduled for future; so, it's time is higher than
        // current time.
        long startScheduler = calendar.getTime().getTime() - currentTime;

        // Setting stop scheduler at 4:21 PM. Over here, we are using current
        // calendar's object; so, date and AM_PM is not needed to set
        calendar.set(Calendar.HOUR, 5);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.AM_PM, Calendar.PM);

        // Calculation stop scheduler
        long stopScheduler = calendar.getTime().getTime() - currentTime;

        // Executor is Runnable. The code which you want to run periodically.
        Runnable task = new Runnable() {

            @Override
            public void run() {

                System.out.println("test");
            }
        };

        // Get an instance of scheduler
        final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        // execute scheduler at fixed time.
        scheduler.scheduleAtFixedRate(task, startScheduler, stopScheduler, MILLISECONDS);
    }
}

Справка: https://chynten.wordpress.com/2016/06/03/java-scheduler-to-run-every-day-on-specific-time/

Шайк Элиас
источник
1

Вы можете использовать класс ниже, чтобы запланировать свою задачу каждый день в определенное время

package interfaces;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class CronDemo implements Runnable{

    public static void main(String[] args) {

        Long delayTime;

        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

        final Long initialDelay = LocalDateTime.now().until(LocalDate.now().plusDays(1).atTime(12, 30), ChronoUnit.MINUTES);

        if (initialDelay > TimeUnit.DAYS.toMinutes(1)) {
            delayTime = LocalDateTime.now().until(LocalDate.now().atTime(12, 30), ChronoUnit.MINUTES);
        } else {
            delayTime = initialDelay;
        }

        scheduler.scheduleAtFixedRate(new CronDemo(), delayTime, TimeUnit.DAYS.toMinutes(1), TimeUnit.MINUTES);

    }

    @Override
    public void run() {
        System.out.println("I am your job executin at:" + new Date());
    }
}
Амол Сурьяванши
источник
1
Пожалуйста, не используйте устаревшие Dateи TimeUnitв 2019 году
Марк Иеронимус
0

Что, если ваш сервер выйдет из строя в 4:59 и вернется в 5:01? Думаю, он просто пропустит пробежку. Я бы порекомендовал постоянный планировщик, такой как Quartz, который где-то хранит свои данные расписания. Затем он увидит, что этот прогон еще не был выполнен, и выполнит его в 5:01.

Сэм
источник