性能提升!@Async與CompletableFuture優雅應用
1. 簡介
@Async 和 CompletableFuture 是實現異步處理的強大工具組合。@Async 是Spring框架提供的一個注解,用于標記方法以表明它將在Spring管理的線程池中的另一個線程上異步執行。這使得開發人員能夠在不阻塞主線程的情況下執行耗時的任務,從而提高應用程序的整體性能和響應速度。
CompletableFuture 是Java 8引入的一個強大的類,它代表了一個可能尚未完成的計算的結果。CompletableFuture 提供了豐富的API來支持異步編程模式,如回調、組合操作、錯誤處理等。通過將@Async與CompletableFuture結合使用,可以實現更高效的異步任務處理。
接下來,我們將介紹@Async與CompletableFuture結合的使用。
2. 實戰案例
2.1 @EnableAsync and @Async
Spring 自帶 @EnableAsync 注解,可應用于 @Configuration 類以實現異步行為。@EnableAsync 注解會查找標有 @Async 注解的方法,并在后臺線程池中運行這些方法。
@Async 注解方法在單獨的線程中執行,并返回 CompletableFuture 來保存異步計算的結果。
開啟異步功能
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
int core = Runtime.getRuntime().availableProcessors() ;
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(core) ;
executor.setMaxPoolSize(core) ;
executor.setQueueCapacity(100) ;
executor.setThreadNamePrefix("PackAsync-") ;
executor.initialize() ;
return executor ;
}
}
如上我們自定義了線程池,該線程池用來執行我們的異步任務。你也可以不用配置,使用系統默認的線程池。
創建異步任務
@Async("asyncExecutor")
public CompletableFuture<EmployeeNames> task() {
// TODO
}
用 @Async 對方法進行注解,該方法應異步運行。該方法必須是公共的,可以返回值,也可以不返回值。如果返回值,則應使用 Future 接口實現對其進行封裝。
這里指定了使用我們自定義的線程池執行異步任務。
多個異步任務同時執行
CompletableFuture.allOf(
asyncMethodOne,
asyncMethodTwo,
asyncMethodThree
).join() ;
要合并多個異步任務的結果,通過使用 join() 方法,這將等待所有異步任務執行完成才會繼續往后執行。
2.2 Rest Controller中調用異步任務
接下來,我們將創建一個 REST API,從三個遠程服務異步獲取數據,當所有三個服務的響應都可用時,再匯總響應。
- 調用/addresses接口獲取所有地址信息
- 調用/phones接口獲取所有電話數據
- 調用/names接口獲取所有姓名
- 等待以上3個接口都返回結果后再進行處理
- 匯總所有三個應用程序接口的響應,并生成最終響應發送回客戶端
遠程接口準備
@RestController
public class EmployeeController {
@GetMapping("/addresses")
public EmployeeAddresses addresses() {
// TODO
}
@GetMapping("/phones")
public EmployeePhone phones() {
// TODO
}
@GetMapping("/names")
public EmployeeNames names() {
// TODO
}
}
我們將通過異步的方式調用上面定義的3個接口。
異步調用REST API
這些服務方法將從遠程應用程序接口或數據庫中提取數據,必須在不同的線程中并行運行,以加快處理速度。
@Service
public class AsyncService {
private static Logger logger = LoggerFactory.getLogger(AsyncService.class);
private final RestTemplate restTemplate;
public AsyncService(RestTemplate restTemplate) {
this.restTemplate = restTemplate ;
}
@Async("asyncExecutor")
public CompletableFuture<EmployeeNames> names() {
logger.info("getEmployeeName starts");
EmployeeNames employeeNameData = restTemplate.getForObject("http://localhost:8080/names", EmployeeNames.class) ;
logger.info("employeeNameData, {}", employeeNameData) ;
logger.info("employeeNameData completed");
return CompletableFuture.completedFuture(employeeNameData);
}
@Async("asyncExecutor")
public CompletableFuture<EmployeeAddresses> addresses() {
logger.info("getEmployeeAddress starts");
EmployeeAddresses employeeAddressData = restTemplate.getForObject("http://localhost:8080/addresses", EmployeeAddresses.class);
logger.info("employeeAddressData, {}", employeeAddressData) ;
logger.info("employeeAddressData completed");
return CompletableFuture.completedFuture(employeeAddressData);
}
@Async("asyncExecutor")
public CompletableFuture<EmployeePhone> phones() {
logger.info("getEmployeePhone starts") ;
EmployeePhone employeePhoneData = restTemplate.getForObject("http://localhost:8080/phones", EmployeePhone.class) ;
logger.info("employeePhoneData, {}", employeePhoneData) ;
logger.info("employeePhoneData completed") ;
return CompletableFuture.completedFuture(employeePhoneData) ;
}
}
注意:你可不能如下方式來執行遠程接口的調用。
CompletableFuture.supplyAsync(() -> {
return restTemplate.getForObject("http://localhost:8080/phones", EmployeePhone.class) ;
}) ;
如果你這樣寫,你的遠程接口并非在你的異步線程中執行,而是在CompletableFuturue的線程池中執行(ForkJoinPool)。
2.3 聚合異步任務
接下來在REST API中調用上面的異步方法、消耗和聚合其響應并返回客戶端。
@RestController
public class AsyncController {
private final AsyncService asyncService;
public AsyncController(AsyncService service) {
this.asyncService = asyncService ;
}
@GettMapping("/profile/infos")
public EmployeeDTO infos() throws Exception {
CompletableFuture<EmployeeAddresses> addresses = asyncService.addresses() ;
CompletableFuture<EmployeeNames> names = asyncService.names() ;
CompletableFuture<EmployeePhone> phones = asyncService.phones() ;
// 等待所有異步任務都執行完成
CompletableFuture.allOf(addresses, names, phones).join() ;
return new EmployeeDTO(addresses.get(), names.get(), phones.get()) ;
}
}
整個請求的耗時將會是請求最長REST API調用所用的時間,這大大提升該接口的性能。
2.4 異常處理
當方法的返回類型是 Future 時,Future.get() 方法會拋出異常,我們應該在聚合結果之前使用 try-catch 塊捕獲并處理異常。
問題是,如果異步方法不返回任何值,那么就很難知道方法執行時是否發生了異常。我們可以使用 AsyncUncaughtExceptionHandler 實現來捕獲和處理此類異常。
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler() ;
}
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(AsyncExceptionHandler.class);
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
logger.error("Unexpected asynchronous exception at : "
+ method.getDeclaringClass().getName() + "." + method.getName(), ex);
}
}
}