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