Начиная с версии 3.0, Spring содержит пространство имен XML для конфигурирования экземпляров TaskExecutor и TaskScheduler. Фреймворк также предусматривает удобный способ конфигурирования задач для планирования с помощью триггера.

Элемент "scheduler"

Следующий элемент создает экземпляр ThreadPoolTaskScheduler с указанным размером пула потоков:

<task:scheduler id="scheduler" pool-size="10"/>

Значение, указанное для атрибута id, используется в качестве префикса для имен потоков в пуле. Элемент scheduler относительно прост и понятен. Если атрибут pool-size не будет указан, то пул потоков по умолчанию будет содержать лишь один поток. Других вариантов конфигурации для планировщика нет.

Элемент executor

Ниже показано создание экземпляра ThreadPoolTaskExecutor:

<task:executor id="executor" pool-size="10"/>

Как и в планировщике, показанном в предыдущем разделе, значение атрибута id используется в качестве префикса для имен потоков в пуле. Что касается размера пула, элемент executor поддерживает больше вариантов конфигурации, чем элемент scheduler. Во-первых, пул потоков для ThreadPoolTaskExecutor сам по себе лучше поддается изменениям конфигурации. Вместо единственного значения размера, пул потоков исполнителя может иметь разные значения для размера основного пула и для максимального размера пула. Если указывается единственное значение, исполнителю присваивается пул потоков фиксированного размера (размер основного пула и максимальный размер пула одинаковы). Однако атрибут pool-size элемента executor также принимает диапазон в виде min-max. В следующем примере установлено минимальное значение 5 и максимальное значение 25:

<task:executor
        id="executorWithPoolSizeRange"
        pool-size="5-25"
        queue-capacity="100"/>

В предыдущей конфигурации также было указано значение для queue-capacity. Конфигурацию пула потоков также следует рассматриваться в свете вместимости очереди исполнителя. Полное описание взаимосвязи между размером пула и вместимостью очереди см. в документации к ThreadPoolExecutor. Основная идея заключается в том, что при передаче задачи исполнитель сначала пытается использовать свободный поток, если количество активных потоков в текущий момент не превышает размер основного пула. Когда значение размера основного пула будет достигнуто, задача будет добавлена в очередь, если еще не превышена её вместимость. Только после этого, если значение вместимости очереди будет достигнуто, исполнитель создаст новый поток, превышающий размер основного пула. Если максимальный размер также был достигнут, то исполнитель отклонит задачу.

По умолчанию очередь не ограничена, но это едва ли желаемый вариант конфигурации, поскольку может привести к OutOfMemoryErrors, если в очередь будет добавлено достаточное количество задач, пока все потоки пула будут заняты. Более того, если очередь не ограничить, максимальный размер пула вообще не будет ни на что влиять. Поскольку исполнитель всегда проверяет очередь перед созданием нового потока, превышающего размер основного пула, очередь должна иметь конечную вместимость, чтобы пул потоков мог уйти за пределы размера основного пула (именно поэтому пул фиксированного размера является единственной разумной мерой при использовании неограниченной очереди).

Рассмотрим случай, о котором говорилось выше, когда задача отклоняется. По умолчанию, если задача отклоняется, исполнитель пула потоков генерирует TaskRejectedException. Однако на самом деле политика отклонения является конфигурируемой. Исключение генерируется при использовании политики отклонения по умолчанию, которая представляет собой реализацию AbortPolicy. Для приложений, где некоторые задачи могут пропускаться при высокой нагрузке, можно вместо этого сконфигурировать DiscardPolicy или DiscardOldestPolicy. Другой вариант, который отлично подходит для приложений, для которых необходимо регулировать (дросселировать) выполняемые задачи при высокой нагрузке, – это CallerRunsPolicy. Вместо того чтобы генерировать исключение или отклонять задачи, эта политика заставляет поток, вызывающий метод передачи, выполнять задачу самостоятельно. Идея заключается в том, чтоб такая вызывающая программа была занята во время выполнения этой задачи и не могла немедленно передавать другие задачи. Поэтому такая политика предоставляет простой способ регулирования входящей нагрузки, сохраняя при этом ограничения пула потоков и очереди. Как правило, это позволяет исполнителю "наверстать упущенное" относительно задач, которые он обрабатывает, и тем самым освободить часть вместимости в очереди, в пуле или и там, и там. Можно выбрать любой из этих вариантов из перечня значений, доступных для атрибута rejection-policy элемента executor.

В следующем примере показан элемент executor с рядом атрибутов для задания различных вариантов логики работы:

<task:executor
        id="executorWithCallerRunsPolicy"
        pool-size="5-25"
        queue-capacity="100"
        rejection-policy="CALLER_RUNS"/>

Наконец, параметр keep-alive определяет границу времени (в секундах), в течение которого потоки могут простаивать, прежде чем будут остановлены. Если количество потоков в пуле превышает значение для основного пула, то после истечения времени ожидания без обработки задачи, лишние потоки будут остановлены. Значение времени, равное нулю, приводит к тому, что лишние потоки останавливаются сразу после выполнения задачи, не оставляя ничего для последующей работы в очереди задач. В следующем примере значение параметра keep-alive установлено на две минуты:

<task:executor
        id="executorWithKeepAlive"
        pool-size="5-25"
        keep-alive="120"/>

Элемент "scheduled-tasks"

Наиболее мощной особенностью пространства имен задач в Spring является поддержка конфигурирования задач для планирования в рамках контекста приложения в Spring. Это соответствует подходу, аналогичному другим "методам-инициаторам вызова" в Spring, например, предоставляемому пространством имен JMS для конфигурирования POJO-объектов, управляемых сообщениями. В принципе, атрибут ref может указывать на любой объект, управляемый Spring, а атрибут method передает имя метода, который будет вызван для этого объекта. В следующем листинге показан простой пример:

<task:scheduled-tasks scheduler="myScheduler">
    <task:scheduled ref="beanA" method="methodA" fixed-delay="5000"/>
</task:scheduled-tasks>
<task:scheduler id="myScheduler" pool-size="10"/>

На планировщик ссылается внешний элемент, а каждая отдельная задача включает конфигурацию метаданных своего триггера. В предыдущем примере эти метаданные определяют периодический триггер с фиксированной задержкой, указывающей на количество миллисекунд, которое необходимо подождать после завершения выполнения каждой задачи. Другой вариант – fixed-rate, указывающий, как часто должен выполняться метод, независимо от того, сколько времени заняло предыдущее выполнение. Кроме того, для задач с fixed-delay и fixed-rate можно задать параметр начальной задержки ("initial-delay"), указывающий на количество миллисекунд ожидания перед первым выполнением метода. Для большего контроля можно вместо этого предоставить атрибут cron, чтобы передать выражение в формате cron. В следующем примере показаны другие варианты:

<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>
<task:scheduler id="myScheduler" pool-size="10"/>