Comment exécuter certaines tâches tous les jours à une heure donnée en utilisant ScheduledExecutorService?

90

J'essaie d'exécuter une certaine tâche tous les jours à 5 heures du matin. J'ai donc décidé de l'utiliser ScheduledExecutorServicepour cela, mais jusqu'à présent, j'ai vu des exemples qui montrent comment exécuter une tâche toutes les quelques minutes.

Et je ne suis pas en mesure de trouver un exemple qui montre comment exécuter une tâche tous les jours à une heure particulière (5 heures du matin) du matin et en tenant également compte du fait de l'heure d'été -

Voici mon code qui fonctionnera toutes les 15 minutes -

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

Existe-t-il un moyen de planifier une tâche à exécuter tous les jours 5 heures du matin en ScheduledExecutorServicetenant également compte du fait de l'heure d'été?

Et c'est aussi TimerTaskmieux pour cela ou ScheduledExecutorService?

AKIWEB
la source
Utilisez plutôt quelque chose comme Quartz.
millimoose

Réponses:

113

Comme avec la version actuelle de java SE 8, avec son excellente API de date et d'heure, java.timece type de calcul peut être fait plus facilement au lieu d'utiliser java.util.Calendaret java.util.Date.

Maintenant, comme exemple de planification d'une tâche avec votre cas d'utilisation:

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);

Le initalDelayest calculé pour demander au planificateur de retarder l'exécution TimeUnit.SECONDS. Les problèmes de différence de temps avec les millisecondes unitaires et en dessous semblent être négligeables pour ce cas d'utilisation. Mais vous pouvez toujours utiliser duration.toMillis()et TimeUnit.MILLISECONDSgérer les calculs de planification en millisecondes.

Et aussi TimerTask est meilleur pour cela ou ScheduledExecutorService?

NON: ScheduledExecutorService apparemment mieux que TimerTask. StackOverflow a déjà une réponse pour vous .

De @PaddyD,

Vous rencontrez toujours le problème selon lequel vous devez le redémarrer deux fois par an si vous souhaitez qu'il s'exécute à la bonne heure locale. scheduleAtFixedRate ne le coupera que si vous êtes satisfait de la même heure UTC toute l'année.

Comme c'est vrai et que @PaddyD a déjà donné une solution de contournement (+1 pour lui), je fournis un exemple de travail avec l'API de date et heure Java8 avec ScheduledExecutorService. Utiliser un thread démon est dangereux

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

Remarque:

  • MyTaskest une interface avec la fonction execute.
  • Lors de l'arrêt ScheduledExecutorService, utilisez toujours awaitTerminationaprès l'invocation shutdown: il est toujours probable que votre tâche soit bloquée / bloquée et que l'utilisateur attende indéfiniment.

L'exemple précédent que j'ai donné avec Calender n'était qu'une idée que j'ai mentionnée, j'ai évité le calcul de l'heure exacte et les problèmes d'heure d'été. Mise à jour de la solution sur la plainte de @PaddyD

sauge
la source
Merci pour la suggestion, pouvez-vous s'il vous plaît m'expliquer en détail comment cela intDelayInHoursignifie que je vais exécuter ma tâche à 5 heures du matin?
AKIWEB
Quel est le but de aDate?
José Andias
Mais si vous démarrez cela à HH: mm, la tâche sera exécutée à 05: mm au lieu de 5 heures du matin? Il ne tient pas non plus compte de l'heure d'été comme OP demandé. Ok si vous le démarrez immédiatement après l'heure, ou si vous êtes satisfait d'une heure entre 5 et 6, ou si cela ne vous dérange pas de redémarrer l'application au milieu de la nuit deux fois par an après le changement d'horloge, je suppose ...
PaddyD
2
Vous rencontrez toujours le problème selon lequel vous devez le redémarrer deux fois par an si vous souhaitez qu'il s'exécute à la bonne heure locale. scheduleAtFixedRatene le coupera pas à moins que vous ne soyez satisfait de la même heure UTC toute l'année.
PaddyD
3
Pourquoi l'exemple suivant (deuxième) déclencheur s'exécute-t-il n fois ou jusqu'à ce que le deuxième passe? Le code n'était-il pas supposé déclencher une tâche une fois par jour?
krizajb le
25

Dans 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);
Victor
la source
14
Pour plus de lisibilité, je suggérerais TimeUnit.DAYS.toMinutes(1)au lieu du "nombre magique" 1440.
philonous
Merci, Victor. De cette façon, besoin de redémarrer deux fois par an si je veux qu'il fonctionne à la bonne heure locale?
invzbl3
le taux fixe ne doit pas changer lorsque l'heure locale change, après sa création, il devient sur le taux.
Victor
Comment se fait-il que cela déclenche ma tâche à 23:59:59?
krizajb
Pour la lisibilité, je suggérerais 1 au lieu de 1440 ou TimeUnit.DAYS.toMinutes (1), puis utiliser l'unité de temps TimeUnit.DAYS. ;-)
Kelly Denehy
7

Si vous n'avez pas le luxe de pouvoir utiliser Java 8, ce qui suit fera ce dont vous avez besoin:

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

Il ne nécessite aucune bibliothèque externe et tiendra compte de l'heure d'été. Indiquez simplement l'heure à laquelle vous souhaitez exécuter la tâche en tant Calendarqu'objet et la tâche en tant qu'objet Runnable. Par exemple:

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 la tâche du minuteur s'exécute dans un thread Daemon qui n'est pas recommandé pour effectuer des E / S. Si vous devez utiliser un fil utilisateur, vous devrez ajouter une autre méthode qui annule le minuteur.

Si vous devez utiliser a ScheduledExecutorService, modifiez simplement la startTimerméthode comme suit:

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

Je ne suis pas sûr du comportement, mais vous aurez peut-être besoin d'une méthode d'arrêt qui appelle shutdownNowsi vous empruntez la ScheduledExecutorServiceroute, sinon votre application peut se bloquer lorsque vous essayez de l'arrêter.

PaddyD
la source
Je vois ce que tu veux dire. +1 et merci. Cependant, il est préférable de ne pas utiliser le thread Daemon (c'est-à-dire new Timer(runThreadName, true)).
Sage
@Sage pas de soucis. Un thread démon convient si vous n'effectuez aucune E / S. Le cas d'utilisation pour lequel j'ai écrit ceci était juste une simple classe de feu et d'oublier pour lancer certains threads pour effectuer des tâches de ménage quotidiennes. Je suppose que si vous effectuez des lectures de base de données dans le fil de la tâche du minuteur, comme la demande d'OP pourrait l'indiquer, vous ne devriez pas utiliser de démon et vous aurez besoin d'une sorte de méthode d'arrêt que vous devrez appeler pour permettre à votre application de se terminer. stackoverflow.com/questions/7067578/…
PaddyD
@PaddyD La dernière partie, à savoir celle utilisant ScheduledExecutorSerive, était-elle correcte ??? La façon dont la classe anonyme est créée ne semble pas correcte en termes de syntaxe. De plus, newSingleThreadExecutor () n'aurait pas la bonne méthode de planification?
FReeze FRancis
6

Avez-vous envisagé d'utiliser quelque chose comme Quartz Scheduler ? Cette bibliothèque dispose d'un mécanisme pour planifier des tâches à exécuter à une période de temps définie chaque jour en utilisant une expression de type cron (jetez un œil à CronScheduleBuilder).

Quelques exemples de code (non testés):

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

Il y a un peu plus de travail au départ et vous devrez peut-être réécrire le code d'exécution de votre tâche, mais cela devrait vous donner plus de contrôle sur la façon dont vous voulez que votre tâche s'exécute. En outre, il serait plus facile de modifier l'horaire si vous en avez besoin.

lmika
la source
5

Java8:
Ma version améliorée de la première réponse:

  1. Correction de la situation où le serveur d'applications Web ne voulait pas s'arrêter, en raison d'un pool de threads avec un thread inactif
  2. Sans récursivité
  3. Exécutez la tâche avec votre heure locale personnalisée, dans mon cas, c'est la Biélorussie, Minsk


/**
 * 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");
        }
    }

}
Alexey Alexeenka
la source
1
J'ai une exigence similaire. Je dois planifier une tâche pour une journée à une heure donnée. Une fois la tâche planifiée terminée, planifiez la tâche pour le jour suivant à une heure donnée. Ma question est de savoir comment savoir si la tâche planifiée est terminée ou non. Une fois que je sais que la tâche planifiée est terminée, je ne peux que planifier le lendemain.
amitwdh le
2

J'avais un problème similaire. J'ai dû planifier un tas de tâches qui devraient être exécutées pendant une journée en utilisantScheduledExecutorService . Cela a été résolu par une tâche commençant à 3h30 du matin, planifiant toutes les autres tâches par rapport à son heure actuelle . Et se reprogrammer pour le lendemain à 3h30.

Avec ce scénario, l'heure d'été n'est plus un problème.

user987339
la source
2

Vous pouvez utiliser une analyse de date simple, si l'heure de la journée est avant maintenant, commençons demain:

  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);
Luc Chevallier
la source
1

Juste pour ajouter la réponse de Victor .

Je recommanderais d'ajouter une vérification pour voir si la variable (dans son cas, la longue midnight) est supérieure à 1440. Si c'est le cas, j'omettrais le .plusDays(1), sinon la tâche ne s'exécutera qu'après-demain.

Je l'ai fait simplement comme ceci:

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;
}
Niby
la source
Cela simplifie si vous utiliseztruncatedTo()
Mark Jeronimus
1

L'exemple suivant fonctionne pour moi

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

référence: https://chynten.wordpress.com/2016/06/03/java-scheduler-to-run-every-day-on-specific-time/

Shaik Elias
la source
1

Vous pouvez utiliser la classe ci-dessous pour planifier votre tâche chaque jour à une heure particulière

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());
    }
}
Amol Suryawanshi
la source
1
Veuillez ne pas utiliser la version obsolète Dateet TimeUniten 2019
Mark Jeronimus
0

Que faire si votre serveur tombe en panne à 4 h 59 et revient à 5 h 01? Je pense qu'il va simplement sauter la course. Je recommanderais un planificateur persistant comme Quartz, qui stockerait ses données de planification quelque part. Ensuite, il verra que cette exécution n'a pas encore été effectuée et le fera à 5h01.

Sam
la source