Running on Time With Spring’s Scheduled Tasks
This lesson in scheduled tasks takes a look at your options in Spring for either fixed rate scheduling or setting up cron expressions to help you out.
Join the DZone community and get the full member experience.
Join For FreeDo you need to run a process every day at the exact same time like an alarm? Then Spring’s scheduled tasks are for you. Allowing you to annotate a method with @Scheduled
causes it to run at the specific time or interval that is denoted inside it. In this post, we will look at setting up a project that can use scheduled tasks as well as how to use the different methods for defining when they execute.
I will be using Spring Boot for this post, making the dependencies nice and simple due to scheduling being available to the spring-boot-starter
dependency that will be included in pretty much every Spring Boot project in some way. This allows you to use any of the other starter dependencies, as they will pull in spring-boot-starter
and all its relationships. If you want to include the exact dependency itself, use spring-context
.
You could use spring-boot-starter
.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.0.0.RC1</version>
</dependency>
Or use spring-context
directly.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.3.RELEASE</version>
</dependency>
Creating a scheduled task is pretty straightforward. Add the @Scheduled
annotation to any method that you wish to run automatically and include @EnableScheduling
in a configuration file.
So, for example, you could have something like:
@Component
public class EventCreator {
private static final Logger LOG = LoggerFactory.getLogger(EventCreator.class);
private final EventRepository eventRepository;
public EventCreator(final EventRepository eventRepository) {
this.eventRepository = eventRepository;
}
@Scheduled(fixedRate = 1000)
public void create() {
final LocalDateTime start = LocalDateTime.now();
eventRepository.save(
new Event(new EventKey("An event type", start, UUID.randomUUID()), Math.random() * 1000));
LOG.debug("Event created!");
}
}
There is quite a lot of code here that has no importance to running a scheduled task. As I said a minute ago, we need to use @Scheduled
on a method, and it will start running automatically. So in the above example, the create
method will start running every 1000ms (1 second) as denoted by the fixedRate
property of the annotation. If we wanted to change how often it ran, we could increase or decrease the fixedRate
time, or we could consider using the different scheduling methods available to us.
So you probably want to know what these other ways are, right? Well, here they are (I will include fixedRate
here as well):
fixedRate
executes the method with a fixed period of milliseconds between invocations.fixedRateString
is the same asfixedRate
but with a string value instead.fixedDelay
executes the method with a fixed period of milliseconds between the end of one invocation and the start of the next.fixedDelayString
is the same asfixedDelay
but with a string value instead.cron
uses cron-like expressions to determine when to execute the method (we will look at this more in depth later).
There are a few other utility properties available to the @Scheduled
annotation.
zone
indicates the time zone that the cron expression will be resolved for. If no time zone is included, it will use the server’s default time zone. So if you needed it to run for a specific time zone, say Hong Kong, you could usezone = "GMT+8:00"
.initialDelay
is the number of milliseconds to delay the first execution of a scheduled task. It requires one of the fixed rate or fixed delay properties to be used.initialDelayString
is the same asinitialDelay
but with a string value instead.
A few examples of using fixed rates and delays can be found below:
Same as earlier — runs every 1 second:
@Scheduled(fixedRate = 1000)
Same as above:
@Scheduled(fixedRateString = "1000")
Runs 1 second after the previous invocation finished:
@Scheduled(fixedDelay = 1000)
Runs every second but waits 5 seconds before it executes for the first time:
@Scheduled(fixedRate = 1000, initialDelay = 5000)
Now onto looking at the cron
property, which gives much more control over the scheduling of a task. It lets us define the seconds, minutes ,and hours the task runs at but can go even further and specify even the years that a task will run in.
Below is a breakdown of the components that build a cron expression.
Seconds
can have values0-59
or the special characters, - * /
.Minutes
can have values0-59
or the special characters, - * /
.Hours
can have values0-59
or the special characters, - * /
.Day of month
can have values1-31
or the special characters, - * ? / L W C
.Month
can have values1-12
,JAN-DEC
or the special characters, - * /
.Day of week
can have values1-7
,SUN-SAT
or the special characters, - * ? / L C #
.Year
can be empty, have values1970-2099
or the special characters, - * /
.
Just for some extra clarity, I have combined the breakdown into an expression consisting of the field labels.
@Scheduled(cron = "[Seconds] [Minutes] [Hours] [Day of month] [Month] [Day of week] [Year]")
Please do not include the braces in your expressions (I used them to make the expression clearer).
Before we can move on, we need to go through what the special characters mean.
*
represents all values. So, if it is used in the second field, it means every second. If it is used in the day field, it means run every day.?
represents no specific value and can be used in either the day of month or day of week field — where using one invalidates the other. If we specify it to trigger on the 15th day of a month, then a?
will be used in theDay of week
field.-
represents an inclusive range of values. For example, 1-3 in the hours field means the hours 1, 2, and 3.,
represents additional values. For example, putting MON,WED,SUN in the day of week field means on Monday, Wednesday, and Sunday./
represents increments. For example 0/15 in the seconds field triggers every 15 seconds starting from 0 (0, 15, 3,0 and 45).L
represents the last day of the week or month. Remember that Saturday is the end of the week in this context, so usingL
in the day of week field will trigger on a Saturday. This can be used in conjunction with a number in the day of month field, such as6L
to represent the last Friday of the month or an expression likeL-3
denoting the third from the last day of the month. If we specify a value in the day of week field, we must use?
in the day of month field, and vice versa.W
represents the nearest weekday of the month. For example,15W
will trigger on the 15th day of the month if it is a weekday. Otherwise, it will run on the closest weekday. This value cannot be used in a list of day values.#
specifies both the day of the week and the week that the task should trigger. For example,5#2
means the second Thursday of the month. If the day and week you specified overflows into the next month, then it will not trigger.
A helpful resource with slightly longer explanations can be found here, which helped me write this post.
Let's go through a few examples:
Fires at 12 PM every day:
@Scheduled(cron = "0 0 12 * * ?")
Fires at 10:15 AM every day in the year 2005:
@Scheduled(cron = "0 15 10 * * ? 2005")
Fires every 20 seconds:
@Scheduled(cron = "0/20 * * * * ?")
For some more examples, see the link I mentioned earlier, shown again here. Luckily, if you get stuck when writing a simple cron expression, you should be able to Google the scenario that you need — as someone has probably asked the same question on Stack Overflow already.
To tie some of the above lessons into a little code example, see the code below:
@Component
public class AverageMonitor {
private static final Logger LOG = LoggerFactory.getLogger(AverageMonitor.class);
private final EventRepository eventRepository;
private final AverageRepository averageRepository;
public AverageMonitor(
final EventRepository eventRepository, final AverageRepository averageRepository) {
this.eventRepository = eventRepository;
this.averageRepository = averageRepository;
}
@Scheduled(cron = "0/20 * * * * ?")
public void publish() {
final double average =
eventRepository.getAverageValueGreaterThanStartTime(
"An event type", LocalDateTime.now().minusSeconds(20));
averageRepository.save(
new Average(new AverageKey("An event type", LocalDateTime.now()), average));
LOG.info("Average value is {}", average);
}
}
Here, we have a class that is querying Cassandra every 20 seconds for the average value of events in the same time period. Again, most of the code here is noise from the @Scheduled
annotation, but it can be helpful to see it in the wild. Furthermore, if you have been observant, for this use-case of running every 20 seconds, using the fixedRate
and possibly the fixedDelay
properties instead of cron
would be suitable here as we are running the task so frequently:
@Scheduled(fixedRate = 20000)
Is the fixedRate
equivalent of the cron expression used above.
The final requirement, which I alluded to earlier, is to add the @EnableScheduling
annotation to a configuration class:
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(final String args[]) {
SpringApplication.run(Application.class);
}
}
Being that this is a small Spring Boot application, I have attached the @EnableScheduling
annotation to the main @SpringBootApplication
class.
In conclusion, we can schedule tasks to trigger using the @Scheduled
annotation along with either a millisecond rate between executions or a cron expression for finer timings that cannot be expressed with the former. For tasks that need to run very often, using the fixedRate
or fixedDelay
properties will suffice, but once the time between executions becomes larger, it will become harder to quickly determine the defined time. When this occurs, the cron
property should be used for better clarity of the scheduled timing.
The little amount of code used in this post can be found on my GitHub.
If you found this post helpful and wish to keep up to date with my new tutorials as I write them, follow me on Twitter at @LankyDanDev.
Published at DZone with permission of Dan Newton, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments