Task scheduling is frequently used in web applications to execute different jobs at certain times without any manual input. Examples include backing up the database, sending newsletter emails, deleting log files, and moving files from one server to another server, just to name a few.

Spring Boot provides multiple ways to schedule tasks. You can use the @Scheduled annotation or a custom thread pool to run your scheduled tasks at specific times.

In this article, we shall learn how to schedule tasks in a Spring Boot application using the @Scheduled annotation. We will also take a look at using a custom thread pool to execute scheduled tasks.

Dependencies

We only need the spring-boot-starter dependency to use the @Scheduled annotation. Add the following to your build.gradle file:

implementation 'org.springframework.boot:spring-boot-starter'

If you are using Maven, add the following dependency to your pom.xml file:

<dependency> 
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Enable Scheduling

We can enable the scheduling functionality by adding the @EnableScheduling annotation to the main application class or any other configuration class like the below:

Application.java

package com.attacomsian.scheduling;

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

@EnableScheduling annotation ensures that a background task executor is created with a single thread.

Scheduling Tasks

Scheduling a task is easy. Simply add the @Scheduled annotation to any method you want to schedule and set the time it should execute. However, all such methods must meet the following two conditions:

  1. The method return type should be void (void)
  2. The method should not accept any parameter

That being said, let us create a Java class that will act as a container to hold all our scheduled tasks:

ScheduledTasks.java

package com.attacomsian.scheduling;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.time.format.DateTimeFormatter;

@Component
public class ScheduledTasks {

    private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class);
    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");

    // add scheduled methods here
}

Schedule Task with Fixed Rate

Let us schedule our first task that executes at a fixed interval of time by using the fixedRate property in the @Scheduled annotation:

@Scheduled(fixedRate = 2500)
public void scheduleTaskWithFixedRate() {
    logger.info("Fixed Rate Task: Current Time - {}", formatter.format(LocalDateTime.now()));
}

The above task is scheduled to execute every 3 seconds and outputs the following on the console:

Fixed Rate Task: Current Time - 13:36:53
Fixed Rate Task: Current Time - 13:36:56
Fixed Rate Task: Current Time - 13:36:58
Fixed Rate Task: Current Time - 13:37:01
...

The fixedRate task is invoked for every specified interval. The specified interval between method invocations is measured from the start time of each execution. In other words, the job is executed again even if the previous task invocation is not completed.

This option is best suited when each execution of the task is independent. The fixedRate property executes the job at every n milliseconds. It does not wait for any previous execution to finish.

Schedule Task with Fixed Delay

Like fixedRate, the fixedDelay task is invoked for every specified interval, but the time is measured from the completion time of each preceding invocation.

In short, the time between the end of the last execution and the start of the subsequent invocation is constant. The task always waits until the previous one is completed.

Consider the following example:

@Scheduled(fixedDelay = 2000)
public void scheduleTaskWithFixedDelay() throws InterruptedException {
    logger.info("Fixed Delay Task: Start Time - {}", formatter.format(LocalDateTime.now()));

    // add some virtual processing time
    TimeUnit.SECONDS.sleep(3);

    logger.info("Fixed Delay Task: End Time - {}", formatter.format(LocalDateTime.now()));
}

The task is scheduled to execute every 2 seconds and prints the start and finish times of the execution. Since we have added 3 seconds of virtual processing time, it will take at least 3 seconds to complete. There will be a delay of 5 seconds between successive invocations:

Fixed Delay Task: Start Time - 14:02:28
Fixed Delay Task: End Time - 14:02:31
Fixed Delay Task: Start Time - 14:02:33
Fixed Delay Task: End Time - 14:02:36
Fixed Delay Task: Start Time - 14:02:38
Fixed Delay Task: End Time - 14:02:41
Fixed Delay Task: Start Time - 14:02:43
Fixed Delay Task: End Time - 14:02:46
...

This option is best suited when the previous execution of the task must be completed before executing it again. The fixedDelay property ensures that there is always a delay of n milliseconds between consecutive invocations of a task. For dependent tasks, it is pretty helpful.

Schedule Task with Initial Delay

You can also specify the initial time to wait (in milliseconds) before the first execution of the task begins by using the initialDelay property. It works with both fixedRate and fixedDelay properties.

In the following example, the task is executed the first time after a wait of 5 seconds, and then it executes normally after every 2 seconds:

@Scheduled(fixedRate = 2000, initialDelay = 5000)
public void scheduleTaskWithFixedRateAndInitialDelay() {
    logger.info("Fixed Rate Task with Initial Delay: Current Time - {}", formatter.format(LocalDateTime.now()));
}

The initialDelay property delays the first execution of the task for the specified milliseconds. After the first execution, the job starts executing normally:

# Server started at 14:42:20
Fixed Rate Task with Initial Delay: Current Time - 14:42:25
Fixed Rate Task with Initial Delay: Current Time - 14:42:27
Fixed Rate Task with Initial Delay: Current Time - 14:42:29
Fixed Rate Task with Initial Delay: Current Time - 14:42:31
...

The initialDelay property helps delay the first execution of the task until the data required to execute the task is supplied by some other services.

Schedule Task using Cron Expression

Sometimes the fixed rate and fixed delay are not enough to fulfill our needs. We want more flexibility to control the schedule of our tasks. For example, we might need to send a newsletter every Thursday or back up the database weekly. This is something that cannot be done with the above properties.

That is where the cron expressions come in handy. They provide complete flexibility to schedule the tasks, whatever way you want to choose.

Here is an example task that uses cron expression to execute every minute:

@Scheduled(cron = "0 * * * * ?")
public void scheduleTaskWithCronExpression() {
    logger.info("Cron Task: Current Time - {}", formatter.format(LocalDateTime.now()));
}
Cron Task: Current Time - 15:17:00
Cron Task: Current Time - 15:18:00
Cron Task: Current Time - 15:19:00
Cron Task: Current Time - 15:20:00
...

Let us have a few more examples of cron expressions. The following tasks are scheduled to be executed at 4:45 AM on the 10th day of every month:

@Scheduled(cron = "0 45 4 10 * ?")
public void scheduleTaskWithCronExpression2() {
    logger.info("Cron Task: Current Time - {}", formatter.format(LocalDateTime.now()));
}

Cron expressions are somehow complex to write and understand. crontab.guru is a nice little tool that makes it easy to generate cron schedule expressions:

# At 12:00 on Sunday
0 0 12 * * Sun

# At 14:15 in every 2nd month
0 15 14 * */2 *

# At 08:00 on every day of the week from Monday through Friday
0 0 8 * * Mon-Fri

# At 12:30 on every 15th day of the month if it's on Wednesday
0 30 12 */15 * Wed

Parameterizing the Schedule

We have hard-coded the time intervals and cron expressions in the above examples. Now, if you want to change the execution time of any task, we have to recompile and redeploy the entire application. This is certainly not flexible.

Fortunately, we can make use of Spring Expression Language (SpPL) and store the configuration of the tasks in a properties file:

@Scheduled(fixedRateString = "${fixed-rate.in.milliseconds}")
public void scheduleDynamicTaskWithFixedRate() {
    logger.info("Fixed Rate Dynamic Task: Current Time - {}", formatter.format(LocalDateTime.now()));
}

@Scheduled(fixedDelayString = "${fixed-delay.in.milliseconds}")
public void scheduleDynamicTaskWithFixedDelay() {
    logger.info("Fixed Delay Dynamic Task: Current Time - {}", formatter.format(LocalDateTime.now()));
}

@Scheduled(cron = "${cron.expression}")
public void scheduleDynamicTaskWithCronExpression() {
    logger.info("Cron Dynamic Task: Current Time - {}", formatter.format(LocalDateTime.now()));
}

And our application.properties file will look like this:

application.properties

fixed-rate.in.milliseconds=5000
fixed-delay.in.milliseconds=4000
cron.expression=0 15 5 * * FRI

Custom Thread Pool Configuration

By default, the @EnableScheduling annotation creates a thread pool with only one thread. The invocation of all @Scheduled tasks is queued and executed by only thread. So if you have multiple scheduled tasks in your application, you might see weird invocation behavior (as the tasks are queued).

But the good thing is you can create your own custom thread pool with multiple threads and configure the application to use that for executing all scheduled tasks:

SchedulerConfig.java

package com.attacomsian.scheduling;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

public class SchedulerConfig implements SchedulingConfigurer {

    @Value("${thread.pool.size}")
    private int POOL_SIZE;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();

        scheduler.setPoolSize(POOL_SIZE);
        scheduler.setThreadNamePrefix("my-scheduled-task-pool-");
        scheduler.initialize();

        taskRegistrar.setTaskScheduler(scheduler);
    }
}

Add the following property to your application.properties file:

thread.pool.size=10

Spring will now create a custom thread pool with 10 threads to execute the tasks. You can also find the thread name in the log that was invoked to execute the task:

[ My-Scheduler-4] : Fixed Delay Dynamic Task: Current Time - 17:20:03
[ My-Scheduler-8] : Fixed Rate Dynamic Task: Current Time - 17:20:04
[ My-Scheduler-1] : Fixed Delay Dynamic Task: Current Time - 17:20:07
[ My-Scheduler-7] : Fixed Rate Dynamic Task: Current Time - 17:20:09
[ My-Scheduler-2] : Fixed Delay Dynamic Task: Current Time - 17:20:11
...

Source code: Download the complete source code from GitHub available under MIT license.

Conclusion

That's all for task scheduling in Spring Boot. We have learned how to schedule tasks in Spring Boot using the @Scheduled annotation. We also learned to create and use a custom thread pool for executing these tasks.

Task scheduling is much more valuable to automate complex tasks that would have taken a lot of time to do manually. It saves time that could be spent on other productive things instead of worrying about routine tasks.

✌️ Like this article? Follow me on Twitter and LinkedIn. You can also subscribe to RSS Feed.