Czasami zdarza się taka potrzeba, aby uruchomić, cyklicznie, pewne zadania bez ingerencji użytkownika. Może to być np. przetwarzanie wsadowe, o którym pisałem w jednym z poprzednich wpisów. Z pomocą przychodzi nam, więc adnotacja @Scheduled udostępniona przez framework Springa. Sprawdźmy jak ona działa.

Wymagania

Projektując metodę, którą chcemy oznaczyć adnotacją @Scheduled, musimy pamiętać o dwóch warunkach:

  • powinna mieć zwracany typ void (w przypadku, gdyby coś jednak zwracała to wynik zostanie zignorowany)
  • nie powinna mieć żadnych parametrów (inaczej zostanie rzucony wyjątek IllegalStateException)

Oczywiście należy jeszcze wykorzystać adnotację @EnableScheduling na głównej klasie aplikacji lub klasie konfiguracyjnej. Gdybyśmy tego nie zrobili to program od razu by się wyłączył bez uruchomienia naszego zadania ani razu.

Bebechy @Scheduled

Zajrzyjmy teraz do bebechów adnotacji @Scheduled. Do dyspozycji mamy w niej trochę elementów, które mają swoje specjalne zastosowanie.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
    String CRON_DISABLED = "-";

    String cron() default "";

    String zone() default "";

    long fixedDelay() default -1L;

    String fixedDelayString() default "";

    long fixedRate() default -1L;

    String fixedRateString() default "";

    long initialDelay() default -1L;

    String initialDelayString() default "";

    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}

Przyjrzyjmy się teraz dokładniej każdemu z osobna.

fixedDelay

1
2
3
4
@Scheduled(fixedDelay = 2000)
void runTaskUsingFixedDelayElement() {
    System.out.println("Fixed delay task: " + LocalDateTime.now());
}

Element fixedDelay pozwala nam uruchomić zadanie co określoną ilość milisekund (domyślnie) pomiędzy zakończeniem poprzedniego wywołania a rozpoczęciem następnego.

Uruchamianie zadań przy użyciu fixedDelay
Uruchamianie zadań przy użyciu fixedDelay

Jeżeli uruchomimy to zadanie zobaczymy, że pomiędzy wywołaniami zawsze jest odstęp 2 sekund plus kilka milisekund. Dzieje się tak dlatego, że framework potrzebuje jeszcze chwilę, aby uruchomić metodę. Stąd ta niewielka obsuwa.

Fixed delay task: 2021-10-13T12:16:46.851164600
Fixed delay task: 2021-10-13T12:16:48.866140
Fixed delay task: 2021-10-13T12:16:50.881142
Fixed delay task: 2021-10-13T12:16:52.896140
Fixed delay task: 2021-10-13T12:16:54.911142100
Fixed delay task: 2021-10-13T12:16:56.926142200
Fixed delay task: 2021-10-13T12:16:58.941142400
Fixed delay task: 2021-10-13T12:17:00.956170100
Fixed delay task: 2021-10-13T12:17:02.971140800
Fixed delay task: 2021-10-13T12:17:04.986165500

Do dyspozycji jest również element fixedDelayString, który przyjmuje wartość liczbową w postaci, jak sama nazwa wskazuje, literału. Może to być po prostu liczba albo wyrażenie w formacie zrozumiałym przez klasę java.time.Duration jak np. “PT48H” określające dwa dni. Daje to nam jeszcze taką przewagę, że element może przybrać wartość zdefiniowaną w pliku konfiguracyjnym.

fixedRate

1
2
3
4
@Scheduled(fixedRate = 2000)
void runTaskUsingFixedRateElement() {
    System.out.println("Fixed rate task: " + LocalDateTime.now());
}

Element fixedRate natomiast uruchomia zadanie co określoną ilość milisekund (domyślnie) pomiędzy rozpoczęciem poprzedniego wywołania a rozpoczęciem następnego.

Uruchamianie zadań przy użyciu fixedRate
Uruchamianie zadań przy użyciu fixedRate

Wynikiem działania wyżej przedstawionej metody są logi w konsoli, które informują nas, że pomiędzy kolejnymi wywołaniami zawsze mija +/- 2 sekundy. Jest to bliższe temu co podaliśmy w elemencie adnotacji, ponieważ zdania nie weryfikują czy poprzednie zostało zakończone. Po prostu się uruchamiają. Dla fixedDelay jest także dostępna wersja z literałem tak jak to było w przypadku fixedDelay - ma ona takie samo zastosowanie.

Fixed rate task: 2021-10-13T12:44:05.200284400
Fixed rate task: 2021-10-13T12:44:07.199753300
Fixed rate task: 2021-10-13T12:44:09.205254900
Fixed rate task: 2021-10-13T12:44:11.198753800
Fixed rate task: 2021-10-13T12:44:13.198253200
Fixed rate task: 2021-10-13T12:44:15.197782800
Fixed rate task: 2021-10-13T12:44:17.197253200
Fixed rate task: 2021-10-13T12:44:19.196782500
Fixed rate task: 2021-10-13T12:44:21.196285700
Fixed rate task: 2021-10-13T12:44:23.195755300

Warto zaznaczyć, że w przypadku fixedRate może wystąpić pewien problem. Istnieje szansa, że jedno zadanie nie zdąży się zakończyć, kiedy będzie podjęta próba uruchomienia następnego. Domyślenie zadania uruchamiają się jednowątkowo, więc kolejne zadanie nie wystartuje pomimo tego, że wymagany czas upłynął. Będzie czekało dopóki poprzednie nie zostanie zakończone.

Tutaj na pomoc przychodzi adnotacja @Async. Umieszczając ją nad problematyczną metodą pozwalamy, aby zadania były uruchamiane współbieżnie. Oczywiście musimy przy tym skorzystać z adnotacji @EnableAsync nad klasą konfiguracyjną, aby w ogóle włączyć taką opcje. Teraz nie jest ważne czy poprzednie zadanie się zakończyło. W sytuacji, gdy nastąpi kolizja kolejne po prostu uruchomi się na nowym wątku.

Jako podsumowanie można dodać, że fixedDelay został stworzony do zadań, które mogą być od siebie zależne. Natomiast przeznaczeniem fixedRate są zadania nieposiadające zasobów współdzielonych między sobą.

initialDelay

Odpowiedzialnością initialDelay jest to, aby opóźnić cykliczne wywołanie danego zadania o podaną wartość. Działa zarówno dla fixedRate jak i fixedDelay. Po upływie wybranej liczby milisekund (domyślnie) wszystko będzie działało w ten sposób jak to było opisane wyżej. Oczywiście również przy initialDelay możemy wspomagać się literałem do zdefiniowania wartości.

cron

Jest to bardzo potężne narzędzie pozwalające nam uruchomić wybrane zadanie zgodnie z podanym przez nas wzorem. Dzięki niemu możemy je uruchomić np. co godzinę, w ostatni piątek miesiąca czy między 6 a 9 co 10 minut każdego dnia. Jak to zrobić? Trzeba nauczyć się posługiwać wyrażeniami Cron.

Objaśnienie wyrażenia Cron
Objaśnienie wyrażenia Cron, źródło

Zasady użycia Cron

Według dokumentacji należy podążać według następujących zasad:

  • pole z ‘*’ oznacza pełny zasięg danej zmiennej (np. dla sekundy będzie to 0-59), w przypadku dni miesiąca i dni tygodnia dopuszczalne jest stosowanie ‘?’ zamiast ‘*’
  • zakres wyrażamy za pomocą znaku ‘-‘ włącznie (np. 1-3 oznacza 1, 2 i 3 godzinę)
  • znak ‘/’ oznacza interwał trwający podany zakres (np. */10 oznacza, że coś będzie wywoływało się co 10 minut)
  • w przypadku miesięcy i dni tygodnia można stosować nazwy angielskie jak np. JAN - January czy MON - Monday (wielkość liter nie ma znaczenia)
  • dni tygodnia i miesiąca mogą zawierać literkę ‘L’ oznaczającą ostatni dzień, dla każdego jednak ma to odmienne znaczenie:
    • dla dni miesiąca:
      • dla dnia miesiąca ‘L’ oznacza ostatni dzień miesiąca
      • zapis ‘L-n’ (gdzie n to liczba) określa, że coś ma się wykonać od n dni wstecz do ostatniego dnia (np. L-1 oznacza przedostatni i ostatni miesiąca)
      • ‘LW’ natomiast oznacza ostatni dzień pracujący miesiąca
    • dla dni tygodnia:
      • symbol ‘L’ oznacza ostatni dzień tygodnia
      • prefix przed ‘L’ w postaci liczby bądź trzech angielskich liter dnia tygodnia określa ostatni wybrany dzień tygodnia w miesiącu (np. MONL to ostatni poniedziałek miesiąca)
  • dzień miesiąca może zawierać zapis ‘nW’ (gdzie n to liczba) określając najbliższy pracujący dzień miesiąca do podanej wartości ‘n’ (np. 3W to trzeci pracujący dzień miesiąca)
  • dzień tygodnia zapisany w postaci ‘d#n’ lub ‘DDD#n’ to wybrany dzień tygodnia w skali miesiąca (np. ‘5#2 to drugi piątek miesiąca)

Przykłady użycia Cron

Bogatsi w tą wiedzę możemy utworzyć takie o to przykładowe zapisy:

  • ”*/10 * 10 * * *” - co równe 10 sekund o godzinie 10
  • “30 10 14 * * WEDL” - co miesiąc o 14:10:30 w ostatnią środę miesiąca
  • “0 0 4 * * SAT-SUN” - o 4 nad ranem w weekendy
  • “0 0 0 ? 5 ?” - o północy każdego dnia w maju
  • “0 0 17 L-1 DEC *” - przedostatniego i ostatniego dnia grudnia o godzinie 17

zone i timeUnit

zone to nic innego jak strefa czasowa. Domyślnie wykorzystywana jest lokalna strefa czasowa (pusty String), ale możemy to zmienić podając literał akceptowany przez wywołanie TimeZone.getTimeZone(String) (czyli np. “GMT-8:00”, “Poland/Warsaw” czy “PST” - czas pacyficzny). timeUnit domyślnie ustawiony jest na milisekundy, jednak również i tu jesteśmy w stanie dostosować rozwiązanie do naszych potrzeb. Do wykorzystania mamy: dni (DAYS), godziny (HOURS), minuty (MINUTES), sekundy (SECONDS), milisekundy (MILLISECONDS), mikrosekundy (MICROSECONDS) oraz nanosekundy (NANOSECONDS). Podawane przez nas wartości do fixedRate czy fixedDelay będą miały taką jednostkę jaką zdefiniujemy właśnie w timeUnit.

Podsumowanie

Dzięki @Scheduled zyskujemy inny sposób na wywołanie naszego kodu niż przy pomocy wiersza poleceń czy wywołań REST. Jest to przydatne narzędzie jeśli chcemy wykonać jakieś zadanie, które może być czasochłonne i przez to najlepiej jak wykona się w nocy albo chcemy zebrać więcej danych i wysłać je gdzieś w paczce. Dla przykładu może to być codziennie naliczanie odsetek kredytowych dla banku czy też zbiorcza wysyłka maili.

Teraz przekonaj się na własnej skórze jak dokładnie działają elementy adnotacji @Scheduled we własnym kodzie. Jeśli jednak to będzie dla Ciebie za mało to sprawdź co możesz osiągnąć wykorzystując interfejs SchedulingConfigurer zapoznając się z tym artykułem na Baeldung.