Java предоставляет множество способов создания пула потоков и получения экземпляра Future в результате выполнения задачи. Это в равной степени кусок пирога для весны, благодаряscheduling
Пакет может выполнять фоновое выполнение потока задачи.
В первой части этой статьи мы обсудим некоторые основы выполнения запланированных задач в Spring. После этого мы объясним, как эти классы работают вместе, чтобы инициировать и выполнять запланированные задачи. В следующем разделе будет рассмотрена настройка запланированных и асинхронных задач. Наконец, давайте напишем демонстрацию, чтобы увидеть, как организовывать запланированные задачи с помощью модульного тестирования.
Что такое асинхронные задачи в Spring?
Прежде чем мы перейдем к теме формально, для Spring нам нужно понять две разные концепции, которые он реализует: асинхронные задачи и задачи планирования. Очевидно, что у них есть одна общая черта: оба работают в фоновом режиме. Однако между ними есть большая разница. Планирование задач отличается от асинхронного, и его роль такая же, как в Linux.CRON job
Точно так же (есть и запланированные задачи в виндовс). Например, если есть задача, которая должна выполняться каждые 40 минут, эту настройку можно выполнить с помощью XML-файлов или аннотаций. Простые асинхронные задачи могут выполняться в фоновом режиме, нет необходимости настраивать частоту выполнения.
Поскольку это два разных типа задач, их исполнители, естественно, разные. Первый немного похож на параллельный исполнитель Java (concurrency executor
), будет специальная статья об исполнителе в Java, чтобы узнать о нем больше. согласно сВесенняя документацияTaskExecutorКак уже упоминалось, он предоставляет основанную на Spring абстракцию для обработки пулов потоков, что также можно понять с помощью аннотаций его классов. Другой абстрактный интерфейсTaskScheduler, который используется для планирования задачи в определенный момент в будущем и выполнения ее один раз или периодически.
В процессе анализа исходного кода я обнаружил еще один интересный момент — это триггер. Существует в двух видах:CronTriggerилиPeriodTrigger. Первый имитирует поведение задач CRON. Таким образом, мы можем представить выполнение задачи в определенный момент в будущем. Другой триггер может использоваться для периодического выполнения задач.
Класс асинхронных задач Spring
Давайте начнем сorg.springframework.core.task.TaskExecutorНачинается анализ класса. Вы обнаружите, что это не просто, это интерфейс, который расширяет интерфейс Java Executor. Это единственный способвоплощать в жизнь, используйте в параметре задачу типа Runnable.
package org.springframework.core.task;
import java.util.concurrent.Executor;
/**
* Simple task executor interface that abstracts the execution
* of a {@link Runnable}.
*
* <p>Implementations can use all sorts of different execution strategies,
* such as: synchronous, asynchronous, using a thread pool, and more.
*
* <p>Equivalent to JDK 1.5's {@link java.util.concurrent.Executor}
* interface; extending it now in Spring 3.0, so that clients may declare
* a dependency on an Executor and receive any TaskExecutor implementation.
* This interface remains separate from the standard Executor interface
* mainly for backwards compatibility with JDK 1.4 in Spring 2.x.
*
* @author Juergen Hoeller
* @since 2.0
* @see java.util.concurrent.Executor
*/
@FunctionalInterface
public interface TaskExecutor extends Executor {
/**
* Execute the given {@code task}.
* <p>The call might return immediately if the implementation uses
* an asynchronous execution strategy, or might block in the case
* of synchronous execution.
* @param task the {@code Runnable} to execute (never {@code null})
* @throws TaskRejectedException if the given task was not accepted
*/
@Override
void execute(Runnable task);
}
Условно говоря,org.springframework.scheduling.TaskSchedulerИнтерфейс немного сложнее. Он определяет набор методов с именами, начинающимися с Schedule, которые позволяют нам определять задачи, которые будут выполняться в будущем. всеschedule* метод возвращаетjava.util.concurrent.ScheduledFutureпример. Весна 5 парscheduleAtFixedRate
Метод был дополнительно обогащен, фактически, окончательный вызов по-прежнемуScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period);
public interface TaskScheduler {
/**
* Schedule the given {@link Runnable}, invoking it whenever the trigger
* indicates a next execution time.
* <p>Execution will end once the scheduler shuts down or the returned
* {@link ScheduledFuture} gets cancelled.
* @param task the Runnable to execute whenever the trigger fires
* @param trigger an implementation of the {@link Trigger} interface,
* e.g. a {@link org.springframework.scheduling.support.CronTrigger} object
* wrapping a cron expression
* @return a {@link ScheduledFuture} representing pending completion of the task,
* or {@code null} if the given Trigger object never fires (i.e. returns
* {@code null} from {@link Trigger#nextExecutionTime})
* @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted
* for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress)
* @see org.springframework.scheduling.support.CronTrigger
*/
@Nullable
ScheduledFuture<?> schedule(Runnable task, Trigger trigger);
/**
* Schedule the given {@link Runnable}, invoking it at the specified execution time.
* <p>Execution will end once the scheduler shuts down or the returned
* {@link ScheduledFuture} gets cancelled.
* @param task the Runnable to execute whenever the trigger fires
* @param startTime the desired execution time for the task
* (if this is in the past, the task will be executed immediately, i.e. as soon as possible)
* @return a {@link ScheduledFuture} representing pending completion of the task
* @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted
* for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress)
* 使用了默认实现,值得我们学习使用的,Java9中同样可以有私有实现的,从这里我们可以做到我只通过 * 一个接口你来实现,我把其他相应的功能默认实现下,最后调用你自定义实现的接口即可,使接口功能更 * 加一目了然
* @since 5.0
* @see #schedule(Runnable, Date)
*/
default ScheduledFuture<?> schedule(Runnable task, Instant startTime) {
return schedule(task, Date.from(startTime));
}
/**
* Schedule the given {@link Runnable}, invoking it at the specified execution time.
* <p>Execution will end once the scheduler shuts down or the returned
* {@link ScheduledFuture} gets cancelled.
* @param task the Runnable to execute whenever the trigger fires
* @param startTime the desired execution time for the task
* (if this is in the past, the task will be executed immediately, i.e. as soon as possible)
* @return a {@link ScheduledFuture} representing pending completion of the task
* @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted
* for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress)
*/
ScheduledFuture<?> schedule(Runnable task, Date startTime);
...
/**
* Schedule the given {@link Runnable}, invoking it at the specified execution time
* and subsequently with the given period.
* <p>Execution will end once the scheduler shuts down or the returned
* {@link ScheduledFuture} gets cancelled.
* @param task the Runnable to execute whenever the trigger fires
* @param startTime the desired first execution time for the task
* (if this is in the past, the task will be executed immediately, i.e. as soon as possible)
* @param period the interval between successive executions of the task
* @return a {@link ScheduledFuture} representing pending completion of the task
* @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted
* for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress)
* @since 5.0
* @see #scheduleAtFixedRate(Runnable, Date, long)
*/
default ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) {
return scheduleAtFixedRate(task, Date.from(startTime), period.toMillis());
}
/**
* Schedule the given {@link Runnable}, invoking it at the specified execution time
* and subsequently with the given period.
* <p>Execution will end once the scheduler shuts down or the returned
* {@link ScheduledFuture} gets cancelled.
* @param task the Runnable to execute whenever the trigger fires
* @param startTime the desired first execution time for the task
* (if this is in the past, the task will be executed immediately, i.e. as soon as possible)
* @param period the interval between successive executions of the task (in milliseconds)
* @return a {@link ScheduledFuture} representing pending completion of the task
* @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted
* for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress)
*/
ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Date startTime, long period);
...
}
Два компонента триггера, упомянутые ранее, оба реализованыorg.springframework.scheduling.Triggerинтерфейс. Здесь нам нужно сосредоточиться только на одном методеnextExecutionTime, который определяет время выполнения следующей запущенной задачи. Две его реализации, CronTrigger и PeriodicTrigger, предоставляютсяorg.springframework.scheduling.TriggerContextЧтобы добиться хранения информации, мы можем легко получить время последнего выполнения задачи (lastScheduledExecutionTime), время последнего завершения данной задачи (lastCompletionTime) или последнее фактическое время выполнения (lastActualExecutionTime). Далее мы просто понимаем эти вещи, читая исходный код.org.springframework.scheduling.concurrent.ConcurrentTaskSchedulerсодержит частный классEnterpriseConcurrentTriggerScheduler
. в этотclass
Внутри мы можем найти метод расписания:
public ScheduledFuture<?> schedule(Runnable task, final Trigger trigger) {
ManagedScheduledExecutorService executor = (ManagedScheduledExecutorService) scheduledExecutor;
return executor.schedule(task, new javax.enterprise.concurrent.Trigger() {
@Override
public Date getNextRunTime(LastExecution le, Date taskScheduledTime) {
return trigger.nextExecutionTime(le != null ?
new SimpleTriggerContext(le.getScheduledStart(), le.getRunStart(), le.getRunEnd()) :
new SimpleTriggerContext());
}
@Override
public boolean skipRun(LastExecution lastExecution, Date scheduledRunTime) {
return false;
}
});
}
SimpleTriggerContext
Многое видно из его названия, потому что оно реализуетTriggerContext
интерфейс.
/**
* Simple data holder implementation of the {@link TriggerContext} interface.
*
* @author Juergen Hoeller
* @since 3.0
*/
public class SimpleTriggerContext implements TriggerContext {
@Nullable
private volatile Date lastScheduledExecutionTime;
@Nullable
private volatile Date lastActualExecutionTime;
@Nullable
private volatile Date lastCompletionTime;
...
/**
* Create a SimpleTriggerContext with the given time values.
* @param lastScheduledExecutionTime last <i>scheduled</i> execution time
* @param lastActualExecutionTime last <i>actual</i> execution time
* @param lastCompletionTime last completion time
*/
public SimpleTriggerContext(Date lastScheduledExecutionTime, Date lastActualExecutionTime, Date lastCompletionTime) {
this.lastScheduledExecutionTime = lastScheduledExecutionTime;
this.lastActualExecutionTime = lastActualExecutionTime;
this.lastCompletionTime = lastCompletionTime;
}
...
}
Также, как вы можете видеть, значение времени, установленное в конструкторе, берется изjavax.enterprise.concurrent.LastExecutionреализация, где:
- getScheduledStart: возвращает время последнего запуска задачи. Он соответствует свойству lastScheduledExecutionTime TriggerContext.
- getRunStart: возвращает время запуска данной задачи. В TriggerContext это соответствует lastActualExecutionTime.
- getRunEnd: возвращается после завершения задачи. Он используется для установки lastCompletionTime в TriggerContext.
Еще один важный класс в Spring для планирования и асинхронного выполнения — этоorg.springframework.core.task.support.TaskExecutorAdapter. это будетjava.util.concurrent.ExecutorВ качестве адаптера для базового актуатора Spring (описание немного неудобное, просто посмотрите на код ниже), он был описан ранее.TaskExecutor
. На самом деле он ссылается на ExecutorService Java, который также наследуетExecutor
интерфейс. Эта ссылка используется для выполнения всех представленных задач.
/**
* Adapter that takes a JDK {@code java.util.concurrent.Executor} and
* exposes a Spring {@link org.springframework.core.task.TaskExecutor} for it.
* Also detects an extended {@code java.util.concurrent.ExecutorService 从此解释上面的说明}, adapting
* the {@link org.springframework.core.task.AsyncTaskExecutor} interface accordingly.
*
* @author Juergen Hoeller
* @since 3.0
* @see java.util.concurrent.Executor
* @see java.util.concurrent.ExecutorService
* @see java.util.concurrent.Executors
*/
public class TaskExecutorAdapter implements AsyncListenableTaskExecutor {
private final Executor concurrentExecutor;
@Nullable
private TaskDecorator taskDecorator;
...
/**
* Create a new TaskExecutorAdapter,
* using the given JDK concurrent executor.
* @param concurrentExecutor the JDK concurrent executor to delegate to
*/
public TaskExecutorAdapter(Executor concurrentExecutor) {
Assert.notNull(concurrentExecutor, "Executor must not be null");
this.concurrentExecutor = concurrentExecutor;
}
/**
* Delegates to the specified JDK concurrent executor.
* @see java.util.concurrent.Executor#execute(Runnable)
*/
@Override
public void execute(Runnable task) {
try {
doExecute(this.concurrentExecutor, this.taskDecorator, task);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(
"Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex);
}
}
@Override
public void execute(Runnable task, long startTimeout) {
execute(task);
}
@Override
public Future<?> submit(Runnable task) {
try {
if (this.taskDecorator == null && this.concurrentExecutor instanceof ExecutorService) {
return ((ExecutorService) this.concurrentExecutor).submit(task);
}
else {
FutureTask<Object> future = new FutureTask<>(task, null);
doExecute(this.concurrentExecutor, this.taskDecorator, future);
return future;
}
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(
"Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex);
}
}
...
}
Настройка асинхронных и запланированных задач в Spring
Ниже мы реализуем асинхронные задачи с помощью кода. Во-первых, нам нужно включить настройку через аннотации. Его XML-конфигурация выглядит следующим образом:
<task:scheduler id="taskScheduler"/>
<task:executor id="taskExecutor" pool-size="2" />
<task:annotation-driven executor="taskExecutor" scheduler="taskScheduler"/>
<context:component-scan base-package="com.migo.async"/>
можно сделать, поставив@EnableScheduling
а также@EnableAsync
В класс конфигурации добавляется аннотация (с аннотацией @Configuration ), чтобы активировать оба. После этого мы можем приступить к реализации планирования и асинхронных задач. Для реализации задач планирования мы можем использовать@Scheduled
аннотация. мы можем начать сorg.springframework.scheduling.annotationНайдите эту аннотацию в упаковке. Он содержит следующие свойства:
-
cron
:использоватьCRON
Конфигурация стиля (стиль задач конфигурации Linux) для настройки аннотированных задач, которые необходимо запустить. -
zone
: разобратьCRON
Часовой пояс выражения. -
fixedDelay
илиfixedDelayString
: выполнение задачи после фиксированного времени задержки. т. е. задача будет выполнена по истечении этого фиксированного периода времени между окончанием последнего вызова и началом следующего вызова. -
fixedRate
илиfixedRateString
:использоватьfixedRate
Вызов аннотированного метода будет выполняться в фиксированный период времени (например, каждые 10 секунд), независимо от жизненного цикла выполнения (начало, конец). -
initialDelay
илиinitialDelayString
: задерживает первое выполнение метода отправки. Обратите внимание, что все значения (fixedDelay ,fixedRate ,initialDelay ) должен быть выражен в миллисекундах. Важно помнить, что, метод с аннотацией @Scheduled не может принимать никакие параметры и ничего не возвращает (void).Если есть возвращаемое значение, возвращаемое значение также будет проигнорировано, что бесполезно. Метод задачи cron управляется контейнером, а не вызывающей стороной во время выполнения. Они состоят изorg.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessorto parse, который содержит следующий метод для отказа от выполнения всех некорректно определенных функций:protected void processScheduled(Scheduled scheduled, Method method, Object bean) { try { Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled"); /** * 之前的版本中直接把返回值非空的给拒掉了,在Spring 4.3 Spring5 的版本中就没那么严格了 * Assert.isTrue(void.class.equals(method.getReturnType()), * "Only void-returning methods may be annotated with @Scheduled"); **/ // ...
/** * 注释很重要 * An annotation that marks a method to be scheduled. Exactly one of * the {@link #cron()}, {@link #fixedDelay()}, or {@link #fixedRate()} * attributes must be specified. * * <p>The annotated method must expect no arguments. It will typically have * a {@code void} return type; if not, the returned value will be ignored * when called through the scheduler. * * <p>Processing of {@code @Scheduled} annotations is performed by * registering a {@link ScheduledAnnotationBeanPostProcessor}. This can be * done manually or, more conveniently, through the {@code <task:annotation-driven/>} * element or @{@link EnableScheduling} annotation. * * <p>This annotation may be used as a <em>meta-annotation</em> to create custom * <em>composed annotations</em> with attribute overrides. * * @author Mark Fisher * @author Dave Syer * @author Chris Beams * @since 3.0 * @see EnableScheduling * @see ScheduledAnnotationBeanPostProcessor * @see Schedules */ @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Repeatable(Schedules.class) public @interface Scheduled { ... }
использовать@Async
Аннотация помечает метод или класс (помечая класс, мы автоматически помечаем все его методы как асинхронные). а также@Scheduled
Иными словами, асинхронные задачи могут принимать параметры и, возможно, что-то возвращать.
Напишите демонстрацию, которая выполняет асинхронные задачи в Spring.
Вооружившись вышеуказанными знаниями, мы можем писать асинхронные и запланированные задачи. Мы продемонстрируем на тестовых примерах. Начнем с тестов разных исполнителей задач:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:applicationContext-test.xml"})
@WebAppConfiguration
public class TaskExecutorsTest {
@Test
public void simpeAsync() throws InterruptedException {
/**
* SimpleAsyncTaskExecutor creates new Thread for every task and executes it asynchronously. The threads aren't reused as in
* native Java's thread pools.
*
* The number of concurrently executed threads can be specified through concurrencyLimit bean property
* (concurrencyLimit XML attribute). Here it's more simple to invoke setConcurrencyLimit method.
* Here the tasks will be executed by 2 simultaneous threads. Without specifying this value,
* the number of executed threads will be indefinite.
*
* You can observe that only 2 tasks are executed at a given time - even if 3 are submitted to execution (lines 40-42).
**/
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor("thread_name_prefix_____");
executor.setConcurrencyLimit(2);
executor.execute(new SimpleTask("SimpleAsyncTask-1", Counters.simpleAsyncTask, 1000));
executor.execute(new SimpleTask("SimpleAsyncTask-2", Counters.simpleAsyncTask, 1000));
Thread.sleep(1050);
assertTrue("2 threads should be terminated, but "+Counters.simpleAsyncTask.getNb()+" were instead", Counters.simpleAsyncTask.getNb() == 2);
executor.execute(new SimpleTask("SimpleAsyncTask-3", Counters.simpleAsyncTask, 1000));
executor.execute(new SimpleTask("SimpleAsyncTask-4", Counters.simpleAsyncTask, 1000));
executor.execute(new SimpleTask("SimpleAsyncTask-5", Counters.simpleAsyncTask, 2000));
Thread.sleep(1050);
assertTrue("4 threads should be terminated, but "+Counters.simpleAsyncTask.getNb()+" were instead", Counters.simpleAsyncTask.getNb() == 4);
executor.execute(new SimpleTask("SimpleAsyncTask-6", Counters.simpleAsyncTask, 1000));
Thread.sleep(1050);
assertTrue("6 threads should be terminated, but "+Counters.simpleAsyncTask.getNb()+" were instead",
Counters.simpleAsyncTask.getNb() == 6);
}
@Test
public void syncTaskTest() {
/**
* SyncTask works almost as Java's CountDownLatch. In fact, this executor is synchronous with the calling thread. In our case,
* SyncTaskExecutor tasks will be synchronous with JUnit thread. It means that the testing thread will sleep 5
* seconds after executing the third task ('SyncTask-3'). To prove that, we check if the total execution time is ~5 seconds.
**/
long start = System.currentTimeMillis();
SyncTaskExecutor executor = new SyncTaskExecutor();
executor.execute(new SimpleTask("SyncTask-1", Counters.syncTask, 0));
executor.execute(new SimpleTask("SyncTask-2", Counters.syncTask, 0));
executor.execute(new SimpleTask("SyncTask-3", Counters.syncTask, 0));
executor.execute(new SimpleTask("SyncTask-4", Counters.syncTask, 5000));
executor.execute(new SimpleTask("SyncTask-5", Counters.syncTask, 0));
long end = System.currentTimeMillis();
int execTime = Math.round((end-start)/1000);
assertTrue("Execution time should be 5 seconds but was "+execTime+" seconds", execTime == 5);
}
@Test
public void threadPoolTest() throws InterruptedException {
/**
* This executor can be used to expose Java's native ThreadPoolExecutor as Spring bean, with the
* possibility to set core pool size, max pool size and queue capacity through bean properties.
*
* It works exactly as ThreadPoolExecutor from java.util.concurrent package. It means that our pool starts
* with 2 threads (core pool size) and can be growth until 3 (max pool size).
* In additionally, 1 task can be stored in the queue. This task will be treated
* as soon as one from 3 threads ends to execute provided task. In our case, we try to execute 5 tasks
* in 3 places pool and 1 place queue. So the 5th task should be rejected and TaskRejectedException should be thrown.
**/
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(3);
executor.setQueueCapacity(1);
executor.initialize();
executor.execute(new SimpleTask("ThreadPoolTask-1", Counters.threadPool, 1000));
executor.execute(new SimpleTask("ThreadPoolTask-2", Counters.threadPool, 1000));
executor.execute(new SimpleTask("ThreadPoolTask-3", Counters.threadPool, 1000));
executor.execute(new SimpleTask("ThreadPoolTask-4", Counters.threadPool, 1000));
boolean wasTre = false;
try {
executor.execute(new SimpleTask("ThreadPoolTask-5", Counters.threadPool, 1000));
} catch (TaskRejectedException tre) {
wasTre = true;
}
assertTrue("The last task should throw a TaskRejectedException but it wasn't", wasTre);
Thread.sleep(3000);
assertTrue("4 tasks should be terminated, but "+Counters.threadPool.getNb()+" were instead",
Counters.threadPool.getNb()==4);
}
}
class SimpleTask implements Runnable {
private String name;
private Counters counter;
private int sleepTime;
public SimpleTask(String name, Counters counter, int sleepTime) {
this.name = name;
this.counter = counter;
this.sleepTime = sleepTime;
}
@Override
public void run() {
try {
Thread.sleep(this.sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.counter.increment();
System.out.println("Running task '"+this.name+"' in Thread "+Thread.currentThread().getName());
}
@Override
public String toString() {
return "Task {"+this.name+"}";
}
}
enum Counters {
simpleAsyncTask(0),
syncTask(0),
threadPool(0);
private int nb;
public int getNb() {
return this.nb;
}
public synchronized void increment() {
this.nb++;
}
private Counters(int n) {
this.nb = n;
}
}
В прошлом мы могли использовать больше исполнителей (SimpleThreadPoolTaskExecutor, TimerTaskExecutor, это старые 2.x 3.x). Но оба были объявлены устаревшими и заменены собственным исполнителем Java в качестве первого выбора Spring. Взгляните на вывод:
Running task 'SimpleAsyncTask-1' in Thread thread_name_prefix_____1
Running task 'SimpleAsyncTask-2' in Thread thread_name_prefix_____2
Running task 'SimpleAsyncTask-3' in Thread thread_name_prefix_____3
Running task 'SimpleAsyncTask-4' in Thread thread_name_prefix_____4
Running task 'SimpleAsyncTask-5' in Thread thread_name_prefix_____5
Running task 'SimpleAsyncTask-6' in Thread thread_name_prefix_____6
Running task 'SyncTask-1' in Thread main
Running task 'SyncTask-2' in Thread main
Running task 'SyncTask-3' in Thread main
Running task 'SyncTask-4' in Thread main
Running task 'SyncTask-5' in Thread main
Running task 'ThreadPoolTask-2' in Thread ThreadPoolTaskExecutor-2
Running task 'ThreadPoolTask-1' in Thread ThreadPoolTaskExecutor-1
Running task 'ThreadPoolTask-4' in Thread ThreadPoolTaskExecutor-3
Running task 'ThreadPoolTask-3' in Thread ThreadPoolTaskExecutor-2
Из этого мы можем сделать вывод, что первый тест создает новый поток для каждой задачи. Используя разные имена потоков, мы можем увидеть разницу. Второй, синхронный исполнитель, должен выполнять задачу в вызываемом потоке. Здесь вы можете видеть, что «main» — это имя основного потока, который вызывается основным потоком для синхронизации всех задач. В последнем примере задействован пул потоков, который может создать максимум 3 потока. Как видно из результатов, у них есть только 3 создающих потока.
Теперь мы напишем несколько модульных тестов, чтобы посмотреть на реализации @Async и @Scheduled.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:applicationContext-test.xml"})
@WebAppConfiguration
public class AnnotationTest {
@Autowired
private GenericApplicationContext context;
@Test
public void testScheduled() throws InterruptedException {
System.out.println("Start sleeping");
Thread.sleep(6000);
System.out.println("Wake up !");
TestScheduledTask scheduledTask = (TestScheduledTask) context.getBean("testScheduledTask");
/**
* Test fixed delay. It's executed every 6 seconds. The first execution is registered after application's context start.
**/
assertTrue("Scheduled task should be executed 2 times (1 before sleep in this method, 1 after the sleep), but was "+scheduledTask.getFixedDelayCounter(),
scheduledTask.getFixedDelayCounter() == 2);
/**
* Test fixed rate. It's executed every 6 seconds. The first execution is registered after application's context start.
* Unlike fixed delay, a fixed rate configuration executes one task with specified time. For example, it will execute on
* 6 seconds delayed task at 10:30:30, 10:30:36, 10:30:42 and so on - even if the task 10:30:30 taken 30 seconds to
* be terminated. In teh case of fixed delay, if the first task takes 30 seconds, the next will be executed 6 seconds
* after the first one, so the execution flow will be: 10:30:30, 10:31:06, 10:31:12.
**/
assertTrue("Scheduled task should be executed 2 times (1 before sleep in this method, 1 after the sleep), but was "+scheduledTask.getFixedRateCounter(),
scheduledTask.getFixedRateCounter() == 2);
/**
* Test fixed rate with initial delay attribute. The initialDelay attribute is set to 6 seconds. It causes that
* scheduled method is executed 6 seconds after application's context start. In our case, it should be executed
* only once because of previous Thread.sleep(6000) invocation.
**/
assertTrue("Scheduled task should be executed 1 time (0 before sleep in this method, 1 after the sleep), but was "+scheduledTask.getInitialDelayCounter(), scheduledTask.getInitialDelayCounter() == 1);
/**
* Test cron scheduled task. Cron is scheduled to be executed every 6 seconds. It's executed only once,
* so we can deduce that it's not invoked directly before applications
* context start, but only after configured time (6 seconds in our case).
**/
assertTrue("Scheduled task should be executed 1 time (0 before sleep in this method, 1 after the sleep), but was "+scheduledTask.getCronCounter(), scheduledTask.getCronCounter() == 1);
}
@Test
public void testAsyc() throws InterruptedException {
/**
* To test @Async annotation, we can create a bean in-the-fly. AsyncCounter bean is a
* simple counter which value should be equals to 2 at the end of the test. A supplementary test
* concerns threads which execute both of AsyncCounter methods: one which
* isn't annotated with @Async and another one which is annotated with it. For the first one, invoking
* thread should have the same name as the main thread. For annotated method, it can't be executed in
* the main thread. It must be executed asynchrounously in a new thread.
**/
context.registerBeanDefinition("asyncCounter", new RootBeanDefinition(AsyncCounter.class));
String currentName = Thread.currentThread().getName();
AsyncCounter asyncCounter = context.getBean("asyncCounter", AsyncCounter.class);
asyncCounter.incrementNormal();
assertTrue("Thread executing normal increment should be the same as JUnit thread but it wasn't (expected '"+currentName+"', got '"+asyncCounter.getNormalThreadName()+"')",
asyncCounter.getNormalThreadName().equals(currentName));
asyncCounter.incrementAsync();
// sleep 50ms and give some time to AsyncCounter to update asyncThreadName value
Thread.sleep(50);
assertFalse("Thread executing @Async increment shouldn't be the same as JUnit thread but it wasn (JUnit thread '"+currentName+"', @Async thread '"+asyncCounter.getAsyncThreadName()+"')",
asyncCounter.getAsyncThreadName().equals(currentName));
System.out.println("Main thread execution's name: "+currentName);
System.out.println("AsyncCounter normal increment thread execution's name: "+asyncCounter.getNormalThreadName());
System.out.println("AsyncCounter @Async increment thread execution's name: "+asyncCounter.getAsyncThreadName());
assertTrue("Counter should be 2, but was "+asyncCounter.getCounter(), asyncCounter.getCounter()==2);
}
}
class AsyncCounter {
private int counter = 0;
private String normalThreadName;
private String asyncThreadName;
public void incrementNormal() {
normalThreadName = Thread.currentThread().getName();
this.counter++;
}
@Async
public void incrementAsync() {
asyncThreadName = Thread.currentThread().getName();
this.counter++;
}
public String getAsyncThreadName() {
return asyncThreadName;
}
public String getNormalThreadName() {
return normalThreadName;
}
public int getCounter() {
return this.counter;
}
}
Кроме того, нам нужно создать новый файл конфигурации и класс, содержащий метод задачи cron:
<!-- imported configuration file first -->
<!-- Activates various annotations to be detected in bean classes -->
<context:annotation-config />
<!-- Scans the classpath for annotated components that will be auto-registered as Spring beans.
For example @Controller and @Service. Make sure to set the correct base-package-->
<context:component-scan base-package="com.migo.test.schedulers" />
<task:scheduler id="taskScheduler"/>
<task:executor id="taskExecutor" pool-size="40" />
<task:annotation-driven executor="taskExecutor" scheduler="taskScheduler"/>
// scheduled methods after, all are executed every 6 seconds (scheduledAtFixedRate and scheduledAtFixedDelay start to execute at
// application context start, two other methods begin 6 seconds after application's context start)
@Component
public class TestScheduledTask {
private int fixedRateCounter = 0;
private int fixedDelayCounter = 0;
private int initialDelayCounter = 0;
private int cronCounter = 0;
@Scheduled(fixedRate = 6000)
public void scheduledAtFixedRate() {
System.out.println("<R> Increment at fixed rate");
fixedRateCounter++;
}
@Scheduled(fixedDelay = 6000)
public void scheduledAtFixedDelay() {
System.out.println("<D> Incrementing at fixed delay");
fixedDelayCounter++;
}
@Scheduled(fixedDelay = 6000, initialDelay = 6000)
public void scheduledWithInitialDelay() {
System.out.println("<DI> Incrementing with initial delay");
initialDelayCounter++;
}
@Scheduled(cron = "**/6 ** ** ** ** **")
public void scheduledWithCron() {
System.out.println("<C> Incrementing with cron");
cronCounter++;
}
public int getFixedRateCounter() {
return this.fixedRateCounter;
}
public int getFixedDelayCounter() {
return this.fixedDelayCounter;
}
public int getInitialDelayCounter() {
return this.initialDelayCounter;
}
public int getCronCounter() {
return this.cronCounter;
}
}
Вывод этого теста:
<R> Increment at fixed rate
<D> Incrementing at fixed delay
Start sleeping
<C> Incrementing with cron
<DI> Incrementing with initial delay
<R> Increment at fixed rate
<D> Incrementing at fixed delay
Wake up !
Main thread execution's name: main
AsyncCounter normal increment thread execution's name: main
AsyncCounter @Async increment thread execution's name: taskExecutor-1
Эта статья знакомит нас с еще одной интересующей всех особенностью фреймворка Spring — задачами тайминга. Мы видим, что, как и в конфигурации в стиле Linux CRON, эти задачи также могут быть установлены как временные задачи с фиксированной частотой. Мы также демонстрируем на примере, что методы, аннотированные @Async, выполняются в разных потоках.