¿Cómo ejecutar una determinada tarea todos los días a una hora determinada usando ScheduledExecutorService?

90

Estoy tratando de ejecutar una determinada tarea todos los días a las 5 de la mañana. Así que decidí usarlo ScheduledExecutorServicepara esto, pero hasta ahora he visto ejemplos que muestran cómo ejecutar la tarea cada pocos minutos.

Y no puedo encontrar ningún ejemplo que muestre cómo ejecutar una tarea todos los días a una hora en particular (5 a.m.) de la mañana y también considerando el hecho del horario de verano:

A continuación se muestra mi código que se ejecutará cada 15 minutos:

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

¿Hay alguna manera de que pueda programar una tarea para que se ejecute todos los días a las 5 a.m. de la mañana usando ScheduledExecutorServiceteniendo en cuenta el hecho del horario de verano también?

¿Y también TimerTaskes mejor para esto o ScheduledExecutorService?

AKIWEB
fuente
En su lugar, use algo como Quartz.
Millimoose

Respuestas:

113

Al igual que con la versión actual de Java SE 8 con su excelente API de fecha y hora, java.timeeste tipo de cálculo se puede hacer más fácilmente en lugar de usar java.util.Calendary java.util.Date.

Ahora, como ejemplo de muestra para programar una tarea con su caso de uso:

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

Se initalDelaycalcula para pedirle al planificador que retrase la ejecución en TimeUnit.SECONDS. Los problemas de diferencia de tiempo con milisegundos de unidad e inferiores parecen ser insignificantes para este caso de uso. Pero aún puede utilizar duration.toMillis()y TimeUnit.MILLISECONDSmanejar los cálculos de programación en milisegundos.

¿Y también TimerTask es mejor para esto o ScheduledExecutorService?

NO: ScheduledExecutorService aparentemente mejor que TimerTask. StackOverflow ya tiene una respuesta para ti .

De @PaddyD,

Aún tiene el problema por el cual debe reiniciar esto dos veces al año si desea que se ejecute a la hora local correcta. scheduleAtFixedRate no lo reducirá a menos que esté satisfecho con la misma hora UTC durante todo el año.

Como es cierto y @PaddyD ya ha dado una solución (+1 para él), estoy proporcionando un ejemplo de trabajo con la API de fecha y hora de Java8 con ScheduledExecutorService. Usar hilo de demonio es peligroso

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

Nota:

  • MyTaskes una interfaz con función execute.
  • Mientras se detiene ScheduledExecutorService, usar siempre awaitTerminationdespués de invocarlo shutdown: siempre existe la posibilidad de que su tarea se bloquee o se bloquee y el usuario esperaría para siempre.

El ejemplo anterior que di con Calender fue solo una idea que mencioné, evité el cálculo de la hora exacta y los problemas de horario de verano. Se actualizó la solución según la queja de @PaddyD

Sabio
fuente
Gracias por la sugerencia, ¿podría explicarme en detalle cómo intDelayInHoursignifica que ejecutaré mi tarea a las 5 a.m. de la mañana?
AKIWEB
¿Cuál es el propósito de aDate?
José Andias
Pero si comienza esto en HH: mm, ¿la tarea se ejecutará a las 05: mm en lugar de a las 5 am? Tampoco tiene en cuenta el horario de verano como lo solicitó OP. Está bien, si lo inicia inmediatamente después de la hora, o si está satisfecho con cualquier tiempo entre las 5 y las 6, o si no le importa reiniciar la aplicación en medio de la noche dos veces al año después de que los relojes hayan cambiado, supongo. ...
PaddyD
2
Aún tiene el problema por el cual debe reiniciar esto dos veces al año si desea que se ejecute a la hora local correcta. scheduleAtFixedRateno lo cortará a menos que esté satisfecho con la misma hora UTC durante todo el año.
PaddyD
3
¿Por qué el siguiente ejemplo (segundo) disparador se ejecuta n veces o hasta que pasa el segundo? ¿No se suponía que el código activaba la tarea una vez al día?
krizajb
25

En 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);
Víctor
fuente
14
Para mayor legibilidad, sugeriría en TimeUnit.DAYS.toMinutes(1)lugar del "número mágico" 1440.
philonous
Gracias, Victor. De esta manera, ¿necesito reiniciar dos veces al año si quiero que se ejecute a la hora local correcta?
invzbl3
la tasa fija no debe cambiar cuando cambia la hora local, después de creada, se convierte en la tasa.
Victor
¿Cómo es que esto activa mi tarea a las 23:59:59?
krizajb
Para facilitar la lectura, sugeriría 1 en lugar de 1440 o TimeUnit.DAYS.toMinutes (1), y luego use la unidad de tiempo TimeUnit.DAYS. ;-)
Kelly Denehy
7

Si no tiene el lujo de poder utilizar Java 8, lo siguiente hará lo que necesite:

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

No requiere ninguna biblioteca externa y tendrá en cuenta el horario de verano. Simplemente ingrese la hora del día en la que desea ejecutar la tarea como un Calendarobjeto y la tarea como un Runnable. Por ejemplo:

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

Nota: la tarea del temporizador se ejecuta en un subproceso Daemon que no se recomienda para realizar ninguna E / S. Si necesita utilizar un hilo de usuario, deberá agregar otro método que cancele el temporizador.

Si tiene que usar un ScheduledExecutorService, simplemente cambie el startTimermétodo por el siguiente:

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

No estoy seguro del comportamiento, pero es posible que necesite un método de detención que llame shutdownNowsi sigue la ScheduledExecutorServiceruta; de lo contrario, su aplicación puede bloquearse cuando intente detenerla.

PaddyD
fuente
Tengo tu punto. +1 y gracias. Sin embargo, es mejor si no usamos el hilo de Daemon (es decir, new Timer(runThreadName, true)).
Sage
@Sage no se preocupe. Un hilo de demonio está bien si no está haciendo ninguna E / S. El caso de uso para el que escribí esto fue solo una clase simple de disparar y olvidar para iniciar algunos hilos para realizar algunas tareas domésticas diarias. Supongo que si está realizando lecturas de base de datos en el hilo de la tarea del temporizador como lo indica la solicitud de OP, entonces no debe usar un Daemon y necesitará algún tipo de método de detención al que tendrá que llamar para permitir que su aplicación termine. stackoverflow.com/questions/7067578/…
PaddyD
@PaddyD ¿La última parte, es decir, la que usa ScheduledExecutorSerive, fue correcta? La forma en que se crea la clase anónima no parece correcta en cuanto a sintaxis. Además, newSingleThreadExecutor () no tendría el método de programación, ¿verdad?
FReeze FRancis
6

¿Ha considerado utilizar algo como Quartz Scheduler ? Esta biblioteca tiene un mecanismo para programar tareas para que se ejecuten en un período de tiempo establecido todos los días usando una expresión tipo cron (échale un vistazo CronScheduleBuilder).

Algún código de ejemplo (no probado):

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

Hay un poco más de trabajo por adelantado y es posible que deba volver a escribir el código de ejecución de su trabajo, pero debería darle más control sobre cómo desea que se ejecute su trabajo. Además, sería más fácil cambiar el horario si fuera necesario.

lmika
fuente
5

Java8:
mi versión de actualización de la respuesta superior:

  1. Se corrigió la situación en la que el servidor de aplicaciones web no quería detenerse debido al grupo de subprocesos con subproceso inactivo
  2. Sin recursividad
  3. Ejecute la tarea con su hora local personalizada, en mi caso, es Bielorrusia, 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
fuente
1
Tengo un requisito similar. Tengo que programar la tarea para un día a una hora determinada. Una vez que se complete la tarea programada, programe la tarea para el día siguiente a la hora determinada. Esto debería continuar. Mi pregunta es cómo saber que la tarea programada se ha completado o no. Una vez que sepa que la tarea programada se ha completado, solo yo puedo programarla para el día siguiente.
amitwdh
2

Tuve un problema similar. Tuve que programar un montón de tareas que deberían ejecutarse durante un día usando ScheduledExecutorService. Esto se resolvió con una tarea que comenzaba a las 3:30 a.m. y programaba todas las demás tareas en relación con su hora actual . Y reprogramarse para el día siguiente a las 3:30 AM.

Con este escenario, el horario de verano ya no es un problema.

usuario987339
fuente
2

Puede usar un análisis de fecha simple, si la hora del día es anterior a ahora, comencemos mañana:

  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
fuente
1

Solo para sumar la respuesta de Víctor .

Recomendaría agregar un cheque para ver si la variable (en su caso la larga midnight) es mayor que 1440. Si es así, omitiría el .plusDays(1); de lo contrario, la tarea solo se ejecutará pasado mañana.

Lo hice simplemente así:

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
fuente
Se simplifica si utilizatruncatedTo()
Mark Jeronimus
1

El siguiente ejemplo funciona para mí

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

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

Shaik Elias
fuente
1

Puede usar la clase a continuación para programar su tarea todos los días a una hora determinada

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
fuente
1
Por favor, no use lo desactualizado Datey TimeUniten 2019
Mark Jeronimus
0

¿Qué pasa si su servidor deja de funcionar a las 4:59 a. M. Y vuelve a las 5:01 a. M.? Creo que simplemente se saltará la carrera. Recomendaría un programador persistente como Quartz, que almacenaría sus datos de programación en algún lugar. Luego verá que esta ejecución aún no se ha realizado y lo hará a las 5:01 AM.

Sam
fuente