Статьи Утилиты Telegram YouTube VK Видео RuTube Отзывы

Запуск задач по расписанию в Spring Boot

Исходники

7 марта 2026

Тэги: gradle, Java, Spring, Spring Boot, yaml, многопоточность.

Содержание

  1. Простой пример шедулера
  2. Выносим настройки шедулера в конфиг
  3. Добавляем асинхронность
  4. Запуск задач в определённое время
  5. Выводы

Часто в приложениях возникает необходимость выполнять некоторые действия по расписанию, а не по запросу извне. Если вы пишете приложение на Spring, то можете реализовать похожий функционал, добавив всего пару аннотаций.

Простой пример шедулера

В качестве примера давайте создадим простой проект с помощью Spring Initializr. Мы выберем в качестве языка Java 25, в качестве сборщика - Gradle, Spring последней стабильной версии и конфигурацию в формате yaml. В зависимости проекта добавим Spring Web, который содержит функционал шедулинга. Также добавим Lombok, чтобы меньше писать шаблонного кода.

Создание заготовки проекта шедулера

Предположим, у нас в проекте есть сервис, выполняющий некоторую «тяжёлую» задачу. В демонстрационных целях мы будем имитировать такую задачу задержкой в три секунды с помощью метода TimeUnit.SECONDS.sleep().

@Slf4j
@Service
public class WorkService {
    public void doWork(String schedulerName) throws InterruptedException {
        log.info("Start work for {}", schedulerName);
        TimeUnit.SECONDS.sleep(3);
        log.info("Work completed for {}", schedulerName);
    }
}

Вызов метода doWork() занимает заметное время, поэтому его логичнее запускать «в фоне» по определённому расписанию. Для наглядности метод принимает имя шедулера. Также мы логируем начало и конец работы с указанием этого имени.

Аннотация @Slf4j относится к Lombok и позволяет нам не определять логгер в явном виде, а сразу воспользоваться объектом log.

Теперь создадим сам шедулер:

@Slf4j
@Component
@RequiredArgsConstructor
public class WorkScheduler {
    private final WorkService workService;

    @Scheduled(initialDelay = 3000, fixedDelay = 5000)
    public void simpleScheduler() throws InterruptedException {
        workService.doWork("simpleScheduler");
    }
}

Он объявляется как спринговый @Component и в него в качестве зависимости добавим наш WorkService. Аннотация @RequiredArgsConstructor из Lombok автоматически сгенерирует конструктор с параметрами на основе private final полей, которые мы добавляем в данном классе.

Далее создадим метод simpleScheduler(), а внутри него просто будем вызывать сервисный метод doWork() с указанием имени этого шедулера. И повесим на метод аннотацию @Scheduled. Благодаря этой аннотации метод simpleScheduler() будет вызван первый раз спустя initialDelay после старта приложения, и далее каждый раз с паузой fixedDelay после окончания предыдущего вызова. По умолчанию значения initialDelay и fixedDelay указываются в миллисекундах, т.е. в данном примере метод будет вызван через 3 секунды после старта приложения и далее с интервалом в 5 секунд.

Наконец, чтобы активировать механизм шедулинга в нашем приложении, надо повесить на main-класс аннотацию @EnableScheduling:

@EnableScheduling // активируем шедулинг
@SpringBootApplication
public class SpringSchedulerApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringSchedulerApplication.class, args);
    }
}

Это самый минимальный способ реализовать запуск задач по расписанию.

Обратите внимание, что вся бизнес-логика инкапсулирована внутри метода doWork() сервисного слоя, а шедулер просто вызывает его. В шедулере не должно быть никакой сложной логики. Такое разделение позволяет переиспользовать метод doWork() в других местах приложения (например, в rest-контроллере).

Выносим настройки шедулера в конфиг

Давайте посмотрим на simpleScheduler() и подумаем, как его можно улучшить? Во-первых, значения всех интервалов мы захардкодили, а хочется вынести их в параметры конфигурации приложения. Во-вторых, значения интервалов хочется указывать не только в миллисекундах, а в других единицах. И в-третьих, хочется включать и отключать шедулер с помощью какого-нибудь флага.

Для внедрения этих улучшений сперва добавим все параметры в конфигурацию приложения, т.е. в application.yaml:

my-scheduler:
  enabled: true
  initial-delay-seconds: 3
  fixed-delay-seconds: 5

Здесь мы группируем настройки шедулера общим префиксом my-scheduler, флаг включения/выключения называем enabled, а также добавляем значения обоих интервалов в секундах.

Для удобства заведём отдельный бин, в котором повторим структуру этого конфига:

@ConfigurationProperties(prefix = "my-scheduler")
public record SchedulerConfig(
        boolean enabled,
        int initialDelaySeconds,
        int fixedDelaySeconds
) {
}

Это обычный record-класс, имена и типы его полей соответствуют параметрам конфига, а общий префикс мы указываем в аннотации @ConfigurationProperties. Чтобы все подобные бины автоматически инициализировались при старте приложения, надо добавить ещё одну аннотацию к main-классу:

@EnableScheduling
@SpringBootApplication
@ConfigurationPropertiesScan // сканируем все ConfigurationProperties
public class SpringSchedulerApplication {
    // main-метод
}

@ConfigurationPropertiesScan позволяет автоматически сканировать все бины с конфигами в текущем пакете и во всех дочерних.

Теперь мы готовы к тому, чтобы добавить второй шедулер в WorkScheduler, который будет использовать параметры конфига:

public class WorkScheduler {
    private final WorkService workService;
    private final SchedulerConfig schedulerConfig; // подгружаем конфиг

    // simpleScheduler() { ... }

    @Scheduled(
            initialDelayString = "${my-scheduler.initial-delay-seconds}",
            fixedDelayString = "${my-scheduler.fixed-delay-seconds}",
            timeUnit = TimeUnit.SECONDS
    )
    public void parametrizedScheduler() throws InterruptedException {
        if (schedulerConfig.enabled()) {
            workService.doWork("parametrizedScheduler");
        } else {
            log.warn("Scheduler disabled. Do nothing");
        }
    }
}

На метод parametrizedScheduler() мы также повесим аннотацию @Scheduled, только вместо initialDelay и fixedDelay будем использовать параметры initialDelayString и fixedDelayString. Они позволяют указывать любые значения в виде строки, а это, в свою очередь, позволяет использовать стандартный механизм подгрузки параметров Spring. В самой строке мы указываем знак доллара и в фигурных скобках пишем полное имя параметра (т.е. с префиксом my-scheduler). Также в параметре timeUnit мы указываем, что интервалы хотим определять в секундах.

Внутри метода мы делаем простую проверку флага enabled из schedulerConfig и пишем предупреждение в лог, если шедулер выключен. Таким образом, для изменения периода срабатывания этого шедулера или его отключения потребуется внести правки только в application.yaml.

Добавляем асинхронность

Теперь у нас имеется два шедулера, которые вызывают один и тот же метод doWork() и которые имеют одинаковые интервалы срабатывания. Мы ожидаем, что оба шедулера будут срабатывать одновременно. Однако если запустим приложение, то увидим, что шедулеры срабатывают последовательно друг за другом:

Start work for simpleScheduler
Work completed for simpleScheduler
Start work for parametrizedScheduler
Work completed for parametrizedScheduler
Start work for simpleScheduler
...

Давайте добавим в наше приложение асинхронность. Тогда каждый шедулер будет работать в своём собственном потоке и это повысит общую производительность приложения.

Сначала повесим аннотацию @Async на бизнесовый метод doWork():

public class WorkService {
    @Async
    public void doWork() throws InterruptedException {
        // ...
    }
}

Затем включим механизм асинхронного выполнения, добавив к main-классу ещё одну аннотацию @EnableAsync:

@EnableAsync // включаем механизм асинхронного выполнения
@EnableScheduling
@SpringBootApplication
@ConfigurationPropertiesScan
public class SpringSchedulerApplication {
    // main-метод
}

Теперь запустим приложение и в логах увидим, что оба шедулера запускаются одновременно:

Start work for parametrizedScheduler
Start work for simpleScheduler
Work completed for simpleScheduler
Work completed for parametrizedScheduler
Start work for simpleScheduler
Start work for parametrizedScheduler
Work completed for simpleScheduler
Work completed for parametrizedScheduler
...

Запуск задач в определённое время

Как быть, если задачу требуется запускать не с фиксированными интервалами, а в определённое время дня? Например, каждый день в 9 утра. Обычно для этих целей используют cron-выражения. В unix-системах это набор из пяти значений, которые последовательно слева направо позволяют задать определённую минуту, час, день месяца, месяц и день недели. Вместо конкретного значения можно указывать звёздочку, тогда это означает «каждый час» или «каждую минуту». Также можно указывать несколько конкретных значений или временные интервалы.

В Spring также можно использовать cron-выражения, только вместо 5 значений указываются 6, т.к. есть возможность указывать ещё и секунды. Например, «каждый день в 9 утра» для Spring будет выглядеть вот так: 0 0 9 * * *.

Добавим третий шедулер, который будет использовать cron-выражение. Для этого будем использовать параметр cron. Он является более универсальным, поэтому заменяет собой все остальные параметры вроде initialDelay, fixedDelay и timeUnit.

@Scheduled(cron = "0 * * * * *")
public void cronScheduler() throws InterruptedException {
    workService.doWork("cronScheduler");
}

Cron-выражения - это очень гибкий механизм, но если вы не unix-администратор, то сходу в них разобраться бывает сложно. Для простых случаев можно генерить cron-выражения с помощью моего Генератора cron-выражений.

Выводы

Spring предоставляет простой декларативный способ для запуска фоновых задач с помощью аннотации @Scheduled. При этом рекомендуется делать шедулер отключаемым, а все параметры выносить в конфиг с общим префиксом. Если же требуется запускать шедулер в определённое время или гибко настраивать время запуска - используйте cron-выражения.

Бизнес-логику, которую планируете запускать шедулером, выделяйте в метод сервисного слоя и помечайте его аннотацией @Async, иначе все шедулеры будут срабатывать по очереди в один поток, что может замедлить обработку данных.


См. также

Облако тэгов

Kotlin, Java, Spring, Spring Boot, Spring Data, Spring AI, SQL, PostgreSQL, Oracle, H2, Linux, Hibernate, Collections, Stream API, многопоточность, чат-боты, нейросети, файлы, devops, Docker, Nginx, Apache, maven, gradle, JUnit, YouTube, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.

Последние статьи


Комментарии

24.05.2022 20:59 Александр

Подскажите актуальный способ отключения планировщика и возобновления задания по требованию.

24.05.2022 23:35 devmark

Наиболее простое решение - это хранить специальный признак в базе (можно отдельную таблицу завести), который считывать при каждом срабатывании аннотации Scheduled и проверять это значение. Если значение флага равно false, то просто ничего не делать. То есть в выключенном состоянии шедулер будет работать вхолостую.

Добавить комментарий

×

devmark.ru