7 марта 2026
Тэги: gradle, Java, Spring, Spring Boot, yaml, многопоточность.
Часто в приложениях возникает необходимость выполнять некоторые действия по расписанию, а не по запросу извне. Если вы пишете приложение на Spring, то можете реализовать похожий функционал, добавив всего пару аннотаций.
В качестве примера давайте создадим простой проект с помощью Spring Initializr. Мы выберем в качестве языка Java 25, в качестве сборщика - Gradle, Spring последней стабильной версии и конфигурацию в формате yaml. В зависимости проекта добавим Spring Web, который содержит функционал шедулинга. Также добавим Lombok, чтобы меньше писать шаблонного кода.
Предположим, у нас в проекте есть сервис, выполняющий некоторую «тяжёлую» задачу. В демонстрационных целях мы будем имитировать такую задачу задержкой в три секунды с помощью метода TimeUnit.SECONDS.sleep().
Вызов метода doWork() занимает заметное время, поэтому его логичнее запускать «в фоне» по определённому расписанию. Для наглядности метод принимает имя шедулера. Также мы логируем начало и конец работы с указанием этого имени.
Аннотация @Slf4j относится к Lombok и позволяет нам не определять логгер в явном виде, а сразу воспользоваться объектом log.
Теперь создадим сам шедулер:
Он объявляется как спринговый @Component и в него в качестве зависимости добавим наш WorkService. Аннотация @RequiredArgsConstructor из Lombok автоматически сгенерирует конструктор с параметрами на основе private final полей, которые мы добавляем в данном классе.
Далее создадим метод simpleScheduler(), а внутри него просто будем вызывать сервисный метод doWork() с указанием имени этого шедулера. И повесим на метод аннотацию @Scheduled. Благодаря этой аннотации метод simpleScheduler() будет вызван первый раз спустя initialDelay после старта приложения, и далее каждый раз с паузой fixedDelay после окончания предыдущего вызова. По умолчанию значения initialDelay и fixedDelay указываются в миллисекундах, т.е. в данном примере метод будет вызван через 3 секунды после старта приложения и далее с интервалом в 5 секунд.
Наконец, чтобы активировать механизм шедулинга в нашем приложении, надо повесить на main-класс аннотацию @EnableScheduling:
Это самый минимальный способ реализовать запуск задач по расписанию.
Обратите внимание, что вся бизнес-логика инкапсулирована внутри метода doWork() сервисного слоя, а шедулер просто вызывает его. В шедулере не должно быть никакой сложной логики. Такое разделение позволяет переиспользовать метод doWork() в других местах приложения (например, в rest-контроллере).
Давайте посмотрим на simpleScheduler() и подумаем, как его можно улучшить? Во-первых, значения всех интервалов мы захардкодили, а хочется вынести их в параметры конфигурации приложения. Во-вторых, значения интервалов хочется указывать не только в миллисекундах, а в других единицах. И в-третьих, хочется включать и отключать шедулер с помощью какого-нибудь флага.
Для внедрения этих улучшений сперва добавим все параметры в конфигурацию приложения, т.е. в application.yaml:
Здесь мы группируем настройки шедулера общим префиксом my-scheduler, флаг включения/выключения называем enabled, а также добавляем значения обоих интервалов в секундах.
Для удобства заведём отдельный бин, в котором повторим структуру этого конфига:
Это обычный record-класс, имена и типы его полей соответствуют параметрам конфига, а общий префикс мы указываем в аннотации @ConfigurationProperties. Чтобы все подобные бины автоматически инициализировались при старте приложения, надо добавить ещё одну аннотацию к main-классу:
@ConfigurationPropertiesScan позволяет автоматически сканировать все бины с конфигами в текущем пакете и во всех дочерних.
Теперь мы готовы к тому, чтобы добавить второй шедулер в WorkScheduler, который будет использовать параметры конфига:
На метод parametrizedScheduler() мы также повесим аннотацию @Scheduled, только вместо initialDelay и fixedDelay будем использовать параметры initialDelayString и fixedDelayString. Они позволяют указывать любые значения в виде строки, а это, в свою очередь, позволяет использовать стандартный механизм подгрузки параметров Spring. В самой строке мы указываем знак доллара и в фигурных скобках пишем полное имя параметра (т.е. с префиксом my-scheduler). Также в параметре timeUnit мы указываем, что интервалы хотим определять в секундах.
Внутри метода мы делаем простую проверку флага enabled из schedulerConfig и пишем предупреждение в лог, если шедулер выключен. Таким образом, для изменения периода срабатывания этого шедулера или его отключения потребуется внести правки только в application.yaml.
Теперь у нас имеется два шедулера, которые вызывают один и тот же метод doWork() и которые имеют одинаковые интервалы срабатывания. Мы ожидаем, что оба шедулера будут срабатывать одновременно. Однако если запустим приложение, то увидим, что шедулеры срабатывают последовательно друг за другом:
Давайте добавим в наше приложение асинхронность. Тогда каждый шедулер будет работать в своём собственном потоке и это повысит общую производительность приложения.
Сначала повесим аннотацию @Async на бизнесовый метод doWork():
Затем включим механизм асинхронного выполнения, добавив к main-классу ещё одну аннотацию @EnableAsync:
Теперь запустим приложение и в логах увидим, что оба шедулера запускаются одновременно:
Как быть, если задачу требуется запускать не с фиксированными интервалами, а в определённое время дня? Например, каждый день в 9 утра. Обычно для этих целей используют cron-выражения. В unix-системах это набор из пяти значений, которые последовательно слева направо позволяют задать определённую минуту, час, день месяца, месяц и день недели. Вместо конкретного значения можно указывать звёздочку, тогда это означает «каждый час» или «каждую минуту». Также можно указывать несколько конкретных значений или временные интервалы.
В Spring также можно использовать cron-выражения, только вместо 5 значений указываются 6, т.к. есть возможность указывать ещё и секунды. Например, «каждый день в 9 утра» для Spring будет выглядеть вот так: 0 0 9 * * *.
Добавим третий шедулер, который будет использовать cron-выражение. Для этого будем использовать параметр cron. Он является более универсальным, поэтому заменяет собой все остальные параметры вроде initialDelay, fixedDelay и timeUnit.
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, то просто ничего не делать. То есть в выключенном состоянии шедулер будет работать вхолостую.