Scheduled tasks
Some jobs an application should do on its own, without a request: count new posts every morning, clean old data every hour, send emails at night. Spring can call a bean method on a schedule. Two annotations are needed: one switches the scheduler on, the other says when to call the method.
Switching it on
// src/main/java/com/example/blog/AppConfig.java
@Configuration
@EnableCaching
@EnableScheduling
public class AppConfig {
@Bean
public Clock clock() {
return Clock.systemUTC();
}
}@EnableScheduling turns the scheduler on. We put it in AppConfig rather than on the main class — the @DataJpaTest lesson explains why. @EnableCaching is needed in the next lesson.
The task
// src/main/java/com/example/blog/DigestJob.java
package com.example.blog;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class DigestJob {
private static final Logger log = LoggerFactory.getLogger(DigestJob.class);
private final PostRepository posts;
private final Clock clock;
public DigestJob(PostRepository posts, Clock clock) {
this.posts = posts;
this.clock = clock;
}
@Scheduled(cron = "${blog.digest-cron}")
public void logDailyDigest() {
Instant dayAgo = Instant.now(clock).minus(Duration.ofDays(1));
long count = posts.countByCreatedAtAfter(dayAgo);
log.info("Posts in the last 24 hours: {}", count);
}
}The method counts the posts of the last day with the countByCreatedAtAfter query (add it to PostRepository) and writes the result to the log. A @Scheduled method takes nothing and returns nothing. The schedule comes from a setting:
# src/main/resources/application.yaml
blog:
digest-cron: "0 0 9 * * *"A cron expression
In Spring a cron expression has six fields:
| Field | Seconds | Minutes | Hours | Day of month | Month | Day of week |
|---|---|---|---|---|---|---|
0 0 9 * * * | 0 | 0 | 9 | any | any | any |
*/10 * * * * * | every 10 | any | any | any | any | any |
0 0 3 * * MON | 0 | 0 | 3 | any | any | Monday |
The first expression is every day at 9:00, the second every 10 seconds. Time follows the server's time zone; another zone is set with the zone = "Europe/Berlin" parameter. The quotes in YAML are needed because of the asterisks.
Checking it
No need to wait until nine in the morning — pass a frequent schedule at startup:
./mvnw spring-boot:run -Dspring-boot.run.arguments="--blog.digest-cron=*/10 * * * * *"INFO ... [ scheduling-1] com.example.blog.DigestJob : Posts in the last 24 hours: 1
INFO ... [ scheduling-1] com.example.blog.DigestJob : Posts in the last 24 hours: 2The task ran twice, 10 seconds apart; a post was created in between. The thread name scheduling-1 shows the method runs not in a request thread but in the scheduler's thread.
Without cron
| Parameter | Meaning |
|---|---|
fixedRate = 60000 | Every 60 seconds from the start of the previous run |
fixedDelay = 60000 | 60 seconds after the previous run finished |
initialDelay = 5000 | The first run 5 seconds after startup |
There is also a timeUnit parameter: @Scheduled(fixedRate = 1, timeUnit = TimeUnit.HOURS) — once an hour.
Common mistake
If you forget @EnableScheduling, @Scheduled methods are simply never called — no error, no warning. If a task stays silent, check the annotation and that the task class is a bean (@Component). Keep another thing in mind: when the application runs on two servers, the task fires on both. Such cases use a lock in the database, for example the ShedLock library.