Kora облачно ориентированный серверный фреймворк написанный на Java для написания Java / Kotlin приложений с упором на производительность, эффективность, прозрачность сделанный выходцами из Т-Банк / Тинькофф

Kora is a cloud-oriented server-side Java framework for writing Java / Kotlin applications with a focus on performance, efficiency and transparency

Перейти к содержанию

Планировщик

Модуль планирования Kora позволяет запускать методы приложения по расписанию в декларативном стиле через аннотации. Во время компиляции Kora генерирует компоненты задач и связывает их с выбранным механизмом планирования.

Доступны два варианта: собственный планировщик на основе ScheduledExecutorService из JDK и планировщик на основе Quartz. Собственный вариант подходит для простых периодических задач внутри одного приложения, а Quartz полезен для cron-выражений, пользовательских экземпляров Trigger и дополнительных правил выполнения задач.

Собственный планировщик

Собственный планировщик использует стандартный ScheduledExecutorService, который поставляется вместе с JDK.

Для создания задач через аспекты используются специальные аннотации, соответствующие методам ScheduledExecutorService. Параметры аннотаций совпадают с параметрами методов scheduleAtFixedRate, scheduleWithFixedDelay и schedule.

У всех аннотаций есть параметр config. Если он указан, значения параметров берутся из конфигурации по этому пути и имеют приоритет над значениями из аннотации. Конфигурация конкретной задачи также может содержать секцию telemetry, значения которой переопределяют общую телеметрию планировщика для этой задачи.

Методы, выполняемые по расписанию, должны удовлетворять следующим требованиям:

  • Класс, в котором объявлен метод, должен быть компонентом в графе зависимостей, например помеченным аннотацией @Component.
  • Метод собственного планировщика не должен иметь аргументов (планировщик Quartz дополнительно допускает необязательный аргумент JobExecutionContext).
  • Возвращаемое значение метода игнорируется.
  • В Kotlin метод не должен быть suspend-функцией.

Интервал обязателен

@ScheduleAtFixedRate требует period, а @ScheduleWithFixedDelay требует delay. Если не задан ни атрибут аннотации (его значение по умолчанию равно 0), ни путь config, предоставляющий значение, компиляция завершается ошибкой Either period() or config() annotation parameter must be provided.

Подключение

Зависимость build.gradle:

implementation "ru.tinkoff.kora:scheduling-jdk"

Модуль:

@KoraApp
public interface Application extends SchedulingJdkModule { }

Зависимость build.gradle.kts:

implementation("ru.tinkoff.kora:scheduling-jdk")

Модуль:

@KoraApp
interface Application : SchedulingJdkModule

Конфигурация

Полный пример конфигурации, описываемой классом ScheduledExecutorServiceConfig, со значениями по умолчанию:

scheduling {
    threads = 2 //(1)!
    shutdownWait = "30s" //(2)!
    telemetry {
        logging {
            enabled = false //(3)!
        }
        metrics {
            enabled = true //(4)!
            slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(5)!
            tags = { // (6)!
                "key1" = "value1"
                "key2" = "value2"
            }
        }
        tracing {
            enabled = true //(7)!
            attributes = { // (8)!
                "key1" = "value1"
                "key2" = "value2"
            }
        }
    }
}
  1. Максимальное количество потоков в ScheduledExecutorService (по умолчанию: 2)
  2. Время ожидания завершения задач перед остановкой планировщика при плавной остановке (по умолчанию: 30s)
  3. Включает логирование модуля (по умолчанию: false)
  4. Включает метрики модуля (по умолчанию: true)
  5. Настраивает SLO для метрик (по умолчанию: ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO)
  6. Настраивает теги метрик (по умолчанию: {})
  7. Включает трассировку модуля (по умолчанию: true)
  8. Настраивает атрибуты трассировки (по умолчанию: {})
scheduling:
  threads: 2 #(1)!
  shutdownWait: "30s" #(2)!
  telemetry:
    logging:
      enabled: false #(3)!
    metrics:
      enabled: true #(4)!
      slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(5)!
      tags: #(6)!
        key1: value1
        key2: value2
    tracing:
      enabled: true #(7)!
      attributes: #(8)!
        key1: value1
        key2: value2
  1. Максимальное количество потоков в ScheduledExecutorService (по умолчанию: 2)
  2. Время ожидания завершения задач перед остановкой планировщика при плавной остановке (по умолчанию: 30s)
  3. Включает логирование модуля (по умолчанию: false)
  4. Включает метрики модуля (по умолчанию: true)
  5. Настраивает SLO для метрик (по умолчанию: ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO)
  6. Настраивает теги метрик (по умолчанию: {})
  7. Включает трассировку модуля (по умолчанию: true)
  8. Настраивает атрибуты трассировки (по умолчанию: {})

Метрики модуля описаны в разделе Справочник метрик.

Конфигурация конкретной задачи также может содержать собственную секцию telemetry, которая переопределяет общую для планировщика scheduling.telemetry только для этой задачи. Незаданные значения берутся из общей конфигурации, поэтому достаточно указать только то, что должно отличаться:

scheduling {
    jobs {
        fix-rate {
            period = "50ms"
            telemetry {
                logging.enabled = true //(1)!
                metrics.enabled = false //(2)!
            }
        }
    }
}
  1. Переопределяет scheduling.telemetry.logging.enabled только для этой задачи
  2. Переопределяет scheduling.telemetry.metrics.enabled только для этой задачи
scheduling:
  jobs:
    fix-rate:
      period: "50ms"
      telemetry:
        logging:
          enabled: true #(1)!
        metrics:
          enabled: false #(2)!
  1. Переопределяет scheduling.telemetry.logging.enabled только для этой задачи
  2. Переопределяет scheduling.telemetry.metrics.enabled только для этой задачи

Наблюдаемость задач по расписанию также можно настроить в коде, зарегистрировав компонент, реализующий SchedulingLoggerFactory, SchedulingMetricsFactory, SchedulingTracerFactory или целиком SchedulingTelemetryFactory.

Фиксированная частота

Планирование с запуском задач через фиксированный интервал времени независимо от того, завершилось ли предыдущее выполнение. Это может приводить к одновременному выполнению нескольких задач.

Например, если период равен 10 секундам, а каждое выполнение задачи занимает 5 секунд, то следующая задача запускается через 5 секунд после завершения предыдущей.

@Component
public class SomeService {

    @ScheduleAtFixedRate(initialDelay = 50, period = 50, unit = ChronoUnit.MILLIS)
    void schedule() {
        // do something
    }
}
@Component
class SomeService {

    @ScheduleAtFixedRate(initialDelay = 50, period = 50, unit = ChronoUnit.MILLIS)
    fun schedule() {
        // do something
    }
}

Конфигурация

Параметры можно передавать через конфигурацию; конфигурация имеет приоритет над значениями из аннотации. Путь config произвольный, но по соглашению вкладывается в секцию scheduling, чтобы параметры задачи и её telemetry находились вместе (как в проекте-примере, scheduling.jobs.fix-rate):

@Component
public class SomeService {

    @ScheduleAtFixedRate(config = "scheduling.jobs.fix-rate")
    void schedule() {
        // do something
    }
}
@Component
class SomeService {

    @ScheduleAtFixedRate(config = "scheduling.jobs.fix-rate")
    fun schedule() {
        // do something
    }
}

Пример файла конфигурации:

scheduling {
    jobs {
        fix-rate {
            initialDelay = "50ms" //(1)!
            period = "50ms" //(2)!
        }
    }
}
  1. Начальная задержка перед первой задачей (по умолчанию: 0ms)
  2. Периодический интервал между задачами (обязательный, без значения по умолчанию)
scheduling:
  jobs:
    fix-rate:
      initialDelay: "50ms" #(1)!
      period: "50ms" #(2)!
  1. Начальная задержка перед первой задачей (по умолчанию: 0ms)
  2. Периодический интервал между задачами (обязательный, без значения по умолчанию)

Фиксированная задержка

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

Не имеет значения, сколько длится текущее выполнение: следующая задача запускается после того, как предыдущая задача завершилась и прошла настроенная задержка.

@Component
public class SomeService {

    @ScheduleWithFixedDelay(initialDelay = 50, delay = 50, unit = ChronoUnit.MILLIS)
    void schedule() {
        // do something
    }
}
@Component
class SomeService {

    @ScheduleWithFixedDelay(initialDelay = 50, delay = 50, unit = ChronoUnit.MILLIS)
    fun schedule() {
        // do something
    }
}

Конфигурация

Параметры можно передавать через конфигурацию; она имеет приоритет над значениями из аннотации:

@Component
public class SomeService {

    @ScheduleWithFixedDelay(config = "scheduling.jobs.fix-delay")
    void schedule() {
        // do something
    }
}
@Component
class SomeService {

    @ScheduleWithFixedDelay(config = "scheduling.jobs.fix-delay")
    fun schedule() {
        // do something
    }
}

Пример файла конфигурации:

scheduling {
    jobs {
        fix-delay {
            initialDelay = "50ms" //(1)!
            delay = "50ms" //(2)!
        }
    }
}
  1. Начальная задержка перед первой задачей (по умолчанию: 0ms)
  2. Периодическая задержка между задачами (обязательный, без значения по умолчанию)
scheduling:
  jobs:
    fix-delay:
      initialDelay: "50ms" #(1)!
      delay: "50ms" #(2)!
  1. Начальная задержка перед первой задачей (по умолчанию: 0ms)
  2. Периодическая задержка между задачами (обязательный, без значения по умолчанию)

Однократно

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

@Component
public class SomeService {

    @ScheduleOnce(delay = 50, unit = ChronoUnit.MILLIS)
    void schedule() {
        // do something
    }
}
@Component
class SomeService {

    @ScheduleOnce(delay = 50, unit = ChronoUnit.MILLIS)
    fun schedule() {
        // do something
    }
}

Конфигурация

Параметры можно передавать через конфигурацию; она имеет приоритет над значениями из аннотации:

@Component
public class SomeService {

    @ScheduleOnce(config = "scheduling.jobs.once")
    void schedule() {
        // do something
    }
}
@Component
class SomeService {

    @ScheduleOnce(config = "scheduling.jobs.once")
    fun schedule() {
        // do something
    }
}

Пример файла конфигурации:

scheduling {
    jobs {
        once {
            delay = "50ms" //(1)!
        }
    }
}
  1. Задержка перед задачей (обязательный, без значения по умолчанию)
scheduling:
  jobs:
    once:
      delay: "50ms" #(1)!
  1. Задержка перед задачей (обязательный, без значения по умолчанию)

Плавная остановка

Во время плавной остановки собственный планировщик ожидает завершения задач в течение scheduling.shutdownWait. Если задачу нужно остановить раньше, проверяйте Thread.currentThread().isInterrupted() и останавливайте работу вручную.

Программное планирование

Для планирования задач в императивном стиле можно внедрить компонент JdkSchedulingExecutor. Он оборачивает тот же ScheduledExecutorService, что и аннотации, и предоставляет методы scheduleAtFixedRate, scheduleWithFixedDelay и schedule:

@Component
public class SomeService {

    private final JdkSchedulingExecutor executor;

    public SomeService(JdkSchedulingExecutor executor) {
        this.executor = executor;
    }

    public void start() {
        executor.scheduleAtFixedRate(() -> {
            // do something
        }, 50, 50, TimeUnit.MILLISECONDS);
    }
}
@Component
class SomeService(private val executor: JdkSchedulingExecutor) {

    fun start() {
        executor.scheduleAtFixedRate({
            // do something
        }, 50, 50, TimeUnit.MILLISECONDS)
    }
}

Quartz

Реализация на основе библиотеки Quartz используется для задач с расписанием по cron, пользовательских экземпляров Trigger и правил выполнения Quartz.

Подключение

Зависимость build.gradle:

implementation "ru.tinkoff.kora:scheduling-quartz"

Модуль:

@KoraApp
public interface Application extends QuartzModule { }

Зависимость build.gradle.kts:

implementation("ru.tinkoff.kora:scheduling-quartz")

Модуль:

@KoraApp
interface Application : QuartzModule

Конфигурация

Конфигурация Quartz задаётся значениями Properties в формате ключ-значение. Настройки Kora для плавной остановки и телеметрии задаются в секции scheduling. Конфигурация конкретной cron-задачи также может содержать секцию telemetry, значения которой переопределяют общую телеметрию планировщика для этой задачи.

quartz { //(1)!
    "org.quartz.threadPool.threadCount" = "10"
}
scheduling {
    waitForJobComplete = true //(2)!
    telemetry {
        logging {
            enabled = false //(3)!
        }
        metrics {
            enabled = true //(4)!
            slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(5)!
            tags = { // (6)!
                "key1" = "value1"
                "key2" = "value2"
            }
        }
        tracing {
            enabled = true //(7)!
            attributes = { // (8)!
                "key1" = "value1"
                "key2" = "value2"
            }
        }
    }
}
  1. Параметры конфигурации планировщика Quartz (по умолчанию используются свойства из quartz.properties ниже)
  2. Ожидать ли завершения задач перед остановкой планировщика при плавной остановке (по умолчанию: true)
  3. Включает логирование модуля (по умолчанию: false)
  4. Включает метрики модуля (по умолчанию: true)
  5. Настраивает SLO для метрик (по умолчанию: ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO)
  6. Настраивает теги метрик (по умолчанию: {})
  7. Включает трассировку модуля (по умолчанию: true)
  8. Настраивает атрибуты трассировки (по умолчанию: {})
quartz: #(1)!
  org.quartz.threadPool.threadCount: "10"
scheduling:
  waitForJobComplete: true #(2)!
  telemetry:
    logging:
      enabled: false #(3)!
    metrics:
      enabled: true #(4)!
      slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(5)!
      tags: #(6)!
        key1: value1
        key2: value2
    tracing:
      enabled: true #(7)!
      attributes: #(8)!
        key1: value1
        key2: value2
  1. Параметры конфигурации планировщика Quartz (по умолчанию используются свойства из quartz.properties ниже)
  2. Ожидать ли завершения задач перед остановкой планировщика при плавной остановке (по умолчанию: true)
  3. Включает логирование модуля (по умолчанию: false)
  4. Включает метрики модуля (по умолчанию: true)
  5. Настраивает SLO для метрик (по умолчанию: ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO)
  6. Настраивает теги метрик (по умолчанию: {})
  7. Включает трассировку модуля (по умолчанию: true)
  8. Настраивает атрибуты трассировки (по умолчанию: {})

Настройки по умолчанию используются из:

quartz.properties
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false

org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true

org.quartz.jobStore.misfireThreshold: 60000

org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore

Cron

Для запуска задач по расписанию используются cron-выражения.

Выражение Quartz состоит из шести обязательных полей и необязательного седьмого поля года, разделённых пробелами:

Поле Допустимые значения Обязательное
Секунды 0-59 да
Минуты 0-59 да
Часы 0-23 да
День месяца 1-31 да
Месяц 1-12 или JAN-DEC да
День недели 1-7 или SUN-SAT да
Год пусто, 1970-2099 нет

Помимо обычных чисел, диапазонов (8-10), списков (6,19) и шагов (0/30), поддерживаются следующие специальные символы:

Символ Значение
* Все значения поля (например, * в поле минут означает «каждую минуту»)
? Без конкретного значения, используется в поле дня месяца или дня недели, когда указано другое из них
L Последний (последний день месяца или последний указанный день недели в месяце)
W Ближайший будний день к указанному дню месяца
# N-й указанный день недели в месяце, например 5#2 — это вторая пятница

Примеры выражений:

Выражение Значение
0 0 * * * ? В начале каждого часа каждого дня
*/10 * * * * ? Каждые десять секунд
0 0 8-10 * * ? В 8, 9 и 10 часов каждого дня
0 0/30 8-10 * * ? В 8:00, 8:30, 9:00, 9:30, 10:00 и 10:30
0 0 0 L * ? В последний день месяца в полночь
0 0 0 1W * ? В первый будний день месяца в полночь
0 0 0 ? * 5#2 Во вторую пятницу месяца в полночь
@Component
public class SomeService {

    @ScheduleWithCron("* * * ? * * *") //(1)!
    void schedule() {
        // do something
    }
}
  1. cron-выражение, которое запускает задачу каждую секунду
@Component
class SomeService {

    @ScheduleWithCron("* * * ? * * *") //(1)!
    fun schedule() {
        // do something
    }
}
  1. cron-выражение, которое запускает задачу каждую секунду

Атрибут identity задаёт идентичность Quartz Trigger, используемую для именования задачи, что полезно для идентификации и замены задач, особенно с кластерными или персистентными реализациями JobStore:

@Component
public class SomeService {

    @ScheduleWithCron(value = "0 0 * * * ?", identity = "my-hourly-job") //(1)!
    void schedule() {
        // do something
    }
}
  1. cron-выражение, которое запускает задачу в начале каждого часа, зарегистрированное под идентичностью триггера my-hourly-job
@Component
class SomeService {

    @ScheduleWithCron(value = "0 0 * * * ?", identity = "my-hourly-job") //(1)!
    fun schedule() {
        // do something
    }
}
  1. cron-выражение, которое запускает задачу в начале каждого часа, зарегистрированное под идентичностью триггера my-hourly-job

Конфигурация

Параметры можно передавать через конфигурацию; конфигурация имеет приоритет над значениями из аннотации. Как и в случае собственного планировщика, путь config произвольный и по соглашению вкладывается в секцию scheduling (как в проекте-примере, scheduling.jobs.quartz):

@Component
public class SomeService {

    @ScheduleWithCron(config = "scheduling.jobs.quartz")
    void schedule() {
        // do something
    }
}
@Component
class SomeService {

    @ScheduleWithCron(config = "scheduling.jobs.quartz")
    fun schedule() {
        // do something
    }
}

Пример конфигурации:

scheduling {
    jobs {
        quartz {
            cron = "* * * ? * * *" //(1)!
        }
    }
}
  1. cron-выражение, которое запускает задачу каждую секунду (обязательный, без значения по умолчанию)
scheduling:
  jobs:
    quartz:
      cron: "* * * ? * * *" #(1)!
  1. cron-выражение, которое запускает задачу каждую секунду (обязательный, без значения по умолчанию)

Trigger

Для пользовательского расписания можно создать Trigger из библиотеки Quartz, зарегистрировать его в графе зависимостей с тегом, а затем использовать этот тег в аннотации @ScheduleWithTrigger.

@KoraApp
public interface Application extends QuartzModule {

    @Tag(SomeService.class) //(1)!
    default Trigger myTrigger() {
        return TriggerBuilder.newTrigger()
                .withIdentity("myTrigger")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInMilliseconds(50)
                        .repeatForever())
                .build();
    }
}

@Component
public class SomeService {

    @ScheduleWithTrigger(@Tag(SomeService.class)) //(2)!
    void schedule() {
        // do something
    }
}
  1. Тег, используемый для регистрации Trigger в графе зависимостей.
  2. Тот же тег, используемый задачей для получения Trigger.
@KoraApp
interface Application : QuartzModule {

    @Tag(SomeService::class) //(1)!
    fun myTrigger(): Trigger {
        return TriggerBuilder.newTrigger()
            .withIdentity("myTrigger")
            .startNow()
            .withSchedule(
                SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInMilliseconds(50)
                    .repeatForever()
            )
        .build()
    }
}

@Component
class SomeService {

    @ScheduleWithTrigger(@Tag(SomeService::class)) //(2)!
    fun schedule() {
        // do something
    }
}
  1. Тег, используемый для регистрации Trigger в графе зависимостей.
  2. Тот же тег, используемый задачей для получения Trigger.

Неконкурентное выполнение

Аннотация @DisallowConcurrentExecution предотвращает одновременное выполнение одного и того же метода планировщиком Quartz. Это аналог org.quartz.DisallowConcurrentExecution в Kora, который можно разместить на любом методе, помеченном @Schedule*.

@Component
public class SomeService {

    @DisallowConcurrentExecution
    @ScheduleWithCron(config = "scheduling.jobs.quartz")
    void schedule() {
        // do something
    }
}
@Component
class SomeService {

    @DisallowConcurrentExecution
    @ScheduleWithCron(config = "scheduling.jobs.quartz")
    fun schedule() {
        // do something
    }
}

Контекст задачи

Метод, выполняемый по расписанию Quartz, может опционально объявить единственный аргумент org.quartz.JobExecutionContext. Если он присутствует, Kora передаёт методу текущий контекст выполнения; если отсутствует, метод вызывается без аргументов. Контекст даёт доступ к org.quartz.JobDataMap задачи, что является способом чтения и записи состояния, связанного с задачей:

@Component
public class SomeService {

    @ScheduleWithCron(config = "scheduling.jobs.quartz")
    void schedule(JobExecutionContext context) {
        JobDataMap data = context.getJobDetail().getJobDataMap();
        int counter = data.containsKey("counter") ? data.getInt("counter") : 0;
        data.put("counter", counter + 1);
    }
}
@Component
class SomeService {

    @ScheduleWithCron(config = "scheduling.jobs.quartz")
    fun schedule(context: JobExecutionContext) {
        val data = context.jobDetail.jobDataMap
        val counter = if (data.containsKey("counter")) data.getInt("counter") else 0
        data.put("counter", counter + 1)
    }
}

Сохранение данных задачи

Аннотация @PersistJobDataAfterExecution указывает Quartz сохранять обновлённый org.quartz.JobDataMap после выполнения задачи, чтобы изменения, внесённые через JobExecutionContext, были видны при следующем выполнении.

Её рекомендуется использовать вместе с @DisallowConcurrentExecution, чтобы избежать конфликтов при сохранении данных во время одновременного выполнения задачи.

@Component
public class SomeService {

    @DisallowConcurrentExecution
    @PersistJobDataAfterExecution
    @ScheduleWithCron(config = "scheduling.jobs.quartz")
    void schedule(JobExecutionContext context) {
        JobDataMap data = context.getJobDetail().getJobDataMap();
        int counter = data.containsKey("counter") ? data.getInt("counter") : 0;
        data.put("counter", counter + 1); //(1)!
    }
}
  1. Обновлённое значение сохраняется после выполнения и доступно при следующем запуске
@Component
class SomeService {

    @DisallowConcurrentExecution
    @PersistJobDataAfterExecution
    @ScheduleWithCron(config = "scheduling.jobs.quartz")
    fun schedule(context: JobExecutionContext) {
        val data = context.jobDetail.jobDataMap
        val counter = if (data.containsKey("counter")) data.getInt("counter") else 0
        data.put("counter", counter + 1) //(1)!
    }
}
  1. Обновлённое значение сохраняется после выполнения и доступно при следующем запуске

Плавная остановка

Во время плавной остановки параметр scheduling.waitForJobComplete управляет тем, как останавливается планировщик Quartz. При true (по умолчанию) он вызывает scheduler.shutdown(true) и блокируется до завершения выполняющихся задач; при false он останавливается без ожидания. Как и в случае собственного планировщика, длительно выполняющиеся задачи всё же должны кооперативно проверять Thread.currentThread().isInterrupted() и останавливать работу вручную.

Scheduler

Лежащий в основе org.quartz.Scheduler регистрируется как компонент и может быть внедрён для продвинутых сценариев, таких как программная регистрация задач или инспекция состояния планировщика:

@Component
public class SomeService {

    private final Scheduler scheduler;

    public SomeService(Scheduler scheduler) {
        this.scheduler = scheduler;
    }
}
@Component
class SomeService(private val scheduler: Scheduler)