Spring Boot 非同步框架的使用

JavaDog發表於2019-01-17

1. 前言

隨著資料量和呼叫量的增長,使用者對應用的效能要求越來越高。另外,在實際的服務中,還存在著這樣的場景:系統在組裝資料的時候,對於資料的各個部分的獲取實際上是沒有前後依賴關係的。這些問題都很容易讓我們想到將這些同步呼叫全都改造為非同步呼叫。不過自己實現起來比較麻煩,還容易出錯。好在Spring已經提供了該問題的解決方案,而且使用起來十分方便。

2.Spring非同步執行框架的使用方法

2.1 maven 依賴

Spring非同步執行框架的相關bean包含在spring-context和spring-aop模組中,所以只要引入上述的模組即可。

2.2 開啟非同步任務支援

Spring提供了@EnableAsync的註解來標註是否啟用非同步任務支援。使用方式如下:

@Configuration
@EnableAsync
public class AppConfig {
}
複製程式碼

Note: @EnableAsync必須要配合@Configuration使用,否則會不生效

2.3 方法標記為非同步呼叫

將同步方法的呼叫改為非同步呼叫也很簡單。對於返回值為void的方法,直接加上@Async註解即可。對於有返回值的方法,除了加上上述的註解外,還需要將方法的返回值修改為Future型別和將返回值用AsyncResult包裝起來。如下所示:

// 無返回值的方法直接加上註解即可。 
@Async
public void method1() {
  ...
}

// 有返回值的方法需要修改返回值。
@Async
public Future<Object> method2() {
  ...
  return new AsyncResult<>(Object);
}    
複製程式碼

2.4 方法呼叫

對於void的方法,和普通的呼叫沒有任何區別。對於非void的方法,由於返回值是Future型別,所以需要用get()方法來獲取返回值。如下所示:

public static void main(String[] args) {
    service.method1();
    Future<Object> futureResult = service.method2();
    Object result;
    try {
          result = futureResult.get(); 
        } catch (InterruptedException | ExecutionException e) {
           ...
        }
}

複製程式碼

3. 原理簡介

這塊的原始碼的邏輯還是比較簡單的,主要是Spring幫我們生成並管理了一個執行緒池,然後方法呼叫的時候使用動態代理將方法的執行包裝為Callable型別並提交到執行緒池中執行。核心的實現邏輯在AsyncExecutionInterceptor類的invoke()方法中。如下所示:

@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
    Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);
    final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);

    AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
    if (executor == null) {
        throw new IllegalStateException(
                "No executor specified and no default executor set on AsyncExecutionInterceptor either");
    }

    Callable<Object> task = new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            try {
                Object result = invocation.proceed();
                if (result instanceof Future) {
                    return ((Future<?>) result).get();
                }
            }
            catch (ExecutionException ex) {
                handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments());
            }
            catch (Throwable ex) {
                handleError(ex, userDeclaredMethod, invocation.getArguments());
            }
            return null;
        }
    };

    return doSubmit(task, executor, invocation.getMethod().getReturnType());
}
複製程式碼

4.自定義taskExecutor及異常處理

4.1自定義taskExecutor

Spring查詢TaskExecutor邏輯是:

1. 如果Spring context中存在唯一的TaskExecutor bean,那麼就使用這個bean。
2. 如果1中的bean不存在,那麼就會查詢是否存在一個beanName為taskExecutor且是java.util.concurrent.Executor例項的bean,有則使用這個bean。
3. 如果1、2中的都不存在,那麼Spring就會直接使用預設的Executor,即SimpleAsyncTaskExecutor。
複製程式碼

在第2節的例項中,我們直接使用的是Spring預設的TaskExecutor。但是對於每一個新的任務,SimpleAysncTaskExecutor都是直接建立新的執行緒來執行,所以無法重用執行緒。具體的執行的程式碼如下:

@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);
    }
}

protected void doExecute(Runnable task) {
    Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
    thread.start();
} 
複製程式碼

所以我們在使用的時候,最好是使用自定義的TaskExecutor。結合上面描述的Spring查詢TaskExecutor的邏輯,最簡單的自定義的方法是使用@Bean註解。示例如下:

// ThreadPoolTaskExecutor的配置基本等同於執行緒池
@Bean("taskExecutor")
public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
    taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
    taskExecutor.setQueueCapacity(CORE_POOL_SIZE * 10);
    taskExecutor.setThreadNamePrefix("wssys-async-task-thread-pool");
    taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
    taskExecutor.setAwaitTerminationSeconds(60 * 10);
    taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
    return taskExecutor;
}
複製程式碼

另外,Spring還提供了一個AsyncConfigurer介面,通過實現該介面,除了可以實現自定義Executor以外,還可以自定義異常的處理。程式碼如下:

@Configuration
@Slf4j
public class AsyncConfig implements AsyncConfigurer {

    private static final int MAX_POOL_SIZE = 50;

    private static final int CORE_POOL_SIZE = 20;

    @Override
    @Bean("taskExecutor")
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();

        taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
        taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
        taskExecutor.setQueueCapacity(CORE_POOL_SIZE * 10);
        taskExecutor.setThreadNamePrefix("async-task-thread-pool");
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        taskExecutor.setAwaitTerminationSeconds(60 * 10);
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        return taskExecutor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> log.error("invoke async method occurs error. method: {}, params: {}",
            method.getName(), JSON.toJSONString(params), ex);
    }

}
複製程式碼

Note:
Spring還提供了一個AsyncConfigurerSupport類,該類也實現了AsyncConfigurer介面,且方法的返回值都是null,旨在提供一個方便的實現。
當getAsyncExecutor()方法返回null的時候,Spring會使用預設的處理器(強烈不推薦)。
當getAsyncUncaughtExceptionHandler()返回null的時候,Spring會使用SimpleAsyncUncaughtExceptionHandler來處理異常,該類會列印出異常的資訊。
所以對該類的使用,最佳的實踐是繼承該類,並且覆蓋實現getAsyncExecutor()方法。

4.2 異常處理

Spring非同步框架對異常的處理如下所示:

// 所在類:AsyncExecutionAspectSupport
protected void handleError(Throwable ex, Method method, Object... params) throws Exception {
    if (Future.class.isAssignableFrom(method.getReturnType())) {
        ReflectionUtils.rethrowException(ex);
    }
    else {
        // Could not transmit the exception to the caller with default executor
        try {
            this.exceptionHandler.handleUncaughtException(ex, method, params);
        }
        catch (Throwable ex2) {
            logger.error("Exception handler for async method '" + method.toGenericString() +
                    "' threw unexpected exception itself", ex2);
        }
    }
}
複製程式碼

從程式碼來看,如果返回值是Future型別,那麼直接將異常丟擲。如果返回值不是Future型別(基本上包含的是所有返回值void型別的方法,因為如果方法有返回值,必須要用Future包裝起來),那麼會呼叫handleUncaughtException方法來處理異常。

注意:在handleUncaughtException()方法中丟擲的任何異常,都會被Spring Catch住,所以沒有辦法將void的方法再次丟擲並傳播到上層呼叫方的!!!

關於Spring 這個設計的緣由我的理解是:既然方法的返回值是void,就說明呼叫方不關心方法執行是否成功,所以也就沒有必要去處理方法丟擲的異常。如果需要關心非同步方法是否成功,那麼返回值改為boolean就可以了。

4.4 最佳實踐的建議

  1. @Async可以指定方法執行的Executor,用法:@Async("MyTaskExecutor")。推薦指定Executor,這樣可以避免因為Executor配置沒有生效而Spring使用預設的Executor的問題。
  2. 實現介面AsyncConfigurer的時候,方法getAsyncExecutor()必須要使用@Bean,並指定Bean的name。如果不使用@Bean,那麼該方法返回的Executor並不會被Spring管理。用java doc api的原話是:is not a fully managed Spring bean.(具體含義沒有太理解,不過親測不加這個註解無法正常使用)
  3. 由於其本質上還是基於代理實現的,所以如果一個類中有A、B兩個非同步方法,而A中存在對B的呼叫,那麼呼叫A方法的時候,B方法不會去非同步執行的。
  4. 在非同步方法上標註@Transactional是無效的。
  5. future.get()的時候,最好使用get(long timeout, TimeUnit unit)方法,避免長時間阻塞。
  6. ListenableFuture和CompletableFuture也是推薦使用的,他們相比Future,提供了對非同步呼叫的各個階段或過程進行介入的能力。

參考:
1.Spring Boot EnableAsync api doc
2.Spring Task Execution and Scheduling
3.www.atatech.org/articles/11…

碼字不易,如有建議請掃碼Spring Boot 非同步框架的使用


相關文章