Задачи по расписанию
Некоторые дела приложение должно делать само, без запроса: каждое утро считать новые статьи, раз в час чистить старые данные, ночью отправлять письма. Spring умеет вызывать метод бина по расписанию. Нужны две аннотации: одна включает планировщик, вторая задаёт, когда вызывать метод.
Включение
// src/main/java/com/example/blog/AppConfig.java
@Configuration
@EnableCaching
@EnableScheduling
public class AppConfig {
@Bean
public Clock clock() {
return Clock.systemUTC();
}
}@EnableScheduling включает планировщик. Ставим его в AppConfig, а не на главный класс, — почему, объяснено в уроке о @DataJpaTest. @EnableCaching понадобится в следующем уроке.
Задача
// 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("Статей за последние 24 часа: {}", count);
}
}Метод считает статьи за сутки запросом countByCreatedAtAfter (добавьте его в PostRepository) и пишет итог в журнал. Метод с @Scheduled ничего не принимает и ничего не возвращает. Расписание берётся из настройки:
# src/main/resources/application.yaml
blog:
digest-cron: "0 0 9 * * *"Выражение cron
В Spring выражение cron состоит из шести полей:
| Поле | Секунды | Минуты | Часы | День месяца | Месяц | День недели |
|---|---|---|---|---|---|---|
0 0 9 * * * | 0 | 0 | 9 | любой | любой | любой |
*/10 * * * * * | каждые 10 | любые | любые | любой | любой | любой |
0 0 3 * * MON | 0 | 0 | 3 | любой | любой | понедельник |
Первое выражение — каждый день в 9:00, второе — каждые 10 секунд. Время берётся по часовому поясу сервера; другой пояс задаёт параметр zone = "Europe/Moscow". Кавычки в YAML нужны из-за звёздочек.
Проверка
Ждать до девяти утра не обязательно — передайте частое расписание при запуске:
./mvnw spring-boot:run -Dspring-boot.run.arguments="--blog.digest-cron=*/10 * * * * *"INFO ... [ scheduling-1] com.example.blog.DigestJob : Статей за последние 24 часа: 1
INFO ... [ scheduling-1] com.example.blog.DigestJob : Статей за последние 24 часа: 2Задача сработала дважды с разницей в 10 секунд; между запусками была создана статья. Имя потока scheduling-1 показывает, что метод выполняется не в потоке запроса, а в потоке планировщика.
Без cron
| Параметр | Смысл |
|---|---|
fixedRate = 60000 | Каждые 60 секунд от начала прошлого запуска |
fixedDelay = 60000 | Через 60 секунд после окончания прошлого запуска |
initialDelay = 5000 | Первый запуск — через 5 секунд после старта |
Есть и параметр timeUnit: @Scheduled(fixedRate = 1, timeUnit = TimeUnit.HOURS) — раз в час.
Частая ошибка
Если забыть @EnableScheduling, методы с @Scheduled просто никогда не вызываются — ни ошибки, ни предупреждения. Если задача молчит, проверьте аннотацию и то, что класс задачи — бин (@Component). Учтите и другое: при запуске приложения на двух серверах задача сработает на обоих. Для таких случаев используют блокировку через базу, например библиотеку ShedLock.