Spring Boot integracija Spring Task-a za izvršavanje zakazanih zadataka
Scenariji u kojima se zakazani zadaci koriste prilično su česti:
- Backup podataka
- Automatsko otkazivanje neplaćenih porudžbina
- Zakazano preuzimanje podataka
- Zakazano slanje obaveštenja
- Zakazano objavljivanje članaka
- Zakazano generisanje izveštaja
- I tako dalje (ne pada mi na pamet više scenarija, pa eto, ostaje "i tako dalje" — i to je svojevrsni zakazani scenario!)
Timer
Način za implementaciju zakazanih zadataka koji je JDK podržava još od verzije 1.3. Za čuvanje zakazanih zadataka iznutra koristi klasu TaskQueue, prilično je jednostavan za korišćenje, ali ima dosta mana: na primer, jedan Timer pokreće samo jednu nit, pa kada zadataka ima mnogo performanse drastično opadaju; ili ako tokom izvršavanja jedan TimerTask dugo traje, uticaće na raspored ostalih zadataka.
@Slf4j
public class TimerDemo {
public static void main(String[] args) {
TimerTask task = new TimerTask() {
@Override
public void run() {
log.debug("trenutno vreme {} ime niti {}", DateTime.now(),
Thread.currentThread().getName());
}
};
log.debug("trenutno vreme {} ime niti {}", DateTime.now(),
Thread.currentThread().getName());
Timer timer = new Timer("TimerDemo");
timer.schedule(task,1000L);
}
}Nakon pokretanja koda, log izgleda ovako:
13:11:45.268 [main] DEBUG top.springtask.TimerDemo - trenutno vreme 2022-04-27 13:11:45 ime niti main
13:11:46.280 [TimerDemo] DEBUG top.springtask.TimerDemo - trenutno vreme 2022-04-27 13:11:46 ime niti TimerDemoScheduledThreadPoolExecutor
Zakazani zadaci koje nudi JDK od verzije 1.5; nasleđuje ThreadPoolExecutor i implementira interfejs ScheduledExecutorService, pa podržava izvršavanje zadataka u istovremenim scenarijima. Takođe otklanja nedostatke Timer-a. Međutim, pošto za realizaciju tajmera koristi red, postoje operacije ubacivanja/izbacivanja iz reda i prilagođavanja gomile, pa vreme nije baš apsolutno tačno (ovo je sitnica).
@Slf4j
public class ScheduledThreadPoolExecutorDemo {
public static void main(String[] args) throws InterruptedException {
TimerTask task = new TimerTask() {
@Override
public void run() {
log.debug("trenutno vreme {} ime niti {}", DateTime.now(),
Thread.currentThread().getName());
}
};
log.debug("trenutno vreme {} ime niti {}", DateTime.now(),
Thread.currentThread().getName());
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
executorService.scheduleAtFixedRate(task, 1000L,1000L, TimeUnit.MILLISECONDS);
Thread.sleep(1000+1000*4);
executorService.shutdown();
}
}Izlaz izgleda ovako:
14:43:41.740 [main] DEBUG top.springtask.ScheduledThreadPoolExecutorDemo - trenutno vreme 2022-04-27 14:43:41 ime niti main
14:43:42.752 [pool-1-thread-1] DEBUG top.springtask.ScheduledThreadPoolExecutorDemo - trenutno vreme 2022-04-27 14:43:42 ime niti pool-1-thread-1
14:43:43.748 [pool-1-thread-1] DEBUG top.springtask.ScheduledThreadPoolExecutorDemo - trenutno vreme 2022-04-27 14:43:43 ime niti pool-1-thread-1
14:43:44.749 [pool-1-thread-2] DEBUG top.springtask.ScheduledThreadPoolExecutorDemo - trenutno vreme 2022-04-27 14:43:44 ime niti pool-1-thread-2
14:43:45.749 [pool-1-thread-2] DEBUG top.springtask.ScheduledThreadPoolExecutorDemo - trenutno vreme 2022-04-27 14:43:45 ime niti pool-1-thread-2
14:43:46.749 [pool-1-thread-2] DEBUG top.springtask.ScheduledThreadPoolExecutorDemo - trenutno vreme 2022-04-27 14:43:46 ime niti pool-1-thread-2O Spring Task-u
Spring Task je alat za zakazane zadatke koji Spring pruža, što znači da nije potrebno dodavati zavisnosti ka drugim proizvođačima; u poređenju sa drugim third-party bibliotekama zgodniji je i lakši za korišćenje.
Čini se da o Spring Task-u nema više nepotrebnih reči, pa da pređemo direktno na praksu.
Prvi korak: napravite klasu za konfiguraciju SpringTaskConfig i dodajte anotaciju @EnableScheduling kojom se omogućava Spring Task.
@Configuration
@EnableScheduling
public class SpringTaskConfig {
}Naravno, možete i ne praviti ovu konfiguracionu klasu, već dodati anotaciju @EnableScheduling direktno na glavnu klasu.
@SpringBootApplication
@EnableScheduling
public class CodingmoreSpringtaskApplication {
public static void main(String[] args) {
SpringApplication.run(CodingmoreSpringtaskApplication.class, args);
}
}Drugi korak: napravite klasu zakazanih zadataka CronTask; pomoću anotacije @Scheduled registrujte Cron izraz koji izvršava zakazani zadatak.
@Slf4j
@Component
public class CronTask {
@Scheduled(cron = "0/1 * * ? * ?")
public void cron() {
log.info("zakazano izvršavanje, vreme {}", DateUtil.now());
}
}Pokrenite server i ustanovićete da se log ispisuje na svaku sekundu, što dokazuje da je Cron izrazni oblik Spring Task-a proradio.

Podrazumevano, veličina pool-a niti koje kreira @Scheduled iznosi 1; ako želite da je povećate, klasa SpringTaskConfig može da implementira interfejs SchedulingConfigurer i pomoću setPoolSize uveća veličinu pool-a niti.
@Configuration
@EnableScheduling
public class SpringTaskConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(10);
threadPoolTaskScheduler.setThreadNamePrefix("my-scheduled-task-pool-");
threadPoolTaskScheduler.initialize();
taskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
}
}Nakon što se završi hot redeploy usluge, u konzoli ćete videti ovakav ispis:

Može se potvrditi da je prilagođena veličina pool-a niti proradila: neki zadaci koriste nit led-task-pool-3, neki led-task-pool-7; kada se duže radi, nailazi se na led-task-pool-1 pa sve do led-task-pool-10.

Pored Cron izraza, Spring Task podržava i fixedRate (fiksna brzina izvršavanja), fixedDelay (fiksno kašnjenje), i initialDelay (početno kašnjenje) — tri načina upotrebe.
/**
* fixedRate: izvršavanje fiksnom brzinom. Jednom na svakih 5 sekundi.
*/
@Scheduled(fixedRate = 5000)
public void reportCurrentTimeWithFixedRate() {
log.info("Current Thread : {}", Thread.currentThread().getName());
log.info("Fixed Rate Task : The time is now {}", DateUtil.now());
}
/**
* fixedDelay: izvršavanje sa fiksnim kašnjenjem. Izvršava se 2 sekunde nakon poslednjeg uspešnog poziva.
*/
@Scheduled(fixedDelay = 2000)
public void reportCurrentTimeWithFixedDelay() {
try {
TimeUnit.SECONDS.sleep(3);
log.info("Fixed Delay Task : The time is now {}",DateUtil.now());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* initialDelay: početno kašnjenje. Prvo izvršavanje zadatka odlazi 5 sekundi, a zatim se izvršava na svakih 5 sekundi.
*/
@Scheduled(initialDelay = 5000, fixedRate = 5000)
public void reportCurrentTimeWithInitialDelay() {
log.info("Fixed Rate Task with Initial Delay : The time is now {}", DateUtil.now());
}Međutim, fixedRate krije zamku: pretpostavimo da je za neki metod tajmerom fiksno podešeno izvršavanje na svakih 5 sekundi, a taj metod treba da odradi četiri zadatka čija trajanja su 6s, 6s, 2s, 3s — kako će zadaci da se izvrše (u okruženju sa jednom niti)?
2022-04-27 15:25:52.400 INFO 4343 --- [led-task-pool-1] c.codingmore.component.PublishPostTask : Fixed Rate Task : The time is now 2022-04-27 15:25:52
2022-04-27 15:25:58.401 INFO 4343 --- [led-task-pool-1] c.codingmore.component.PublishPostTask : Fixed Rate Task : The time is now 2022-04-27 15:25:58
2022-04-27 15:26:00.407 INFO 4343 --- [led-task-pool-1] c.codingmore.component.PublishPostTask : Fixed Rate Task : The time is now 2022-04-27 15:26:00
2022-04-27 15:26:04.318 INFO 4343 --- [led-task-pool-1] c.codingmore.component.PublishPostTask : Fixed Rate Task : The time is now 2022-04-27 15:26:04Relativno vreme početka prvog zadatka jeste sekunda 0, ali pošto se izvršavao 6 sekundi, zadatak koji je trebalo da krene u sekundi 5 kreće tek u sekundi 6; treći zadatak kasni 12 sekundi, a trebalo je da krene u sekundi 10; četvrti zadatak ne kasni i normalno se izvršava posle 15 sekundi.
Ako pomoću anotacije @EnableAsync omogućimo višenitno okruženje, kakav će biti rezultat?
2022-04-27 15:33:01.385 INFO 4421 --- [led-task-pool-1] c.codingmore.component.PublishPostTask : Fixed Rate Task : The time is now 2022-04-27 15:33:01
2022-04-27 15:33:07.390 INFO 4421 --- [led-task-pool-1] c.codingmore.component.PublishPostTask : Fixed Rate Task : The time is now 2022-04-27 15:33:07
2022-04-27 15:33:09.391 INFO 4421 --- [led-task-pool-1] c.codingmore.component.PublishPostTask : Fixed Rate Task : The time is now 2022-04-27 15:33:09
2022-04-27 15:33:13.295 INFO 4421 --- [led-task-pool-1] c.codingmore.component.PublishPostTask : Fixed Rate Task : The time is now 2022-04-27 15:33:13O Cron izrazima
Ovde usput pominjemo i Cron izraze koje ćete često sretati u zakazanim zadacima. Reč Cron potiče iz grčkog chronos, što izvorno znači vreme.
Cron izraz je niz znakova sa vremenskim značenjem, razdvojen sa 5 razmaka, što ga deli na 6 vremenskih elemenata. Nekoliko primera i sve postaje jasno.
| Primer | Objašnjenje |
|---|---|
0 15 10 ? * * | Izvršava zadatak svakog dana u 10:15 pre podne |
0 0 10,14,16 * * ? | Izvršava zadatak svakog dana u 10, 14 i 16 časova |
0 0 12 ? * 3 | Izvršava zadatak sredom u podne (12:00) |
0 15 10 15 * ? | Izvršava zadatak 15. u mesecu u 10:15 pre podne |
Sintaksni format Cron-a može se sumirati kao:
Seconds Minutes Hours DayofMonth Month DayofWeek
Opsezi vrednosti svakog vremenskog elementa i specijalni znakovi koji se mogu pojaviti su sledeći.
| Vremenski element | Opseg vrednosti | Mogući specijalni znakovi |
|---|---|---|
| sekunde | [0,59] | *,-/ |
| minuti | [0,59] | *,-/ |
| sati | [0,59] | *,-/ |
| dan u mesecu | [0,31] | *,-/?LW |
| mesec | [1,12] | *,-/ |
| dan u nedelji | [1,7] | *,-/?L# |
Značenja specijalnih znakova i primeri su sledeći.
| Specijalni znak | Značenje | Primer |
|---|---|---|
* | Sve moguće vrednosti | Lako razumljivo: u polju meseca svaki mesec, u polju dana u nedelji svaki dan u nedelji |
, | Nabrojane vrednosti | Lako razumljivo: u polju sati 10,14,16 znači da su izabrani ti sati |
- | Opseg | Lako razumljivo: u polju minuta 10-19 znači da se izvršava svakog minuta od 10. do 19. minuta |
/ | Inkrement zadate vrednosti | Lako razumljivo: u polju minuta 0/15 znači da se izvršava na svakih 15 minuta |
? | Bez zadate vrednosti | Lako razumljivo: ako je polje dana u mesecu zadato, polje dana u nedelji ne može da se zada, i obrnuto, jer su ta dva polja u konfliktu |
L | Prvo slovo reči Last | Lako razumljivo: podržano u poljima dana u mesecu i dana u nedelji, označava poslednji dan u mesecu ili poslednji dan u nedelji |
W | Radni dan, bez vikenda | Lako razumljivo: podržano samo u polju dana u mesecu |
# | Koji dan u nedelji u mesecu | Lako razumljivo: podržano samo u polju dana u nedelji, 4#2 označava drugi četvrtak u mesecu |
Kratak rezime
Iako je Spring Task veoma zgodan za korišćenje, način upotrebe mu je previše jednostavan, pa mnoge složene poslovne logike nije moguće njime ostvariti — na primer, funkcija zakazanog objavljivanja članaka u codingmore-u se ne može izvesti samo pomoću Spring Task-a.
Putanja do izvornog koda
- codingmore: https://github.com/itwanger/coding-more
- codingmore-springtask: https://github.com/itwanger/codingmore-learning
