Перейти к основному содержимому

Аннотация @Scheduled весной

· 6 мин. чтения

Задача: Наибольшая подстрока без повторений

Для заданной строки s, найдите длину наибольшей подстроки без повторяющихся символов. Подстрока — это непрерывная непустая последовательность символов внутри строки...

ANDROMEDA 42

1. Обзор

В этом руководстве мы покажем, как аннотацию Spring @Scheduled можно использовать для настройки и планирования задач.

Простые правила, которым мы должны следовать, чтобы аннотировать метод с помощью @Scheduled :

  • метод обычно должен иметь возвращаемый тип void (в противном случае возвращаемое значение будет проигнорировано)
  • метод не должен ожидать никаких параметров

2. Включите поддержку планирования

Чтобы включить поддержку планирования задач и аннотацию @Scheduled в Spring, мы можем использовать аннотацию в стиле Java enable:

@Configuration
@EnableScheduling
public class SpringConfig {
...
}

И наоборот, мы можем сделать то же самое в XML:

<task:annotation-driven>

3. Запланируйте задачу с фиксированной задержкой

Начнем с настройки задачи для запуска после фиксированной задержки:

@Scheduled(fixedDelay = 1000)
public void scheduleFixedDelayTask() {
System.out.println(
"Fixed delay task - " + System.currentTimeMillis() / 1000);
}

В этом случае продолжительность между окончанием последнего выполнения и началом следующего выполнения является фиксированной. Задача всегда ожидает завершения предыдущей.

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

4. Запланируйте задачу с фиксированной скоростью

Давайте теперь выполним задачу через фиксированный интервал времени:

@Scheduled(fixedRate = 1000)
public void scheduleFixedRateTask() {
System.out.println(
"Fixed rate task - " + System.currentTimeMillis() / 1000);
}

Эту опцию следует использовать, когда каждое выполнение задачи является независимым.

Обратите внимание, что запланированные задачи по умолчанию не выполняются параллельно. Таким образом, даже если мы использовали fixedRate , следующая задача не будет запущена, пока не будет выполнена предыдущая.

Если мы хотим поддерживать параллельное поведение в запланированных задачах, нам нужно добавить аннотацию @Async :

@EnableAsync
public class ScheduledFixedRateExample {
@Async
@Scheduled(fixedRate = 1000)
public void scheduleFixedRateTaskAsync() throws InterruptedException {
System.out.println(
"Fixed rate task async - " + System.currentTimeMillis() / 1000);
Thread.sleep(2000);
}

}

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

5. Фиксированная скорость против фиксированной задержки

Мы можем запустить запланированную задачу, используя аннотацию Spring @Scheduled , но в зависимости от свойств fixedDelay и fixedRate характер выполнения меняется.

Свойство fixedDelay обеспечивает задержку в n миллисекунд между временем окончания выполнения задачи и временем начала следующего выполнения задачи.

Это свойство особенно полезно, когда нам нужно убедиться, что все время выполняется только один экземпляр задачи. Для зависимых работ это весьма полезно.

Свойство fixedRate запускает запланированную задачу каждые n миллисекунд. Он не проверяет какие-либо предыдущие выполнения задачи.

Это полезно, когда все выполнения задачи независимы. Если мы не ожидаем превышения размера памяти и пула потоков, fixedRate должен быть весьма удобен.

Хотя, если входящие задачи не завершаются быстро, возможно, они заканчиваются «исключением нехватки памяти».

6. Запланируйте задачу с начальной задержкой

Далее запланируем задачу с задержкой (в миллисекундах):

@Scheduled(fixedDelay = 1000, initialDelay = 1000)
public void scheduleFixedRateWithInitialDelayTask() {

long now = System.currentTimeMillis() / 1000;
System.out.println(
"Fixed rate task with one second initial delay - " + now);
}

Обратите внимание, что в этом примере мы используем как fixedDelay , так и initialDelay . Задача будет выполняться в первый раз после значения initialDelay и будет продолжать выполняться в соответствии с fixedDelay .

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

7. Запланируйте задачу с помощью выражений Cron

Иногда задержек и рейтов недостаточно, и нам нужна гибкость cron-выражения для управления расписанием наших задач:

@Scheduled(cron = "0 15 10 15 * ?")
public void scheduleTaskUsingCronExpression() {

long now = System.currentTimeMillis() / 1000;
System.out.println(
"schedule tasks using cron jobs - " + now);
}

Обратите внимание, что в этом примере мы планируем выполнение задачи в 10:15 утра 15-го числа каждого месяца.

По умолчанию Spring будет использовать локальный часовой пояс сервера для выражения cron. Однако мы можем использовать атрибут zone для изменения этого часового пояса :

@Scheduled(cron = "0 15 10 15 * ?", zone = "Europe/Paris")

С этой конфигурацией Spring запланирует запуск аннотированного метода в 10:15 утра 15-го числа каждого месяца по парижскому времени.

8. Параметризация расписания

Жестко запрограммировать эти расписания просто, но обычно нам нужно иметь возможность управлять расписанием без повторной компиляции и повторного развертывания всего приложения.

Мы будем использовать Spring Expressions для внешней конфигурации задач и будем хранить их в файлах свойств.

Задача с фиксированной задержкой :

@Scheduled(fixedDelayString = "${fixedDelay.in.milliseconds}")

Задача с фиксированной скоростью :

@Scheduled(fixedRateString = "${fixedRate.in.milliseconds}")

Задача на основе выражения cron :

@Scheduled(cron = "${cron.expression}")

9. Настройка запланированных задач с помощью XML

Spring также предоставляет XML-способ настройки запланированных задач. Вот конфигурация XML для их настройки:

<!-- Configure the scheduler -->
<task:scheduler id="myScheduler" pool-size="10" />

<!-- Configure parameters -->
<task:scheduled-tasks scheduler="myScheduler">
<task:scheduled ref="beanA" method="methodA"
fixed-delay="5000" initial-delay="1000" />
<task:scheduled ref="beanB" method="methodB"
fixed-rate="5000" />
<task:scheduled ref="beanC" method="methodC"
cron="*/5 * * * * MON-FRI" />
</task:scheduled-tasks>

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

Обычно все свойства аннотации @Scheduled разрешаются и инициализируются только один раз при запуске контекста Spring.

Поэтому изменение значений fixedDelay или fixedRate во время выполнения невозможно, если мы используем аннотацию @Scheduled в Spring .

Однако есть обходной путь. Использование Spring SchedulingConfigurer предоставляет более настраиваемый способ дать нам возможность динамически устанавливать задержку или скорость .

Давайте создадим конфигурацию Spring DynamicSchedulingConfig и реализуем интерфейс SchedulingConfigurer :

@Configuration
@EnableScheduling
public class DynamicSchedulingConfig implements SchedulingConfigurer {

@Autowired
private TickService tickService;

@Bean
public Executor taskExecutor() {
return Executors.newSingleThreadScheduledExecutor();
}

@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
taskRegistrar.addTriggerTask(
new Runnable() {
@Override
public void run() {
tickService.tick();
}
},
new Trigger() {
@Override
public Date nextExecutionTime(TriggerContext context) {
Optional<Date> lastCompletionTime =
Optional.ofNullable(context.lastCompletionTime());
Instant nextExecutionTime =
lastCompletionTime.orElseGet(Date::new).toInstant()
.plusMillis(tickService.getDelay());
return Date.from(nextExecutionTime);
}
}
);
}

}

Как мы заметили, с помощью метода ScheduledTaskRegistrar#addTriggerTask мы можем добавить задачу Runnable и реализацию Trigger для пересчета nextExecutionTime после окончания каждого выполнения.

Кроме того, мы аннотируем наш DynamicSchedulingConfig с помощью @EnableScheduling , чтобы планирование работало.

В результате мы запланировали запуск метода TickService#tick после каждой задержки, которая определяется динамически во время выполнения методом getDelay .

11. Параллельное выполнение задач

По умолчанию Spring использует для запуска задач локальный однопоточный планировщик . В результате, даже если у нас есть несколько методов @Scheduled , каждый из них должен ждать, пока поток завершит выполнение предыдущей задачи.

Если наши задачи действительно независимы, их удобнее запускать параллельно. Для этого нам нужно предоставить TaskScheduler , который лучше соответствует нашим потребностям:

@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(5);
threadPoolTaskScheduler.setThreadNamePrefix("ThreadPoolTaskScheduler");
return threadPoolTaskScheduler;
}

В приведенном выше примере мы настроили TaskScheduler с размером пула, равным пяти, но имейте в виду, что фактическая конфигурация должна быть точно настроена в соответствии с конкретными потребностями.

11.1. Использование весенней загрузки

Если мы используем Spring Boot, мы можем использовать еще более удобный подход для увеличения размера пула планировщика.

Достаточно просто установить свойство spring.task.scheduling.pool.size :

spring.task.scheduling.pool.size=5

12. Заключение

В этой статье мы обсудили способ настройки и использования аннотации @Scheduled .

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

Показанные выше примеры можно найти на GitHub .