SpringBoot Controller异步调用接口实现@Async
背景:
后端根据前端传的参数,收集所有符合参数的日志并打印为csv,数据量为10W
因此实现要求:接口异步,不阻塞其他功能的调用
原计划打算另起线程处理,用消息队列传递消息给对应的监听进程,参考:由设计模式引起的多线程思考
后来查到SpringBoot有自带的解决方案@Async,其调用时controller层接口实测能够完成异步调用。
参考:
https://www.jianshu.com/p/2d4b89c7a3f1
https://www.cnblogs.com/huanzi-qch/p/11231041.html
整体流程 + 建立多个线程池并制定其中一个 + service执行方法有无返回值的处理 + 异常处理:
https://www.cnblogs.com/lshan/p/10875372.html
异步处理方法的参考:
https://blog.csdn.net/hry2015/article/details/67640534
结合@Async实现的进度实时展示的demo参考:
https://www.cnblogs.com/yjmyzz/p/how-to-use-Async-annotation-in-spring-mvc.html
具体实现:
线程池配置:
@Configuration
@EnableAsync
public class AsyncConfig {
private static final ExtLogger LOGGER = ExtLogger.create(AsyncConfig.class);
public static final String LOG_EXPORT_EXECUTOR_NAME = "logExportExecutor";
/**
* Operation Log export executor.
*
* @return the executor
*/
@Bean(LOG_EXPORT_EXECUTOR_NAME)
public Executor logExportExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置线程初始化数量
executor.setCorePoolSize(1);
// 设置线程池最大数量
executor.setMaxPoolSize(1);
// 设置等待队列的大小
executor.setQueueCapacity(100);
// 设置线程名称前缀
executor.setThreadNamePrefix("exportThread");
executor.setRejectedExecutionHandler((Runnable r, ThreadPoolExecutor exe) -> {
LOGGER.error("Failed to get new Thread");
});
executor.initialize();
return new DelegatingSecurityContextExecutor(executor);
}
}
return new DelegatingSecurityContextExecutor(executor);
SpringBoot自带的线程池,目的在于拿到启用新线程时能够拿到父线程的认证有关的信息
guiApplication加入async扫描:@EnableAsync
Service层演示代码:
@Async(AsyncConfig.LOG_EXPORT_EXECUTOR_NAME)
public Future<StreamingResponseBody> exportLog() throws Exception {
long currentTimeMillis = System.currentTimeMillis();
StreamingResponseBody str = dataService.download(FileUtil.fileUrl(ExternalConfig.INSTANCE.EXPORT_LOG_SAVE_PATH,
LoginContext.getCurrentUsername()), "test.csv");
LOGGER.error("task1 耗时:{}", System.currentTimeMillis() - currentTimeMillis);
return new AsyncResult<>(str);
}
@Async(AsyncConfig.LOG_EXPORT_EXECUTOR_NAME)
public Future<String> task2() throws Exception {
long currentTimeMillis = System.currentTimeMillis();
Thread.sleep(2000);
LOGGER.error("task2 耗时:{}", System.currentTimeMillis() - currentTimeMillis);
return new AsyncResult<>("task2 执行完毕");
}
@Async(AsyncConfig.LOG_EXPORT_EXECUTOR_NAME)
public Future<String> task3() throws Exception {
long currentTimeMillis = System.currentTimeMillis();
Thread.sleep(3000);
LOGGER.error("task3 耗时:{}", System.currentTimeMillis() - currentTimeMillis);
return new AsyncResult<>("task3 执行完毕");
}
controller层代码:
@RequestMapping(value = "/export", method = RequestMethod.POST)
@PreAuthorize("hasAnyAuthority('hpcSecurityAdmin-logView-r','hpcAuditingAdmin-logView-r')")
public ResponseEntity<StreamingResponseBody> export(OperationLogInputParam operationLogParam, HttpServletRequest request)
throws Exception{
long currentTime = System.currentTimeMillis();
HttpHeaders headers = dataService.getHeadersForDownload(request, "OperationLog.csv");
Future<StreamingResponseBody> res1 = operationLogService.exportLog();
Future<String> res2 = operationLogService.task2();
Future<String> res3 = operationLogService.task3();
//阻塞在此,所有线程完成后启用
while (true) {
if(res1.isDone() && res2.isDone() && res3.isDone()) {
break;
}
Thread.sleep(1000);
}
StreamingResponseBody srb = res1.get();
LOGGER.error("主线程执行完毕,返回结果. 耗时 {} ", System.currentTimeMillis() - currentTime);
return new ResponseEntity(srb, headers, HttpStatus.OK);
}
需要注意,三个线程调用顺序是1-》2-》3
由于线程池只规定了一个线程可用(防止过多类似请求吃尽资源),因此后端测试时日志打印为1-》2-》3
如果放开线程限制,打印与完成时间有关:2-》3-》1
上一篇: Spring boot 使用AOP记录controller日志
下一篇: nginx那些事儿