Jak uruchamiać określone zadanie codziennie o określonej godzinie przy użyciu usługi ScheduledExecutorService?


90

Codziennie o 5 rano staram się wykonać określone zadanie. Postanowiłem więc użyć ScheduledExecutorServicedo tego, ale do tej pory widziałem przykłady, które pokazują, jak uruchamiać zadanie co kilka minut.

I nie jestem w stanie znaleźć żadnego przykładu, który pokazuje, jak uruchamiać zadanie codziennie o określonej godzinie (5 rano) rano, a także biorąc pod uwagę fakt czasu letniego -

Poniżej znajduje się mój kod, który będzie uruchamiany co 15 minut -

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

Czy jest sposób, aby zaplanować uruchamianie zadania codziennie o 5 rano, ScheduledExecutorServicebiorąc pod uwagę również czas letni?

A także TimerTaskjest lepszy do tego lub ScheduledExecutorService?


Zamiast tego użyj czegoś takiego jak kwarc.
millimoose

Odpowiedzi:


113

Podobnie jak w przypadku obecnego wydania java SE 8 z doskonałym interfejsem API daty i czasu, java.timetego rodzaju obliczenia można wykonać łatwiej, zamiast używać java.util.Calendari java.util.Date.

Teraz jako przykładowy przykład planowania zadania w Twoim przypadku użycia:

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

initalDelayJest obliczana zadać harmonogramu opóźnić wykonanie w TimeUnit.SECONDS. Problemy z różnicami czasu z jednostkami milisekund i poniżej wydają się nieistotne w tym przypadku użycia. Ale nadal możesz używać duration.toMillis()i TimeUnit.MILLISECONDSobsługiwać obliczenia planowania w milisekundach.

A także TimerTask jest lepszy dla tego lub ScheduledExecutorService?

NIE: ScheduledExecutorService pozornie lepsze niż TimerTask. StackOverflow ma już dla Ciebie odpowiedź .

Od @PaddyD,

Nadal masz problem, w wyniku którego musisz ponownie uruchomić to dwa razy w roku, jeśli chcesz, aby działało o właściwym czasie lokalnym. scheduleAtFixedRate nie zmniejszy go, chyba że będziesz zadowolony z tego samego czasu UTC przez cały rok.

Ponieważ to prawda, a @PaddyD już podał obejście (+1 dla niego), przedstawiam działający przykład z API daty i godziny Java8 z ScheduledExecutorService. Używanie wątku demona jest niebezpieczne

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

Uwaga:

  • MyTaskjest interfejsem z funkcją execute.
  • Podczas zatrzymywania ScheduledExecutorService, Zawsze używaj awaitTerminationpo wywołaniu shutdown: zawsze istnieje prawdopodobieństwo, że Twoje zadanie utknie / zablokuje się, a użytkownik będzie czekał wiecznie.

Poprzedni przykład, który podałem z Calenderem, był tylko pomysłem, o którym wspomniałem, unikałem dokładnego obliczania czasu i problemów z czasem letnim. Zaktualizowano rozwiązanie na skargę @PaddyD


Dziękuję za sugestię, czy możesz mi szczegółowo wyjaśnić, w jaki intDelayInHoursposób będę wykonywał swoje zadanie o 5 rano?
AKIWEB,

Jaki jest cel aDate?
José Andias

Ale jeśli zaczniesz to od HH: mm, zadanie zostanie uruchomione o 05: mm, a nie o 5 rano? Nie uwzględnia również czasu letniego, zgodnie z żądaniem OP. Ok, jeśli uruchomisz go natychmiast po godzinie lub jeśli jesteś zadowolony z dowolnego czasu między 5 a 6, lub jeśli nie masz nic przeciwko restartowaniu aplikacji w środku nocy dwa razy w roku po zmianie zegarów. ...
PaddyD

2
Nadal masz problem, w wyniku którego musisz ponownie uruchomić to dwa razy w roku, jeśli chcesz, aby działało o właściwym czasie lokalnym. scheduleAtFixedRatenie przerwie, chyba że będziesz zadowolony z tego samego czasu UTC przez cały rok.
PaddyD,

3
Dlaczego poniższy przykład (drugi) wyzwalacz jest wykonywany n razy lub do momentu, gdy minie drugi? Czy kod nie miał uruchamiać zadania raz dziennie?
krizajb

25

W Javie 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
Dla TimeUnit.DAYS.toMinutes(1)większej czytelności sugerowałbym zamiast „magicznej liczby” 1440.
filoniczny

Dzięki, Victor. W ten sposób trzeba uruchamiać ponownie dwa razy w roku, jeśli chcę, aby działał o właściwym czasie lokalnym?
invzbl3

stała stawka nie powinna zmieniać się, gdy zmienia się czas lokalny, po utworzeniu staje się ona o stawce.
Victor,

Dlaczego to uruchamia moje zadanie o 23:59:59?
krizajb

Dla czytelności sugerowałbym 1 zamiast 1440 lub TimeUnit.DAYS.toMinutes (1), a następnie użyj jednostki czasu TimeUnit.DAYS. ;-)
Kelly Denehy

7

Jeśli nie masz luksusu korzystania z języka Java 8, wykonaj następujące czynności:

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

Nie wymaga żadnych zewnętrznych bibliotek i uwzględni czas letni. Po prostu podaj porę dnia, w której chcesz uruchomić zadanie jako Calendarobiekt, a zadanie jako plik Runnable. Na przykład:

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

Uwaga: zadanie czasomierza działa w wątku Daemon, co nie jest zalecane do wykonywania jakichkolwiek operacji we / wy. Jeśli chcesz użyć wątku użytkownika, musisz dodać inną metodę, która anuluje licznik czasu.

Jeśli musisz użyć a ScheduledExecutorService, po prostu zmień startTimermetodę na następującą:

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

Nie jestem pewien zachowania, ale możesz potrzebować metody zatrzymania, która wywoła, shutdownNowjeśli zejdziesz z ScheduledExecutorServicetrasy, w przeciwnym razie aplikacja może się zawiesić, gdy spróbujesz ją zatrzymać.


Rozumiem co masz na myśli. +1 i dziękuję. Jednak lepiej jest nie używać wątku Daemon (tj new Timer(runThreadName, true).).
Sage,

@Sage nie martw się. Wątek demona jest w porządku, jeśli nie wykonujesz żadnych operacji we / wy. Przypadek użycia, dla którego to napisałem, był po prostu prostą klasą „odpal i zapomnij”, służącą do uruchamiania niektórych wątków w celu wykonania niektórych codziennych zadań porządkowych. Przypuszczam, że jeśli wykonujesz odczyty bazy danych w wątku zadań timera, jak może wskazywać żądanie OP, nie powinieneś używać demona i będziesz potrzebować jakiejś metody zatrzymania, którą będziesz musiał wywołać, aby umożliwić zakończenie działania aplikacji. stackoverflow.com/questions/7067578/…
PaddyD

@PaddyD Czy ostatnia część, tj. Ta używająca ScheduledExecutorSerive, była poprawna ??? Sposób tworzenia klasy anonimowej nie wygląda na poprawną składnię. Również newSingleThreadExecutor () nie miałby właściwej metody planowania?
FReeze FRancis

6

Czy rozważałeś użycie czegoś takiego jak Quartz Scheduler ? Ta biblioteka ma mechanizm planowania zadań do wykonywania codziennie o określonej porze przy użyciu wyrażenia podobnego do crona (spójrz CronScheduleBuilder).

Przykładowy kod (nie testowany):

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

Na początku jest trochę więcej pracy i być może będziesz musiał przepisać kod wykonania zadania, ale powinno to dać ci większą kontrolę nad sposobem, w jaki chcesz wykonać zadanie. W razie potrzeby łatwiej byłoby też zmienić harmonogram.


5

Java8:
Moja wersja uaktualnienia z najlepszej odpowiedzi:

  1. Naprawiono sytuację, gdy serwer aplikacji sieci Web nie chce się zatrzymywać z powodu puli wątków z bezczynnym wątkiem
  2. Bez rekursji
  3. Uruchom zadanie z niestandardowym czasem lokalnym, w moim przypadku jest to Białoruś, Mińsk


/**
 * 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
Mam podobne wymaganie. Muszę zaplanować zadanie na jeden dzień o określonej godzinie. Po zakończeniu zaplanowanego zadania zaplanuj zadanie na następny dzień o określonej godzinie. Powinno to trwać. Moje pytanie brzmi, jak sprawdzić, czy zaplanowane zadanie zostało ukończone, czy nie. Kiedy wiem, że zaplanowane zadanie zostało zakończone, mogę zaplanować tylko na następny dzień.
amitwdh

2

Miałem podobny problem. Musiałem zaplanować szereg zadań, które powinny być wykonywane w ciągu dnia za pomocą ScheduledExecutorService. Zostało to rozwiązane przez jedno zadanie rozpoczynające się o 3:30 rano, planujące wszystkie inne zadania w stosunku do jego aktualnego czasu . I przełożył się na następny dzień o 3:30.

W tym scenariuszu czas letni nie jest już problemem.


2

Możesz użyć prostej analizy dat, jeśli pora dnia jest wcześniejsza, zacznijmy jutro:

  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

Żeby podsumować odpowiedź Victora .

Poleciłbym dodać czek, aby zobaczyć, czy zmienna (w jego przypadku długa midnight) jest wyższa niż 1440. Jeśli tak, pominęłabym .plusDays(1), w przeciwnym razie zadanie zostanie uruchomione dopiero pojutrze.

Zrobiłem to po prostu tak:

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

Upraszcza, jeśli korzystasz ztruncatedTo()
Mark Jeronimus

1

Poniższy przykład działa dla mnie

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ódło: https://chynten.wordpress.com/2016/06/03/java-scheduler-to-run-every-day-on-specific-time/


1

Możesz użyć poniższej klasy, aby zaplanować swoje zadanie każdego dnia o określonej porze

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
Proszę nie używać przestarzałych Dateiw TimeUnit2019 roku
Mark Jeronimus

0

Co się stanie, jeśli serwer przestanie działać o 4:59 i wróci o 5:01? Myślę, że po prostu pominie bieg. Poleciłbym stały harmonogram, taki jak Quartz, który przechowywałby gdzieś swoje dane harmonogramu. Wtedy zobaczy, że ten bieg nie został jeszcze wykonany i zrobi to o 5:01.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.