Spring Boot 異步框架的使用詳解
1. 前言
隨著數(shù)據(jù)量和調(diào)用量的增長,用戶對應(yīng)用的性能要求越來越高。另外,在實(shí)際的服務(wù)中,還存在著這樣的場景:系統(tǒng)在組裝數(shù)據(jù)的時候,對于數(shù)據(jù)的各個部分的獲取實(shí)際上是沒有前后依賴關(guān)系的。這些問題都很容易讓我們想到將這些同步調(diào)用全都改造為異步調(diào)用。不過自己實(shí)現(xiàn)起來比較麻煩,還容易出錯。好在Spring已經(jīng)提供了該問題的解決方案,而且使用起來十分方便。
2.Spring異步執(zhí)行框架的使用方法
2.1 maven 依賴
Spring異步執(zhí)行框架的相關(guān)bean包含在spring-context和spring-aop模塊中,所以只要引入上述的模塊即可。
2.2 開啟異步任務(wù)支持
Spring提供了@EnableAsync的注解來標(biāo)注是否啟用異步任務(wù)支持。使用方式如下:
@Configuration @EnableAsync public class AppConfig { }
Note: @EnableAsync必須要配合@Configuration使用,否則會不生效
2.3 方法標(biāo)記為異步調(diào)用
將同步方法的調(diào)用改為異步調(diào)用也很簡單。對于返回值為void的方法,直接加上@Async注解即可。對于有返回值的方法,除了加上上述的注解外,還需要將方法的返回值修改為Future類型和將返回值用AsyncResult包裝起來。如下所示:
// 無返回值的方法直接加上注解即可。 @Async public void method1() { ... } // 有返回值的方法需要修改返回值。 @Async public Future<Object> method2() { ... return new AsyncResult<>(Object); }
2.4 方法調(diào)用
對于void的方法,和普通的調(diào)用沒有任何區(qū)別。對于非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幫我們生成并管理了一個線程池,然后方法調(diào)用的時候使用動態(tài)代理將方法的執(zhí)行包裝為Callable類型并提交到線程池中執(zhí)行。核心的實(shí)現(xiàn)邏輯在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實(shí)例的bean,有則使用這個bean。
3. 如果1、2中的都不存在,那么Spring就會直接使用默認(rèn)的Executor,即SimpleAsyncTaskExecutor。
在第2節(jié)的實(shí)例中,我們直接使用的是Spring默認(rèn)的TaskExecutor。但是對于每一個新的任務(wù),SimpleAysncTaskExecutor都是直接創(chuàng)建新的線程來執(zhí)行,所以無法重用線程。具體的執(zhí)行的代碼如下:
@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。結(jié)合上面描述的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接口,通過實(shí)現(xiàn)該接口,除了可以實(shí)現(xiàn)自定義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類,該類也實(shí)現(xiàn)了AsyncConfigurer接口,且方法的返回值都是null,旨在提供一個方便的實(shí)現(xiàn)。
當(dāng)getAsyncExecutor()方法返回null的時候,Spring會使用默認(rèn)的處理器(強(qiáng)烈不推薦)。
當(dāng)getAsyncUncaughtExceptionHandler()返回null的時候,Spring會使用SimpleAsyncUncaughtExceptionHandler來處理異常,該類會打印出異常的信息。
所以對該類的使用,最佳的實(shí)踐是繼承該類,并且覆蓋實(shí)現(xiàn)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類型的方法,因?yàn)槿绻椒ㄓ蟹祷刂?,必須要用Future包裝起來),那么會調(diào)用handleUncaughtException方法來處理異常。
注意:在handleUncaughtException()方法中拋出的任何異常,都會被Spring Catch住,所以沒有辦法將void的方法再次拋出并傳播到上層調(diào)用方的!??!
關(guān)于Spring 這個設(shè)計的緣由我的理解是:既然方法的返回值是void,就說明調(diào)用方不關(guān)心方法執(zhí)行是否成功,所以也就沒有必要去處理方法拋出的異常。如果需要關(guān)心異步方法是否成功,那么返回值改為boolean就可以了。
4.4 最佳實(shí)踐的建議
- @Async可以指定方法執(zhí)行的Executor,用法:@Async("MyTaskExecutor")。推薦指定Executor,這樣可以避免因?yàn)镋xecutor配置沒有生效而Spring使用默認(rèn)的Executor的問題。
- 實(shí)現(xiàn)接口AsyncConfigurer的時候,方法getAsyncExecutor()必須要使用@Bean,并指定Bean的name。如果不使用@Bean,那么該方法返回的Executor并不會被Spring管理。用java doc api的原話是:is not a fully managed Spring bean.(具體含義沒有太理解,不過親測不加這個注解無法正常使用)
- 由于其本質(zhì)上還是基于代理實(shí)現(xiàn)的,所以如果一個類中有A、B兩個異步方法,而A中存在對B的調(diào)用,那么調(diào)用A方法的時候,B方法不會去異步執(zhí)行的。
- 在異步方法上標(biāo)注@Transactional是無效的。
- future.get()的時候,最好使用get(long timeout, TimeUnit unit)方法,避免長時間阻塞。
- ListenableFuture和CompletableFuture也是推薦使用的,他們相比Future,提供了對異步調(diào)用的各個階段或過程進(jìn)行介入的能力。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 詳解Spring/Spring boot異步任務(wù)編程WebAsyncTask
- 簡述Springboot @Async 異步方法
- Spring Boot @Async 異步任務(wù)執(zhí)行方法
- Spring Boot利用@Async如何實(shí)現(xiàn)異步調(diào)用:自定義線程池
- 詳解Spring Boot 異步執(zhí)行方法
- Spring Boot集成教程之異步調(diào)用Async
- spring boot異步(Async)任務(wù)調(diào)度實(shí)現(xiàn)方法
- spring boot 使用@Async實(shí)現(xiàn)異步調(diào)用方法
- spring boot中使用@Async實(shí)現(xiàn)異步調(diào)用任務(wù)
相關(guān)文章
SpringBoot使用Validation校驗(yàn)參數(shù)的詳細(xì)過程
這篇文章主要介紹了SpringBoot使用Validation校驗(yàn)參數(shù),本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-09-09Java實(shí)現(xiàn)隨機(jī)出題,10道10以內(nèi)加減法計算代碼實(shí)例
這篇文章主要介紹了Java實(shí)現(xiàn)隨機(jī)出題,10道10以內(nèi)加減法計算,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04Alibaba?Nacos配置中心動態(tài)感知原理示例解析
這篇文章主要介紹了Alibaba?Nacos配置中心動態(tài)感知原理示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08SpringBoot重寫addResourceHandlers映射文件路徑方式
這篇文章主要介紹了SpringBoot重寫addResourceHandlers映射文件路徑方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02spring學(xué)習(xí)JdbcTemplate數(shù)據(jù)庫事務(wù)管理
這篇文章主要為大家介紹了spring學(xué)習(xí)JdbcTemplate數(shù)據(jù)庫事務(wù)管理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05