SpringBoot+Vue 后端异步多线程加前端实时更新的实现
Spring Boot在一些场景下需要用户发起请求服务器执行耗时任务(例如批量发送邮件),此时如果让用户一直等待任务执行,明显不合理,而且会很容易超时,这个时候正确的做法是后端采取异步多线程的方式执行任务,并保持任务执行的进度状态,前端使用定时器获取任务执行状态进行持续更新直到任务完成。具体的实现方法
1、后端异步多线程实现
一、springboot开启异步支持
-
在application入口类添加@EnableAsync注解 -
使用配置类为springboot配置一个线程池,此处必须实现AsyncConfigurer接口,并使用@Configuration进行注解 @Configuration
@EnableAsync
public class AsyncTaskConfig implements AsyncConfigurer {
private int corePoolSize = 10;
private int maxPoolSize = 50;
private int queueCapacity = 10;
private int awaitTerminationSeconds = 10;
private String threadNamePrefix = "txzmap-tile-download";
@Override
@Bean
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
threadPool.setCorePoolSize(corePoolSize);
threadPool.setMaxPoolSize(maxPoolSize);
threadPool.setQueueCapacity(queueCapacity);
threadPool.setAwaitTerminationSeconds(awaitTerminationSeconds);
threadPool.setThreadNamePrefix(threadNamePrefix);
threadPool.setWaitForTasksToCompleteOnShutdown(true);
threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
threadPool.initialize();
return threadPool.getThreadPoolExecutor();
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}
二、编写具体的任务方法
在具体的任务方法上使用@Async进行注解,表示该方法需要异步执行,这里没有指定线程池则为上面已经配置的默认线程池,如果有多个线程池可以通过@Async的value属性指定线程池
@Service
@Scope("prototype")
public class MailSendService {
@Autowired
TaskInfoList taskInfoList;
@Async
public void sendMail(String[] mails) throws InterruptedException {
System.out.println("任务开始");
TaskInfoList.TaskInfo task = new TaskInfoList.TaskInfo();
int size = taskInfoList.addNewTask(task);
int process=0;
int total=mails.length;
for (int i = 0; i <= total; i++) {
process++;
doSendMail();
System.out.println(Thread.currentThread().getName() + "Mission is running at " + process + "/" + total);
task.setProcess(process);
task.setStatus(TaskInfoList.TaskStatus.RUNNING);
taskInfoList.update(task);
}
System.out.println("mission completed!");
task.setStatus(TaskInfoList.TaskStatus.FINISHED);
task.setEndTime(System.currentTimeMillis());
task.setUrl("finished");
taskInfoList.update(task);
}
}
三、任务状态保持
为了记录当前任务的执行状态(耗时,完成进度,状态)等信息,就需要一个全局的方法对任务的状态进行维护,这里可以选用redis或者是一个简单的管理类来实现。由于项目较小,就没有必要为了这个功能而上Redis,如果项目已经有Redis了那么可考虑使用Redis,这里我们采用后者,编写一个简单的任务状态维护类。上代码
@Component
public class TaskInfoList {
public enum TaskStatus {
START, RUNNING, FINISHED
}
public static class TaskInfo {
String uuid;
String name = "";
Long startTime;
Long endTime;
TaskStatus status;
Integer total = 0;
Integer process = 0;
Integer size = 0;
String url = "";
}
public static Map<String, TaskInfo> taskList = new HashMap<>();
public Integer addNewTask(TaskInfo task) {
taskList.put(task.getUuid(), task);
return taskList.size();
}
public void update(TaskInfo task) {
taskList.put(task.getUuid(), task);
}
public void remove(TaskInfo task) {
taskList.remove(task.getUuid());
}
public TaskInfo search(String uuid) {
return taskList.get(uuid);
}
@PostConstruct
private void init() {
}
@PreDestroy
public void destroy() {
taskList.clear();
}
}
具体状态维护业务代码可见上文中的业务代码。
四、Controller业务实现
这里需要实现两个接口
-
用于用户提交任务信息的接口 @PostMapping("/sendMail")
public MyResult sendMail(Integer cid) {
String taskId = UUID.randomUUID().toString();
String[] mails=mailService.getByCid(cid);
try {
MailSendService.sendMail(String[] mails);
} catch (InterruptedException e) {
e.printStackTrace();
return new MyResult(0, "创建失败" + e.getMessage());
}
return new MyResult(1, "任务添加成功!", taskId);
}
最后将该任务的uuid返回给用户便于用户查询 -
提供任务状态查询的接口 @GetMapping("/ps")
public TaskInfoList.TaskInfo updateProcessStatus(@RequestParam(value = "id", required = true) String taskId) {
return taskInfoList.search(taskId);
}
2、vue前端进度更新的实现
-
用一个map来维护当前的多任务状态 data: {taskMap:new Map()}
-
任务添加提交 this.post(url, p, data => {
if (data.code == 1) {
this.$message.success('任务添加成功!');
let id = data.data;
this.taskMap.set('id',data);
}
});
-
开启定时器持续获取所有任务的状态直到任务完成 this.timer = setInterval(() => {
this.taskMap.forEach((value, key, map) => {
if (value.status != 'FINISHED') {
this.get(url + key, data => {
console.log(data);
map.set(key, data);
this.$forceUpdate();
});
}
});
}, 1000);
剩下的就是相关的view层的更新,具体实现很简单,大家根据项目情况进行修改就行,这里不赘述。
整个方案是本人在实际项目中的实践过程记录,由于项目较小不涉及高并发等复杂情况,仅供参考,欢迎交流,大佬勿喷!
|