Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Задачи по расписанию

Некоторые дела приложение должно делать само, без запроса: каждое утро считать новые статьи, раз в час чистить старые данные, ночью отправлять письма. 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 * * *009любойлюбойлюбой
*/10 * * * * *каждые 10любыелюбыелюбойлюбойлюбой
0 0 3 * * MON003любойлюбойпонедельник

Первое выражение — каждый день в 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.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.