In this tutorial we will walk through how to implement and define crob jobs with a schedule behind of it in Spring Framework. Generally, in software architectures we want to have some background jobs to do some processing, aggregating or even just fetching some data
and streaming them. Instead of implementing separate cron jobs and managing them as a separate application we can create and maintain cron jobs in same spring application with using @Scheduled
annotation. Generally, all the scheduled tasks will run on the same thread which means when current task is being processed others will get blocked and has to wait current thread to finish. However, this can be configured by implementing our own ThreadPoolTaskExecutor
to assign a thread pool. So that each task will run asyncrohonously.
Dependencies
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>
</dependencies>
Enabling Scheduling
First, we have to enable scheduling globally from Spring context to our @Configuration
class by adding @EnableScheduling
annotation.
1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Now before diving in there are some properties belongs to @Scheduled
annotation that we need to know.
- initialDelay
- fixedDelay
- fixedRate
- cron
initialDelay
This property is for making a task to wait for its first execution after application has started. For example; if initialDelay is set to 5000 then the task will wait 5 seconds after the spring application started.
fixedDelay
This property is for making the task to wait N milliseconds after the previous task has finished. For example; if fixedDelay is set to 5000 then Thread_2 will start 5 seconds after the end of the execution of Thread_1.
fixedRate
This property is for making the task to run at every N millisecond without wating the previous task to finish. For example; if fixedRate is set to 5000 then Thread_2 will start 5 seconds after Thread_1’s start time without waiting it to be finished. One thing to note here is since the current running thread won’t wait other ones which will cause all threads allocated in the thread pool. And you might also get StackOverflowException if you run out of memory while running all the threads.
cron
This is the cron expression to define running interval for a task.
1. Schedule a Task with FixedDelay
The first the task will start 5 seconds after spring application started and each of the following task will start 3 seconds after the previous one finished.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.buraktas.jobs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class JobHandler {
Logger logger = LoggerFactory.getLogger(JobHandler.class);
@Scheduled(initialDelay = 5000L, fixedDelay = 3000L)
public void jobWithFixedDelay() throws InterruptedException {
logger.info("Hi from jobWithFixedDelay");
logger.info("Sleeping started.");
Thread.sleep(1000L);
logger.info("Sleeping finished.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2019-09-03 10:55:55.954 INFO 89447 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-09-03 10:55:56.057 INFO 89447 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'taskScheduler'
2019-09-03 10:55:56.109 INFO 89447 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-09-03 10:55:56.112 INFO 89447 --- [ main] com.buraktas.Application : Started Application in 16.3 seconds (JVM running for 21.671)
2019-09-03 10:56:01.085 INFO 89447 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Hi from jobWithFixedDelay
2019-09-03 10:56:01.085 INFO 89447 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping started.
2019-09-03 10:56:02.089 INFO 89447 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping finished.
2019-09-03 10:56:05.094 INFO 89447 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Hi from jobWithFixedDelay
2019-09-03 10:56:05.094 INFO 89447 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping started.
2019-09-03 10:56:06.097 INFO 89447 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping finished.
2019-09-03 10:56:09.099 INFO 89447 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Hi from jobWithFixedDelay
2019-09-03 10:56:09.099 INFO 89447 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping started.
2019-09-03 10:56:10.099 INFO 89447 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping finished.
2. Schedule a Task with FixedRate
The first the task will start 5 seconds after spring application started and each of the following task will start right after the previous one has finished.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class JobHandler {
Logger logger = LoggerFactory.getLogger(JobHandler.class);
@Scheduled(initialDelay = 5000L, fixedRate = 3000L)
public void jobWithFixedRate() throws InterruptedException {
logger.info("Hi from jobWithFixedRate");
logger.info("Sleeping started.");
Thread.sleep(1000L);
logger.info("Sleeping finished.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2019-09-03 10:54:14.202 INFO 89443 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-09-03 10:54:14.323 INFO 89443 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'taskScheduler'
2019-09-03 10:54:14.378 INFO 89443 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-09-03 10:54:14.381 INFO 89443 --- [ main] com.buraktas.Application : Started Application in 16.432 seconds (JVM running for 21.853)
2019-09-03 10:54:19.352 INFO 89443 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Hi from jobWithFixedRate
2019-09-03 10:54:19.352 INFO 89443 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping started.
2019-09-03 10:54:20.356 INFO 89443 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping finished.
2019-09-03 10:54:22.351 INFO 89443 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Hi from jobWithFixedRate
2019-09-03 10:54:22.351 INFO 89443 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping started.
2019-09-03 10:54:23.353 INFO 89443 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping finished.
2019-09-03 10:54:25.350 INFO 89443 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Hi from jobWithFixedRate
2019-09-03 10:54:25.351 INFO 89443 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping started.
2019-09-03 10:54:26.352 INFO 89443 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping finished.
3. Schedule a Task with Cron Expression
First thing to note here is Spring Cron is not same as Unix Cron format which consists of 5 fields to define an expression. Where a Spring Cron expression has 6 fields including time unit of second
in addition to other 5 fields.
Here is the description from CronSequenceGenerator in Spring;
The pattern is a list of six single space-separated fields: representing second, minute, hour, day, month, weekday. Month and weekday names can be given as the first three letters of the English names.
Unix Cron
1
2
3
4
5
6
7
8
9
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday;
# │ │ │ │ │ 7 is also Sunday on some systems)
# │ │ │ │ │
# │ │ │ │ │
# * * * * * command to execute
Spring Cron
1
2
3
4
5
6
7
8
9
10
# ┌────────────── second (0 - 59)
# | ┌───────────── minute (0 - 59)
# | │ ┌───────────── hour (0 - 23)
# | │ │ ┌───────────── day of the month (1 - 31)
# | │ │ │ ┌───────────── month (1 - 12)
# | │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday;
# | │ │ │ │ │ 7 is also Sunday on some systems)
# | │ │ │ │ │
# | │ │ │ │ │
# * * * * * * command to execute
As an example we will define a cron job which will run at 5th minute at each hour from Monday to Friday. So, the time of running in an hours will be 05, 15, 25, 35, 45, 55
and so on.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class JobHandler {
Logger logger = LoggerFactory.getLogger(JobHandler.class);
@Scheduled(cron = "0 5 * * * Mon-Fri")
public void jobWithCronexpression() throws InterruptedException {
logger.info("Hi from jobWithCronexpression");
logger.info("Sleeping started.");
Thread.sleep(1000L);
logger.info("Sleeping finished.");
}
}
1
2
3
4
5
6
7
2019-09-03 13:01:42.116 INFO 90578 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-09-03 13:01:42.218 INFO 90578 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'taskScheduler'
2019-09-03 13:01:42.270 INFO 90578 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-09-03 13:01:42.273 INFO 90578 --- [ main] com.buraktas.Application : Started Application in 16.406 seconds (JVM running for 21.814)
2019-09-03 13:05:00.003 INFO 90578 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Hi from jobWithCronexpression
2019-09-03 13:05:00.004 INFO 90578 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping started.
2019-09-03 13:05:01.008 INFO 90578 --- [ scheduling-1] com.buraktas.jobs.JobHandler : Sleeping finished.
Before finalizing this tutorial there is one thing to mention which is about configuring a thread pool to make these tasks running on multiple threads concurrently. You might notice that all the tasks we ran was running on one thread with name scheduling-1
. Generally, in sofware systems we had more than one job to make them running without blocking each other. So, to make threads running concunrently we will create our own ThreadPoolTaskScheduler
. Here is a quote from the documentation of @EnableScheduling
1
2
3
By default, will be searching for an associated scheduler definition: either a unique TaskScheduler bean in the context, or a TaskScheduler bean named "taskScheduler" otherwise; the same lookup will also be performed for a ScheduledExecutorService bean. If neither of the two is resolvable, a local single-threaded default scheduler will be created and used within the registrar.
When more control is desired, a @Configuration class may implement SchedulingConfigurer. This allows access to the underlying ScheduledTaskRegistrar instance. For example, the following example demonstrates how to customize the Executor used to execute scheduled tasks:
Now we will create a task executor with a thread pool size of 5 inside a configuration class
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
26
27
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executor;
@EnableScheduling
@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean(destroyMethod="shutdown")
public Executor taskExecutor() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(5);
taskScheduler.setThreadNamePrefix("task-scheduler-");
taskScheduler.initialize();
return taskScheduler;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2019-09-03 14:30:32.767 INFO 91108 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'taskScheduler'
2019-09-03 14:30:33.039 INFO 91108 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-09-03 14:30:33.042 INFO 91108 --- [ main] com.buraktas.Application : Started Application in 16.433 seconds (JVM running for 21.874)
2019-09-03 14:30:34.009 INFO 91108 --- [ask-scheduler-1] com.buraktas.jobs.JobHandler : Hi from jobWithFixedRate_2
2019-09-03 14:30:34.311 INFO 91108 --- [ask-scheduler-3] com.buraktas.jobs.JobHandler : Hi from jobWithFixedRate_2
2019-09-03 14:30:34.612 INFO 91108 --- [ask-scheduler-1] com.buraktas.jobs.JobHandler : Hi from jobWithFixedRate_2
2019-09-03 14:30:34.912 INFO 91108 --- [ask-scheduler-1] com.buraktas.jobs.JobHandler : Hi from jobWithFixedRate_2
2019-09-03 14:30:35.010 INFO 91108 --- [ask-scheduler-4] com.buraktas.jobs.JobHandler : Hi from jobWithFixedRate_3
2019-09-03 14:30:35.212 INFO 91108 --- [ask-scheduler-3] com.buraktas.jobs.JobHandler : Hi from jobWithFixedRate_2
2019-09-03 14:30:35.510 INFO 91108 --- [ask-scheduler-3] com.buraktas.jobs.JobHandler : Hi from jobWithFixedRate_2
2019-09-03 14:30:35.812 INFO 91108 --- [ask-scheduler-3] com.buraktas.jobs.JobHandler : Hi from jobWithFixedRate_2
2019-09-03 14:30:36.010 INFO 91108 --- [ask-scheduler-1] com.buraktas.jobs.JobHandler : Hi from jobWithFixedRate
We can clearly see that all the tasks are distributed and scheduled on different threads from the thread pool we initialized.
The final project structure can be found here
Comments powered by Disqus.