Como executar determinada tarefa todos os dias em um horário específico usando ScheduledExecutorService?

90

Estou tentando executar uma determinada tarefa todos os dias às 5 da manhã. Então decidi usar ScheduledExecutorServicepara isso, mas até agora vi exemplos que mostram como executar tarefas a cada poucos minutos.

E não consigo encontrar nenhum exemplo que mostre como executar uma tarefa todos os dias em um determinado horário (5h) da manhã e também considerando o horário de verão -

Abaixo está meu código que será executado a 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();
    }
}

Existe alguma maneira de programar uma tarefa para ser executada todos os dias às 5h da manhã, ScheduledExecutorServiceconsiderando o horário de verão também?

E também TimerTaské melhor para isso ou ScheduledExecutorService?

AKIWEB
fonte
Use algo como quartzo.
milimoose

Respostas:

113

Tal como acontece com a versão atual do java SE 8 com sua excelente API de data e hora, java.timeesse tipo de cálculo pode ser feito mais facilmente em vez de usar java.util.Calendare java.util.Date.

Agora, como exemplo de amostra para agendar uma tarefa com seu 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);

O initalDelayé calculado para pedir ao planejador para atrasar a execução em TimeUnit.SECONDS. Problemas de diferença de tempo com unidade de milissegundos e abaixo parecem ser insignificantes para este caso de uso. Mas você ainda pode usar duration.toMillis()e TimeUnit.MILLISECONDSmanipular os cálculos de agendamento em milissegundos.

E também TimerTask é melhor para isso ou ScheduledExecutorService?

NÃO: ScheduledExecutorService aparentemente melhor do que TimerTask. StackOverflow já tem uma resposta para você .

De @PaddyD,

Você ainda tem o problema de reiniciá-lo duas vezes por ano se quiser que ele seja executado no horário local certo. scheduleAtFixedRate não vai funcionar, a menos que você esteja feliz com o mesmo horário UTC o ano todo.

Como é verdade e @PaddyD já deu uma solução alternativa (+1 para ele), estou fornecendo um exemplo funcional com a API de data e hora Java8 ScheduledExecutorService. Usar o daemon thread é perigoso

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:

  • MyTaské uma interface com função execute.
  • Ao parar ScheduledExecutorService, use sempre awaitTerminationdepois de invocá shutdown-lo: Sempre há uma probabilidade de sua tarefa travar / travar e o usuário esperar para sempre.

O exemplo anterior que dei com o Calender foi apenas uma ideia que mencionei, evitei o cálculo do tempo exato e problemas de horário de verão. Atualizada a solução de acordo com a reclamação de @PaddyD

Sábio
fonte
Obrigado pela sugestão, você pode me explicar em detalhes como intDelayInHoursignifica que estarei executando minha tarefa às 5 da manhã?
AKIWEB
Qual é o propósito de aDate?
José Andias
Mas se você começar em HH: mm, a tarefa será executada às 05: mm em vez de 5 da manhã? Ele também não leva em consideração o horário de verão conforme solicitado pelo OP. Ok, se você iniciá-lo imediatamente após a hora, ou se estiver satisfeito com qualquer horário entre 5 e 6, ou se não se importar em reiniciar o aplicativo no meio da noite duas vezes por ano depois que os relógios mudarem, suponho ...
PaddyD
2
Você ainda tem o problema de reiniciá-lo duas vezes por ano se quiser que ele seja executado no horário local certo. scheduleAtFixedRatenão vai funcionar, a menos que você esteja satisfeito com o mesmo horário UTC o ano todo.
PaddyD
3
Por que o gatilho de exemplo a seguir (segundo) é executado n vezes ou até que o segundo passe? O código não deveria acionar a tarefa uma vez por dia?
krizajb
25

Em 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);
Vencedor
fonte
14
Para TimeUnit.DAYS.toMinutes(1)
facilitar a
Obrigado, Victor. Desta forma, preciso reiniciar duas vezes por ano se quiser que ele seja executado no horário local certo?
invzbl3
a taxa fixa não deve mudar quando a hora local muda, depois de criada, ela se torna sobre a taxa.
Victor
Como isso aciona minha tarefa às 23:59:59?
Krizajb
Para facilitar a leitura, sugiro 1 em vez de 1440 ou TimeUnit.DAYS.toMinutes (1) e, em seguida, use a unidade de tempo TimeUnit.DAYS. ;-)
Kelly Denehy
7

Se você não pode se dar ao luxo de usar o Java 8, o seguinte fará o que você precisa:

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

Não requer libs externas e contabiliza o horário de verão. Basta informar a hora do dia em que você deseja executar a tarefa como um Calendarobjeto e a tarefa como um Runnable. Por exemplo:

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, a tarefa do cronômetro é executada em um thread Daemon que não é recomendado para fazer qualquer IO. Se você precisar usar um thread de usuário, precisará adicionar outro método que cancele o cronômetro.

Se você precisar usar um ScheduledExecutorService, basta alterar o startTimermétodo para o seguinte:

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

Não tenho certeza do comportamento, mas você pode precisar de um método de parada que chama shutdownNowse você seguir a ScheduledExecutorServicerota, caso contrário, seu aplicativo pode travar quando você tentar pará-lo.

PaddyD
fonte
Compreendi seu argumento. +1 e obrigado. No entanto, é melhor se não usarmos o thread Daemon (ou seja, new Timer(runThreadName, true)).
Sábado
@Sage não se preocupe. Um thread daemon é bom se você não estiver fazendo nenhum IO. O caso de uso para o qual escrevi isso era apenas uma classe simples do tipo dispare e esqueça para iniciar alguns threads para executar algumas tarefas de manutenção diária. Suponho que se você estiver realizando leituras de banco de dados no encadeamento da tarefa do cronômetro como a solicitação do OP pode indicar, você não deve usar um Daemon e precisará de algum tipo de método de parada que terá que chamar para permitir que seu aplicativo seja encerrado. stackoverflow.com/questions/7067578/…
PaddyD
@PaddyD foi a última parte, ou seja, aquele que usa ScheduledExecutorSerive correto ?? A forma como a classe anônima é criada não parece correta quanto à sintaxe. Além disso, newSingleThreadExecutor () não teria o método de agendamento certo ??
FReeze FRancis
6

Você já pensou em usar algo como o Quartz Scheduler ? Esta biblioteca tem um mecanismo para agendar tarefas para rodar em um determinado período de tempo todos os dias usando uma expressão parecida com cron (dê uma olhada em CronScheduleBuilder).

Algum código de exemplo (não testado):

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

Há um pouco mais de trabalho inicial e você pode precisar reescrever o código de execução do trabalho, mas deve dar a você mais controle sobre como deseja que o trabalho seja executado. Além disso, seria mais fácil alterar a programação se necessário.

lmika
fonte
5

Java8:
Minha versão de atualização da resposta principal:

  1. Corrigida a situação quando o servidor de aplicativos da Web não queria parar, devido ao threadpool com thread inativo
  2. Sem recursão
  3. Execute a tarefa com seu horário local personalizado, no meu caso, é Belarus, 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
fonte
1
Tenho requisitos semelhantes. Tenho que agendar a tarefa para um dia em determinado horário. Assim que a tarefa agendada for concluída, agende a tarefa para o dia seguinte em determinado horário. Isso deve continuar. Minha pergunta é como descobrir se a tarefa agendada foi concluída ou não. Depois de saber que a tarefa agendada foi concluída, só posso agendar para o dia seguinte.
amitwdh
2

Eu tive um problema parecido. Tive que agendar um monte de tarefas que deveriam ser executadas durante um dia de uso ScheduledExecutorService. Isso foi resolvido por uma tarefa começando às 3h30 agendando todas as outras tarefas relativamente ao seu horário atual . E remarcando-se para o dia seguinte às 3h30.

Com este cenário, o horário de verão não é mais um problema.

user987339
fonte
2

Você pode usar uma análise de data simples, se a hora do dia for anterior a agora, vamos começar amanhã:

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

Só para somar a resposta de Victor .

Eu recomendaria adicionar um cheque para ver se a variável (no caso dele, o longo midnight) é maior que 1440. Se for, omitirei o .plusDays(1), caso contrário, a tarefa só será executada depois de amanhã.

Eu fiz simplesmente assim:

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
fonte
Simplifica se você usartruncatedTo()
Mark Jeronimus
1

O exemplo a seguir funciona para mim

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

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

Shaik Elias
fonte
1

Você pode usar a classe abaixo para agendar sua tarefa todos os dias em um horário específico

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
fonte
1
Por favor, não use o desatualizado Datee TimeUnitem 2019
Mark Jeronimus
0

E se o seu servidor cair às 4h59 e voltar às 5h01? Eu acho que vai simplesmente pular a corrida. Eu recomendaria um programador persistente como o Quartz, que armazenaria seus dados de programação em algum lugar. Em seguida, verá que essa execução ainda não foi realizada e o fará às 5:01.

Sam
fonte
0

Por que complicar uma situação se você pode apenas escrever assim? (sim -> baixa coesão, codificado -> mas é um exemplo e infelizmente de forma imperativa). Para obter informações adicionais, leia o exemplo de código abaixo;))

package timer.test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.concurrent.*;

public class TestKitTimerWithExecuterService {

    private static final Logger log = LoggerFactory.getLogger(TestKitTimerWithExecuterService.class);

    private static final ScheduledExecutorService executorService 
= Executors.newSingleThreadScheduledExecutor();// equal to => newScheduledThreadPool(1)/ Executor service with one Thread

    private static ScheduledFuture<?> future; // why? because scheduleAtFixedRate will return you it and you can act how you like ;)



    public static void main(String args[]){

        log.info("main thread start");

        Runnable task = () -> log.info("******** Task running ********");

        LocalDateTime now = LocalDateTime.now();

        LocalDateTime whenToStart = LocalDate.now().atTime(20, 11); // hour, minute

        Duration duration = Duration.between(now, whenToStart);

        log.info("WhenToStart : {}, Now : {}, Duration/difference in second : {}",whenToStart, now, duration.getSeconds());

        future = executorService.scheduleAtFixedRate(task
                , duration.getSeconds()    //  difference in second - when to start a job
                ,2                         // period
                , TimeUnit.SECONDS);

        try {
            TimeUnit.MINUTES.sleep(2);  // DanDig imitation of reality
            cancelExecutor();    // after canceling Executor it will never run your job again
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("main thread end");
    }




    public static void cancelExecutor(){

        future.cancel(true);
        executorService.shutdown();

        log.info("Executor service goes to shut down");
    }

}
musa
fonte