Spring5原始碼解析-Spring中的非同步事件

知秋z發表於2017-09-28

上一篇 Spring框架中的事件和監聽器並未對Spring框架中的非同步事件涉及太多,所以本篇是對其一個補充。

同步事件有一個主要缺點:它們在所呼叫執行緒的本地執行(也就是將所呼叫執行緒看成主執行緒的話,就是在主執行緒裡依次執行)。如果監聽器處理同步事件需要5秒鐘的響應,則最終結果是使用者將在至少5秒內無法看到響應(可以通過Spring框架中的事件和監聽器中的例子瞭解具體)。所以,我們可以通過一個替代方案來解決這個問題 - 非同步事件。

接下來也就是介紹Spring框架中的非同步事件。老規矩,第一部分深入框架原始碼,將描述主要組成部分以及它們如何一起協作的。在第二部分,我們將編寫一些測試用例來檢查非同步事件的執行情況。

Spring中的非同步事件

在Spring中處理非同步事件是基於本地的Java併發解決方案---任務執行器(可以瞭解下Java Executor框架的內容)。事件由multicastEvent 方法排程。它通過使用java.util.concurrent.Executor介面的實現將事件傳送到專用的監聽器。Multicaster會呼叫同步執行器,因為它是預設實現,這點在Spring框架中的事件和監聽器有明確的例子,從原始碼的角度也就是是否設定有SyncTaskExecutor例項。從public void setTaskExecutor(@Nullable Executor taskExecutor)其中,@Nullable 可看出Executor引數可為null,預設不設定的話,multicastEvent也就直接 跳過非同步執行了

org.springframework.context.event.SimpleApplicationEventMulticaster

@Override
    public void multicastEvent(ApplicationEvent event) {
        multicastEvent(event, resolveDefaultEventType(event));
    }

    @Override
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
        for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
            Executor executor = getTaskExecutor();
            if (executor != null) {
                executor.execute(() -> invokeListener(listener, event));
            }
            else {
                invokeListener(listener, event);
            }
        }
    }

    private ResolvableType resolveDefaultEventType(ApplicationEvent event) {
        return ResolvableType.forInstance(event);
    }

    /**
     * Set a custom executor (typically a {@link     org.springframework.core.task.TaskExecutor})
     * to invoke each listener with.
     * <p>Default is equivalent to {@link org.springframework.core.task.SyncTaskExecutor},
     * executing all listeners synchronously in the calling thread.
     * <p>Consider specifying an asynchronous task executor here to not block the
     * caller until all listeners have been executed. However, note that asynchronous
     * execution will not participate in the caller's thread context (class loader,
     * transaction association) unless the TaskExecutor explicitly supports this.
     * @see org.springframework.core.task.SyncTaskExecutor
     * @see org.springframework.core.task.SimpleAsyncTaskExecutor
     * @Nullable 可看出Executor引數可為null,預設不設定的話,上面multicastEvent也就直接      * 跳過非同步執行了
     */
    public void setTaskExecutor(@Nullable Executor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    /**
     * Return the current task executor for this multicaster.
     */
    @Nullable
    protected Executor getTaskExecutor() {
        return this.taskExecutor;
    }複製程式碼

非同步執行器的實現可以參考org.springframework.core.task.SimpleAsyncTaskExecutor。這個類為每個提交的任務建立新的執行緒。然而,它不會重用執行緒,所以如果我們有很多長執行時間的非同步任務需要來處理的時候,執行緒建立的風險就會變得太大了,會佔用大量的資源,不光是cpu還包括jvm。具體原始碼如下:

    /**
     * Executes the given task, within a concurrency throttle
     * if configured (through the superclass's settings).
     * @see #doExecute(Runnable)
     */
    @Override
    public void execute(Runnable task) {
        execute(task, TIMEOUT_INDEFINITE);
    }

    /**
     * Executes the given task, within a concurrency throttle
     * if configured (through the superclass's settings).
     * <p>Executes urgent tasks (with 'immediate' timeout) directly,
     * bypassing the concurrency throttle (if active). All other
     * tasks are subject to throttling.
     * @see #TIMEOUT_IMMEDIATE
     * @see #doExecute(Runnable)
     */
    @Override
    public void execute(Runnable task, long startTimeout) {
        Assert.notNull(task, "Runnable must not be null");
        Runnable taskToUse = (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task);
        if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) {
            this.concurrencyThrottle.beforeAccess();
            doExecute(new ConcurrencyThrottlingRunnable(taskToUse));
        }
        else {
            doExecute(taskToUse);
        }
    }

    @Override
    public Future<?> submit(Runnable task) {
          //建立
        FutureTask<Object> future = new FutureTask<>(task, null);
          //執行
        execute(future, TIMEOUT_INDEFINITE);
        return future;
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        FutureTask<T> future = new FutureTask<>(task);
        execute(future, TIMEOUT_INDEFINITE);
        return future;
    }
    /**
     * Template method for the actual execution of a task.
     * <p>The default implementation creates a new Thread and starts it.
     * @param task the Runnable to execute
     * @see #setThreadFactory
     * @see #createThread
     * @see java.lang.Thread#start()
     */
    protected void doExecute(Runnable task) {
        Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
      //可以看出,執行也只是簡單的將建立的執行緒start執行下,別提什麼重用了
        thread.start();
    }複製程式碼

為了從執行緒池功能中受益,我們可以使用另一個Spring的Executor實現,org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor。類如其名,這個Executor允許我們使用執行緒池。關於執行緒池的原始碼,請期待我的Java9的書籍,裡面會涉及到這裡面的細節分析,也可以參考其他部落格的博文(哈哈,我就是打個小廣告而已)。

org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor

    /**
     * Return the underlying ThreadPoolExecutor for native access.
     * @return the underlying ThreadPoolExecutor (never {@code null})
     * @throws IllegalStateException if the ThreadPoolTaskExecutor hasn't been initialized yet
     */
    public ThreadPoolExecutor getThreadPoolExecutor() throws IllegalStateException {
        Assert.state(this.threadPoolExecutor != null, "ThreadPoolTaskExecutor not initialized");
        return this.threadPoolExecutor;
    }

@Override
    public void execute(Runnable task) {
        Executor executor = getThreadPoolExecutor();
        try {
            executor.execute(task);
        }
        catch (RejectedExecutionException ex) {
            throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
        }
    }

    @Override
    public void execute(Runnable task, long startTimeout) {
        execute(task);
    }

    @Override
    public Future<?> submit(Runnable task) {
        ExecutorService executor = getThreadPoolExecutor();
        try {
            return executor.submit(task);
        }
        catch (RejectedExecutionException ex) {
            throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
        }
    }複製程式碼

寫一個Spring中非同步事件的例子

我們來編寫一個能夠同時處理同步和非同步事件的multicaster。同步事件將使用本地同步排程程式進行排程(SyncTaskExecutor),非同步使用Spring的ThreadPoolTaskExecutor實現。

/**
 * 下面的註釋意思很明顯了,不多說了
 * {@link TaskExecutor} implementation that executes each task <i>synchronously</i>
 * in the calling thread.
 *
 * <p>Mainly intended for testing scenarios.
 *
 * <p>Execution in the calling thread does have the advantage of participating
 * in it's thread context, for example the thread context class loader or the
 * thread's current transaction association. That said, in many cases,
 * asynchronous execution will be preferable: choose an asynchronous
 * {@code TaskExecutor} instead for such scenarios.
 *
 * @author Juergen Hoeller
 * @since 2.0
 * @see SimpleAsyncTaskExecutor
 */
@SuppressWarnings("serial")
public class SyncTaskExecutor implements TaskExecutor, Serializable {

    /**
     * Executes the given {@code task} synchronously, through direct
     * invocation of it's {@link Runnable#run() run()} method.
     * @throws IllegalArgumentException if the given {@code task} is {@code null}
     */
    @Override
    public void execute(Runnable task) {
        Assert.notNull(task, "Runnable must not be null");
        task.run();
    }

}複製程式碼

首先,我們需要為我們的測試用例新增一些bean:

<bean id="syncTaskExecutor" class="org.springframework.core.task.SyncTaskExecutor" />
<bean id="asyncTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
  <!-- 10 task will be submitted immediately -->
  <property name="corePoolSize" value="10" />
  <!-- If 10 task are already submitted and treated, we allow to enlarge pool capacity to 15 (10 from core pool size + 5 from max pool size) -->
  <property name="maxPoolSize" value="15" />
  <!-- Number of tasks that can be placed into waiting queue -->
  <property name="queueCapacity" value="10" />
</bean>

<bean id="applicationEventMulticaster" class="com.migo.event.SimpleEventMulticaster">
  <property name="taskExecutor" ref="syncTaskExecutor" />
  <property name="asyncTaskExecutor" ref="asyncTaskExecutor" />
</bean>
<bean id="taskStatsHolder" class="com.migo.event.TaskStatsHolder" />複製程式碼

用於測試任務執行結果的兩個類:

// TaskStatsHolder.java
/****
 ** Holder bean for all executed tasks.
 **/
public class TaskStatsHolder {

  private Map<String, TaskStatData> tasks = new HashMap<String, TaskStatData>();

  public void addNewTaskStatHolder(String key, TaskStatData value) {
    tasks.put(key, value);
  }

  public TaskStatData getTaskStatHolder(String key) {
    return tasks.get(key);
  }
}

// TaskStatData.java
/****
 ** Holder class for all statistic data about already executed tasks.
 **/
public class TaskStatData {

    private String threadName;
    private int executionTime;
    private long startTime;
    private long endTime;

    public TaskStatData(String threadName, long startTime, long endTime) {
      this.threadName = threadName;
      this.startTime = startTime;
      this.endTime = endTime;
      this.executionTime = Math.round((endTime - startTime) / 1000);
    }

    public String getThreadName() {
      return threadName;
    }
    public int getExecutionTime() {
      return this.executionTime;
    }
    public long getStartTime() {
      return this.startTime;
    }
    public long getEndTime() {
      return this.endTime;
    }

    @Override
    public String toString() {
      StringBuilder result = new StringBuilder();
      result.append("TaskStatData {thread name: ").append(this.threadName).append(", start time: ").append(new Date(this.startTime));
      result.append(", end time: ").append(new Date(this.endTime)).append(", execution time: ").append(this.executionTime).append(" seconds}");
      return result.toString();
    }

}複製程式碼

如上程式碼所示,這些都是簡單物件。我們會使用這些物件來檢查我們的假設和執行結果是否相匹配。兩個要分發的事件也很簡單:

// ProductChangeFailureEvent.java
/**
 * This is synchronous event dispatched when one product is modified in the backoffice. 
 * When product's modification fails (database, validation problem), this event is dispatched to
 * all listeners. It's synchronous because we want to inform the user that some actions were done 
 * after the failure. Otherwise (asynchronous character of event) we shouldn't be able to
 * know if something was done or not after the dispatch.
 **/
public class ProductChangeFailureEvent extends ApplicationContextEvent {

  private static final long serialVersionUID = -1681426286796814792L;
  public static final String TASK_KEY = "ProductChangeFailureEvent";

  public ProductChangeFailureEvent(ApplicationContext source) {
    super(source);
  }
}

// NotifMailDispatchEvent.java
/**
 * Event dispatched asynchronously every time when we want to send a notification mail. 
 * Notification mails to send should be stored somewhere (filesystem, database...) but in
 * our case, we'll handle only one notification mail: when one product out-of-stock becomes available again.
 **/
public class NotifMailDispatchEvent extends ApplicationContextEvent implements AsyncApplicationEvent {

  private static final long serialVersionUID = 9202282810553100778L;
  public static final String TASK_KEY = "NotifMailDispatchEvent";

  public NotifMailDispatchEvent(ApplicationContext source) {
    super(source);
  }
}複製程式碼

而用於處理相應排程事件的監聽器也只需要將資料放入TaskStatsHolder例項類中即可:

// ProductChangeFailureListener.java
@Component
public class ProductChangeFailureListener 
    implements ApplicationListener<ProductChangeFailureEvent>{

  @Override
  public void onApplicationEvent(ProductChangeFailureEvent event) {
    long start = System.currentTimeMillis();
    long end = System.currentTimeMillis();
    ((TaskStatsHolder) event.getApplicationContext().getBean("taskStatsHolder")).addNewTaskStatHolder(ProductChangeFailureEvent.TASK_KEY, new TaskStatData(Thread.currentThread().getName(), start, end));
  }

}

// NotifMailDispatchListener.java
@Component
public class NotifMailDispatchListener 
    implements ApplicationListener<NotifMailDispatchEvent>{

  @Override
  public void onApplicationEvent(NotifMailDispatchEvent event) throws InterruptedException {
    long start = System.currentTimeMillis();
    // sleep 5 seconds to avoid that two listeners execute at the same moment
    Thread.sleep(5000);
    long end = System.currentTimeMillis();
    ((TaskStatsHolder) event.getApplicationContext().getBean("taskStatsHolder")).addNewTaskStatHolder(NotifMailDispatchEvent.TASK_KEY, new TaskStatData(Thread.currentThread().getName(), start, end));
  }
}複製程式碼

用於測試的controller如下所示:

@Controller
public class ProductController {

  @Autowired
  private ApplicationContext context;

  @RequestMapping(value = "/products/change-failure")
  public String changeFailure() {
    try {
      System.out.println("I'm modifying the product but a NullPointerException will be thrown");
      String name = null;
      if (name.isEmpty()) {
        // show error message here
        throw new RuntimeException("NullPointerException");
      }
    } catch (Exception e) {
            context.publishEvent(new ProductChangeFailureEvent(context));
    }
    return "success";
  }


  @RequestMapping(value = "/products/change-success")
  public String changeSuccess() {
    System.out.println("Product was correctly changed");
    context.publishEvent(new NotifMailDispatchEvent(context));
    return "success";
  }
}複製程式碼

最後,測試用例:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:applicationContext-test.xml"})
@WebAppConfiguration
public class SpringSyncAsyncEventsTest {

  @Autowired
  private WebApplicationContext wac;

  @Test
  public void test() {
    MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    // execute both urls simultaneously
    mockMvc.perform(get("/products/change-success"));
    mockMvc.perform(get("/products/change-failure"));

    // get stats holder and check if both stats are available:
    // - mail dispatching shouldn't be available because it's executed after a sleep of 5 seconds
    // - product failure should be available because it's executed synchronously, almost immediately (no operations in listeners)
    TaskStatsHolder statsHolder = (TaskStatsHolder) this.wac.getBean("taskStatsHolder");
    TaskStatData mailStatData = statsHolder.getTaskStatHolder(NotifMailDispatchEvent.TASK_KEY);
    TaskStatData productFailureData = statsHolder.getTaskStatHolder(ProductChangeFailureEvent.TASK_KEY);
    assertTrue("Task for mail dispatching is executed after 5 seconds, so at this moment, it taskStatsHolder shouldn't contain it", 
        mailStatData == null);
    assertTrue("productFailureHolder shouldn't be null but it is", 
        productFailureData != null);
    assertTrue("Product failure listener should be executed within 0 seconds but took "+productFailureData.getExecutionTime()+" seconds", 
        productFailureData.getExecutionTime() == 0);
    while (mailStatData == null) {
        mailStatData = statsHolder.getTaskStatHolder(NotifMailDispatchEvent.TASK_KEY);
    }

    // check mail dispatching stats again, when available
    assertTrue("Now task for mail dispatching should be at completed state", 
        mailStatData != null);
    assertTrue("Task for mail dispatching should take 5 seconds but it took "+mailStatData.getExecutionTime()+" seconds", 
        mailStatData.getExecutionTime() == 5);
    assertTrue("productFailureHolder shouldn't be null but it is", 
        productFailureData != null);
    assertTrue("Product failure listener should be executed within 0 seconds but took "+productFailureData.getExecutionTime()+" seconds", 
        productFailureData.getExecutionTime() == 0);
    assertTrue("Thread executing mail dispatch and product failure listeners shouldn't be the same", 
        !productFailureData.getThreadName().equals(mailStatData.getThreadName()));
    assertTrue("Thread executing product failure listener ("+productFailureData.getThreadName()+") should be the same as current thread ("+Thread.currentThread().getName()+") but it wasn't", 
        Thread.currentThread().getName().equals(productFailureData.getThreadName()));
    assertTrue("Thread executing mail dispatch listener ("+mailStatData.getThreadName()+") shouldn't be the same as current thread ("+Thread.currentThread().getName()+") but it was", 
        !Thread.currentThread().getName().equals(mailStatData.getThreadName()));
    // make some output to see the informations about tasks
    System.out.println("Data about mail notif dispatching event: "+mailStatData);
    System.out.println("Data about product failure dispatching event: "+productFailureData);
  }
}複製程式碼

因之前整理的筆記此處SimpleEventMulticaster忘了放進去,也懶得去找了,可以通過xml定義去檢視下,這個測試用例可以看出兩個listener不是由同一個executor啟動的,Product failure 監聽器由同步執行器執行。因為他們沒有做任何操作,幾乎立即返回結果。關於郵件排程事件,通過休眠5秒可以得到其執行時間超過Product failure 監聽器的執行時間。通過分析輸出可以知道,兩者在不同的執行緒中執行,所以由不同的執行器執行(關於這倆執行器的例子可以再搜下相關博文,其實主要還是想表達SyncTaskExecutor是在主執行緒裡執行,而asyncTaskExecutor由執行緒池裡管理的執行緒執行)。

Product was correctly changed
I'm modifying the product but a NullPointerException will be thrown
Data about mail notif dispatching event: TaskStatData {thread name: asyncTaskExecutor-1(非同步執行緒), start time: Thu Jun 19 21:14:18 CEST 2016, end time: Thu Jun 19 21:14:23 CEST 2016, execution time: 5 seconds}
Data about product failure dispatching event: TaskStatData {thread name: main(主執行緒), start time: Thu Jun 19 21:14:21 CEST 2016, end time: Thu Jun 19 21:14:21 CEST 2016, execution time: 0 seconds}複製程式碼

本文簡單介紹瞭如何在Spring中處理非同步事件。當監聽器需要執行很長時間,而我們又不想阻塞應用程式執行,就可以使用非同步執行。非同步執行可以通過非同步執行器(如ThreadPoolTaskExecutor或SimpleAsyncTaskExecutor)實現。

相關文章