Як запускати певне завдання щодня в конкретний час за допомогою 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?


Замість цього використовуйте щось на зразок кварцу.
milimoose

Відповіді:


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: Завжди існує ймовірність, що ваше завдання застрягло / зайшло в глухий кут, і користувач чекав би вічно.

Попередній приклад, який я подав з Calender, був лише ідеєю, яку я вже згадував, я уникав точного розрахунку часу та питань переходу на літній час. Оновлено рішення за скаргою @PaddyD


Дякую за пропозицію, будь ласка, поясніть мені детально, як це intDelayInHourозначає, що я буду виконувати своє завдання о 5 ранку?
AKIWEB

Яка мета aDate?
José Andias

Але якщо ви почнете це з HH: mm, завдання буде виконано о 05: mm на відміну від 5am? Він також не враховує перехід на літній час відповідно до запиту OP. Гаразд, якщо ви запускаєте його одразу після години, або вас влаштовує час від 5 до 6, або якщо ви не проти перезапустити програму серед ночі двічі на рік після зміни годинників, я гадаю ...
PaddyD

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.
філонум

Дякую, Вікторе. Таким чином потрібно перезапускати двічі на рік, якщо я хочу, щоб він працював у потрібний місцевий час?
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");

Примітка: завдання таймера виконується в потоці Daemon, що не рекомендується виконувати будь-яким введенням-виведенням. Якщо вам потрібно використовувати потік користувача, вам потрібно буде додати інший метод, який скасовує таймер.

Якщо вам доведеться використовувати a 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маршруту, інакше ваша програма може зависнути, коли ви намагаєтесь зупинити її.


Я зрозумів вашу думку. +1 та дякую Однак краще, якщо ми не використовуємо Daemon thread (тобто, new Timer(runThreadName, true)).
Sage

@Sage не турбуйся. Демон-потік добре, якщо ви не робите жодного вводу-виводу. Випадок використання, для якого я писав це, був просто простим класом «забудь і забудь» для запуску деяких ниток для виконання щоденних домашніх завдань. Я припускаю, що якщо ви виконуєте читання бази даних у потоці завдань таймера, як може вказувати запит OP, тоді ви не повинні використовувати Daemon і вам знадобиться якийсь метод зупинки, який вам доведеться викликати, щоб дозволити застосунку завершити роботу. stackoverflow.com/questions/7067578 / ...
PaddyD

@PaddyD Чи була правильною остання частина, тобто та, що використовувала ScheduledExecutorSerive ??? Спосіб створення анонімного класу не виглядає правильним синтаксисом. Також newSingleThreadExecutor () не мав би правильного методу розкладу ??
FReeze FRancis

6

Ви роздумували про використання чогось на зразок кварцового планувальника ? Ця бібліотека має механізм планування завдань, що запускаються у встановлений проміжок часу щодня, використовуючи вираз, подібний до 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 ранку.

З цим сценарієм перехід на літній час вже не є проблемою.


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) перевищує 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 ранку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.