Spring Framework предусматривает абстракции для асинхронного выполнения и планирования задач с помощью интерфейсов TaskExecutor и TaskScheduler, соответственно. Spring также имеет реализации этих интерфейсов, которые поддерживают пулы потоков или делегирование полномочий CommonJ в серверном окружении приложений. В конечном итоге, использование этих реализаций за общими интерфейсами абстрагирует различия между окружениями Java SE 5, Java SE 6 и Java EE.

Spring также имеет классы интеграции для поддержки планирования с помощью Timer (входит в состав JDK с версии 1.3) и Quartz Scheduler (https://www.quartz-scheduler.org/). Можно установить оба этих планировщика, используя FactoryBean с дополнительными ссылками на экземпляры Timer или Trigger, соответственно. Кроме того, для планировщика Quartz и Timer доступен вспомогательный класс, позволяющий вызывать метод существующего целевого объекта (аналогично обычной операции MethodInvokingFactoryBean).

Абстракция TaskExecutor в Spring

Исполнители (executors) – это JDK-наименование понятия пулов потоков. Именование "исполнитель" связано с тем, что отсутствует гарантия, что базовая реализация на самом деле является пулом. Исполнитель может быть однопоточным или даже синхронным. Абстракция Spring скрывает детали реализации между окружениями Java SE и Java EE.

Интерфейс TaskExecutor в Spring идентичен интерфейсу java.util.concurrent.Executor. На самом деле, изначально основной причиной его существования было абстрагирование от необходимости использования Java 5 при использовании пулов потоков. Интерфейс имеет единственный метод (execute(Runnable task)), который принимает задачу к выполнению, основываясь на семантике и конфигурации пула потоков.

TaskExecutor был первоначально создан для того, чтобы предоставить другим компонентам Spring абстракцию для объединения потоков, когда это необходимо. Такие компоненты, как ApplicationEventMulticaster, AbstractMessageListenerContainer из JMS и интеграция Quartz, используют абстракцию TaskExecutor для объединения потоков. Однако если для бинов требуется применение логики работы пула потоков, можно использовать эту абстракцию для собственных нужд.

Типы TaskExecutor

Spring содержит ряд готовых реализаций TaskExecutor. Скорее всего, внедрять собственные вам никогда не придется. Варианты, которые предусматривает Spring, следующие:

  • SyncTaskExecutor: Эта реализация не выполняет вызовы асинхронно. Вместо этого каждый вызов происходит в вызывающем потоке. Эта реализация используется в основном в ситуациях, когда многопоточность не требуется, например, в простых тестовых сценариях.

  • SimpleAsyncTaskExecutor: Эта реализация не использует повторно ни один поток. Скорее, она запускает новый поток для каждого вызова. Тем не менее эта реализация поддерживает механизм ограничения параллелизма, который блокирует все вызовы, превышающие лимит, пока не освободится слот. Если требуется настоящее объединение в пул, см. ThreadPoolTaskExecutor далее в этом списке.

  • ConcurrentTaskExecutor: Эта реализация является адаптером для экземпляра java.util.concurrent.Executor. Существует альтернатива (ThreadPoolTaskExecutor), которая открывает параметры конфигурации Executor как свойства бина. Нeобходимость использовать ConcurrentTaskExecutor напрямую возникает крайне редко. Однако, если ThreadPoolTaskExecutor недостаточно гибок для ваших нужд, альтернативным вариантом будет ConcurrentTaskExecutor.

  • ThreadPoolTaskExecutor: Эта реализация используется наиболее часто. Она открывает свойства бина для конфигурирования java.util.concurrent.ThreadPoolExecutor и оборачивает его в TaskExecutor. Если необходима адаптация к другому типу java.util.concurrent.Executor, мы рекомендуем использовать вместо неё реализацию ConcurrentTaskExecutor.

  • WorkManagerTaskExecutor: Эта реализация использует WorkManager из CommonJ в качестве базового поставщика служб и является центральным вспомогательным классом для настройки интеграции пула потоков на основе CommonJ в WebLogic или WebSphere в контексте приложения Spring.

  • DefaultManagedTaskExecutor: Эта реализация использует полученный через JNDI экземпляр ManagedExecutorService в JSR-236-совместимом окружении выполнения (таком как сервер приложений на Java EE 7+), заменяя для этой цели WorkManager из CommonJ.

Использование TaskExecutor

Реализации TaskExecutor из Spring используются как простые классы JavaBeans. В следующем примере определяется бин, который использует ThreadPoolTaskExecutor для асинхронного вывода набора сообщений:

import org.springframework.core.task.TaskExecutor;
public class TaskExecutorExample {
    private class MessagePrinterTask implements Runnable {
        private String message;
        public MessagePrinterTask(String message) {
            this.message = message;
        }
        public void run() {
            System.out.println(message);
        }
    }
    private TaskExecutor taskExecutor;
    public TaskExecutorExample(TaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }
    public void printMessages() {
        for(int i = 0; i < 25; i++) {
            taskExecutor.execute(new MessagePrinterTask("Message" + i));
        }
    }
}

Как можно заметить, вместо того, чтобы извлекать поток из пула и выполнять его самостоятельно, вы добавляете свой Runnable в очередь. Затем TaskExecutor использует свои внутренние правила, чтобы решить, когда выполнять задачу.

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

<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
    <property name="corePoolSize" value="5"/>
    <property name="maxPoolSize" value="10"/>
    <property name="queueCapacity" value="25"/>
</bean>
<bean id="taskExecutorExample" class="TaskExecutorExample">
    <constructor-arg ref="taskExecutor"/>
</bean>

Абстракция TaskScheduler в Spring

В дополнение к абстракции TaskExecutor, в Spring 3.0 представлена абстракция TaskScheduler с различными методами планирования задач для выполнения в определенный момент в будущем. В следующем листинге показано определение интерфейса TaskScheduler:

public interface TaskScheduler {
    ScheduledFuture schedule(Runnable task, Trigger trigger);
    ScheduledFuture schedule(Runnable task, Instant startTime);
    ScheduledFuture schedule(Runnable task, Date startTime);
    ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
    ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period);
    ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);
    ScheduledFuture scheduleAtFixedRate(Runnable task, long period);
    ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);
    ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay);
    ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);
    ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay);
}

Самый простой метод – это метод с именем schedule, который принимает только Runnable и Date. Это приводит к тому, что задача выполняется единожды через заданное время. Все остальные методы могут планировать задачи для многократного выполнения. Методы с фиксированной частотой и фиксированной задержкой предназначены для простого периодического выполнения, но метод, принимающий Trigger, гораздо более гибкий.

Интерфейс Trigger

Интерфейс Trigger главным образом вдохновлен JSR-236, которая по состоянию на Spring 3.0 еще не была официально реализована. Основная идея Trigger заключается в том, что время выполнения может быть определено на основе предыдущих результатов выполнения или даже произвольных условий. Если эти определения учитывают результат предыдущего выполнения, то информация об этом становится доступной в TriggerContext. Сам интерфейс Trigger довольно прост и показан в следующем листинге:

public interface Trigger {
    Date nextExecutionTime(TriggerContext triggerContext);
}

TriggerContext является наиболее важным компонентом. Он включает в себя все необходимые данные и открыт для расширения в будущем, если это будет необходимо. TriggerContext – это интерфейс (по умолчанию используется реализация SimpleTriggerContext). В следующем листинге показаны доступные методы для реализаций Trigger.

public interface TriggerContext {
    Date lastScheduledExecutionTime();
    Date lastActualExecutionTime();
    Date lastCompletionTime();
}

Реализации интерфейса Trigger

Spring предусматривает две реализации интерфейса Trigger. Наиболее интересной из них является CronTrigger. Она позволяет планировать задания на основе выражений формата cron. Например, следующее задание запланировано на 15 минут после каждого часа, но только в рабочие часы с 9 до 5 в будние дни:

scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));

Другая реализация – это PeriodicTrigger, который принимает фиксированный период, необязательное значение начальной задержки и булево значение, указывающее, следует ли интерпретировать период как фиксированную частоту или фиксированную задержку. Поскольку интерфейс TaskScheduler уже определяет методы для планирования задач с фиксированной частотой или с фиксированной задержкой, то эти методы следует использовать напрямую, когда это возможно. Польза реализации PeriodicTrigger заключается в том, что вы можете использовать ее внутри компонентов, которые прибегают к абстракции Trigger. Например, удобнее может быть разрешить использовать периодические триггеры, триггеры на основе выражений в формате cron и даже пользовательские реализации триггеров как взаимозаменяемые. Такой компонент мог бы задействовать преимущества внедрения зависимостей, чтобы можно было настраивать такие Triggers извне и, следовательно, легко изменять или расширять их.

Реализации TaskScheduler

Как и в случае с абстракцией TaskExecutor для Spring, основное преимущество структуры TaskScheduler заключается в том, что потребности приложения в планировании отделены от окружения развертывания. Этот уровень абстракции особенно актуален при развертывании в окружении сервера приложений, где самому приложению не нужно непосредственно создавать потоки. Для таких сценариев Spring предусматривает TimerManagerTaskScheduler, который делегирует полномочия TimerManager из CommonJ в WebLogic или WebSphere, а также более свежий DefaultManagedTaskScheduler, который делегирует полномочия ManagedScheduledExecutorService из JSR-236 в окружении Java EE 7+. Оба варианта обычно конфигурируются при помощи поиска через JNDI.

Если нет нужды во внешнем управлении потоками, то более простой альтернативой является локальная служба ScheduledExecutorService, установленная внутри приложения, которую можно адаптировать с помощью ConcurrentTaskScheduler из Spring. Из соображений удобства Spring также предусматривает ThreadPoolTaskScheduler, который на внутреннем уровне делегирует полномочия службе ScheduledExecutorService для обеспечения общей конфигурации в стиле бинов, подобно ThreadPoolTaskExecutor. Эти варианты прекрасно работают и для локально встроенных пулов потоков в окружениях серверов приложений с опережающими вычислениями – в частности, в Tomcat и Jetty.