SpringBoot異步導(dǎo)出文件的實(shí)現(xiàn)步驟
序言
在工作當(dāng)中經(jīng)常會(huì)碰到文件下載的功能,當(dāng)文件較大時(shí),如果使用原來的下載方式會(huì)導(dǎo)致下載進(jìn)度慢,甚至有可能存在請(qǐng)求超時(shí)的情況,所以封裝異步下載文件的功能是非常有必要的。我私下嘗試了,可以實(shí)現(xiàn),代碼已經(jīng)提交到我倉庫。
思路
既然是要實(shí)現(xiàn)異步下載,那么在發(fā)送下載文件的請(qǐng)求,頁面就不應(yīng)該處于等待狀態(tài),而是給一個(gè)提示,比如 開始導(dǎo)出文件 之類的,然后后臺(tái)開始執(zhí)行下載文件的代碼,下載完成后上傳到 Minio 中,然后將這訪問路徑存儲(chǔ)到數(shù)據(jù)庫表中,前段再去請(qǐng)求這個(gè)下載路徑就可以實(shí)現(xiàn)了,
代碼實(shí)現(xiàn)
前端發(fā)送文件下載的請(qǐng)求時(shí),后端返回一個(gè) processId 給前端,然后前端根據(jù)這個(gè)id一直輪詢查詢這個(gè)流程的狀態(tài),直到這個(gè)流程的狀態(tài)為完成時(shí)結(jié)束輪詢,然后就可以根據(jù)后端返回的訪問 url 地址來下載文件。
上面是一個(gè)大概的思路,下面來看實(shí)現(xiàn):
自定義注解
首先需要定義兩個(gè)注解 : ProcessRunner 和 ProcessHandle
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ProcessRunner {
String processName() default "";
ProcessType processType() default ProcessType.EXCEL_TYPE;
String description() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ProcessHandle {
String value() default "";
ProcessType processType() default ProcessType.EXCEL_TYPE;
}
ProcessType 定義如下:
public enum ProcessType {
EXCEL_TYPE("Excel類型", ProcessTypeEnum.EXCEL_TYPE);
private final String value;
private final ProcessTypeEnum type;
ProcessType(String value, ProcessTypeEnum type) {
this.value = value;
this.type = type;
}
public String getValue() {
return value;
}
public ProcessTypeEnum getType() {
return type;
}
public enum ProcessTypeEnum {
/**
* 進(jìn)程類型
*/
EXCEL_TYPE("Excel類型", "EXCEL_TYPE");
private final String desc;
private final String type;
ProcessTypeEnum(String desc, String type) {
this.desc = desc;
this.type = type;
}
public String getDesc() {
return desc;
}
public String getType() {
return type;
}
}
}
這里的兩個(gè)注解主要用來做Aop切面和掃描使用的,具體如下:
AOP 切面
@Aspect
@Component
@Order(1)
@Slf4j
public class ProcessRunnerAop {
@Resource
private ProcessService processService;
@Around("@annotation(processRunner)")
public Object processRunner(ProceedingJoinPoint proceedingJoinPoint, ProcessRunner processRunner) throws Throwable {
// 插入流程到數(shù)據(jù)庫表
Object proceed = proceedingJoinPoint.proceed();
if (proceed instanceof ProcessDTO processDTO) {
Process process = new Process();
process.setProcessId(processDTO.getProcessId());
process.setProcessName(processRunner.processName());
process.setDescription(processRunner.description());
process.setProcessType(processRunner.processType().name());
process.setStartTime(new Date());
process.setStatus((byte) 0);
process.setParams(JSONUtil.toJsonStr(processDTO));
processService.insert(process);
}
return proceed;
}
}
注解掃描
@Component
@Slf4j
public class ProcessManager implements BeanPostProcessor, PriorityOrdered {
private static final Map<String, Invoker> INVOKER_MAP = new ConcurrentHashMap<>();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static final Pattern SERVICE_IMPL_BEAN_NAME_PATTERN = Pattern.compile("(?i)[.a-z]+ServiceImpl");
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
Class<?> beanClass = AopUtils.isAopProxy(bean) ? AopUtils.getTargetClass(bean) : bean.getClass();
if (!SERVICE_IMPL_BEAN_NAME_PATTERN.matcher(beanClass.getName()).matches()) {
return bean;
}
log.info("beanName:{}, beanType:{}", beanName, beanClass.getName());
Method[] declaredMethods = ClassUtil.getDeclaredMethods(beanClass);
for (Method declaredMethod : declaredMethods) {
if (declaredMethod.isAnnotationPresent(ProcessHandle.class)) {
ProcessHandle processHandle = declaredMethod.getAnnotation(ProcessHandle.class);
if (INVOKER_MAP.containsKey(processHandle.processType().toString())) {
throw new RuntimeException("processType 重復(fù)" + processHandle.processType().toString());
}
Parameter[] parameters = declaredMethod.getParameters();
// 校驗(yàn)processId參數(shù)是否存在
boolean processIdExist = false;
for (Parameter parameter : parameters) {
String name = parameter.getName();
if (StrUtil.equalsIgnoreCase("processId", name)) {
processIdExist = true;
break;
}
}
if (!processIdExist) {
throw new RuntimeException("processId 參數(shù)不存在," + processHandle.processType().toString());
}
if (declaredMethod.getReturnType() != String.class) {
throw new RuntimeException("返回值必須為String," + processHandle.processType().toString());
}
INVOKER_MAP.put(processHandle.processType().name(), new Invoker(bean, declaredMethod, parameters));
}
}
return bean;
}
public static void handleProcess(Long processId, boolean isEnd) {
log.info("processId:{} isEnd:{}", processId, isEnd);
ProcessService processService = SpringUtil.getBean(ProcessService.class);
Process process = processService.queryById(processId);
if (process == null) {
throw new RuntimeException("processId 不存在" + processId);
}
if (isEnd) {
process.setStatus((byte) 2);
} else {
process.setStatus((byte) 1);
}
processService.update(process);
log.info("任務(wù)更新狀態(tài)成功");
}
public void startProcess(Long processId) {
ProcessService processService = SpringUtil.getBean(ProcessService.class);
Process process = processService.queryById(processId);
if (process == null) {
return;
}
String params = process.getParams();
try {
JsonNode jsonNode = OBJECT_MAPPER.readTree(params);
Invoker invoker = INVOKER_MAP.get(process.getProcessType());
Method method = invoker.getMethod();
Parameter[] parameters = method.getParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
Class<?> type = parameter.getType();
String name = parameter.getName();
if (Long.class.isAssignableFrom(type) && StrUtil.equalsIgnoreCase("processId", name)) {
args[i] = jsonNode.get(name).asLong();
} else if (Map.class.isAssignableFrom(type)) {
JsonNode valueNode = jsonNode.get(name);
args[i] = OBJECT_MAPPER.convertValue(valueNode, type);
}
}
method.setAccessible(true);
Object url = method.invoke(INVOKER_MAP.get(process.getProcessType()).getBean(), args);
if (url != null) {
process.setStatus((byte) 2);
process.setUrl(url.toString());
processService.update(process);
}
} catch (JsonProcessingException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
@Override
public int getOrder() {
return 0;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Invoker {
private Object bean;
private Method method;
private Object[] args;
}
}
這段代碼的大體思路就是掃描所有以 ServiceImpl 來結(jié)尾的 Bean 對(duì)象,然后找到帶有 ProcessHandle 注解的方法,構(gòu)建成 Invoker對(duì)象存到 INVOKER_MAP 集合中。在這個(gè)里面不可以使用 Resource這類依賴注入的注解,即便使用了 Spring 也無法注入對(duì)象,因?yàn)?ProcessManager 實(shí)現(xiàn)了 PriorityOrdered 接口(實(shí)現(xiàn)這個(gè)接口是為了保證最先被初始化),這個(gè) Bean 被實(shí)例化的時(shí)候,其他的 Bean 還沒被創(chuàng)建,注入一直都是空對(duì)象。
服務(wù)層代碼實(shí)現(xiàn)
@Override
@ProcessRunner(processName = "異步導(dǎo)出excel", processType = ProcessType.EXCEL_TYPE)
public ProcessDTO startProcess() {
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("userId", 1L);
Process process = new Process();
process.setProcessName("異步導(dǎo)出excel");
hashMap.put("bean", process);
return ProcessDTO.createProcessDTO(IdUtil.getSnowflakeNextId(), hashMap);
}
@Override
@ProcessHandle(value = "異步導(dǎo)出excel", processType = ProcessType.EXCEL_TYPE)
public String downloadExcel(Long processId, Map<String, Object> data) {
// 這里模擬下載excel文件,我就直接找一個(gè)本地excel文件,不使用poi來生成excel文件了
log.info("userId:{}", data.get("userId"));
log.info("bean:{}", data.get("bean"));
File file = FileUtil.file("C:\Users\Administrator\Downloads\1.xlsx");
ProcessManager.handleProcess(processId, true);
try {
minioUtils.putObject("test", file.getName(), FileUtil.getInputStream(file), FileUtil.size(file), FileUtil.getType(file));
} catch (Exception e) {
throw new RuntimeException(e);
}
return minioUtils.getObjectUrl("test", file.getName());
}
定時(shí)任務(wù)
服務(wù)層的代碼實(shí)現(xiàn)完畢,我們需要定義一個(gè)定時(shí)任務(wù)去后臺(tái)執(zhí)行文件的下載操作,后臺(tái)下載完成后,會(huì)把 Minio 的可訪問 url 同步到數(shù)據(jù)庫中,前端根據(jù)這個(gè) url 下載就可以了,具體代碼如下:
/**
* 定時(shí)任務(wù)
* 表示上一次執(zhí)行完畢后,間隔2秒執(zhí)行下一次
*/
@Scheduled(fixedDelay = 2000)
public void run() {
log.info("開始執(zhí)行定時(shí)任務(wù)");
processService.run();
}
定時(shí)使用 HttpUtil 工具類去請(qǐng)求
@Override
public void run() {
// 查詢出之前未開始的任務(wù)
Process process = processMapper.listByStatus(0);
if (process == null) {
return;
}
// 開始執(zhí)行任務(wù)
HashMap<String, Object> map = new HashMap<>();
map.put("processId", process.getProcessId());
HttpUtil.get("http://127.0.0.1:8080/processManager/startProcess?processId=" + process.getProcessId());
}
請(qǐng)求的具體方法如下
@RestController
@RequestMapping("/processManager")
public class ProcessManagerController {
@Resource
private ProcessManager processManager;
@GetMapping("/startProcess")
public void startProcess(@RequestParam(value = "processId") Long processId) {
processManager.startProcess(processId);
}
}
public void startProcess(Long processId) {
ProcessService processService = SpringUtil.getBean(ProcessService.class);
Process process = processService.queryById(processId);
if (process == null) {
return;
}
String params = process.getParams();
try {
JsonNode jsonNode = OBJECT_MAPPER.readTree(params);
Invoker invoker = INVOKER_MAP.get(process.getProcessType());
Method method = invoker.getMethod();
Parameter[] parameters = method.getParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
Class<?> type = parameter.getType();
String name = parameter.getName();
if (Long.class.isAssignableFrom(type) && StrUtil.equalsIgnoreCase("processId", name)) {
args[i] = jsonNode.get(name).asLong();
} else if (Map.class.isAssignableFrom(type)) {
JsonNode valueNode = jsonNode.get(name);
args[i] = OBJECT_MAPPER.convertValue(valueNode, type);
}
}
method.setAccessible(true);
Object url = method.invoke(INVOKER_MAP.get(process.getProcessType()).getBean(), args);
if (url != null) {
process.setStatus((byte) 2);
process.setUrl(url.toString());
processService.update(process);
}
} catch (JsonProcessingException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
這里下載完成后,數(shù)據(jù)庫表中任務(wù)的狀態(tài)和 url 地址就會(huì)更新

總結(jié)
我這里并沒有寫前端代碼,最后和大家口述一下大概的流程:
- 前端發(fā)送一個(gè)下載文件的請(qǐng)求,后端結(jié)構(gòu)返回一個(gè) processId 值,并提示 “開始下載文件”
- 使用 AOP 切面,將這個(gè) processId 存儲(chǔ)到數(shù)據(jù)庫,并記錄開始時(shí)間和任務(wù)狀態(tài)
- 后臺(tái)維護(hù)定時(shí)任務(wù)去定時(shí)查詢數(shù)據(jù)庫中未開始的下載任務(wù),根據(jù)時(shí)間升序排序
- 如果存在未完成的定時(shí)任務(wù),則請(qǐng)求 processManager 去觸發(fā)任務(wù)的下載,下載完成后會(huì)將任務(wù)的狀態(tài)標(biāo)記為已完成,同時(shí)更新文件的可訪問 url
- 前端觸發(fā)下載的請(qǐng)求后,根據(jù)返回的 processId 一直走接口去查詢?nèi)蝿?wù)的狀態(tài)嗎,如果返回的數(shù)據(jù)狀態(tài)為已完成,則直接訪問 url 就可以觸發(fā)瀏覽器的下載操作。
到此這篇關(guān)于SpringBoot異步導(dǎo)出文件的實(shí)現(xiàn)步驟的文章就介紹到這了,更多相關(guān)SpringBoot異步導(dǎo)出 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java代碼實(shí)現(xiàn)C盤文件統(tǒng)計(jì)工具
今天周末,給大家分享基于java代碼實(shí)現(xiàn)C盤文件統(tǒng)計(jì)工具,在這小編使用的版本是Maven-3.9.9,jdk1.8,代碼簡單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2021-07-07
kafka springBoot配置的實(shí)現(xiàn)
本文主要介紹了kafka springBoot配置的實(shí)現(xiàn),通過詳細(xì)解析Spring Boot for Apache Kafka的配置選項(xiàng),以及如何優(yōu)化Kafka生產(chǎn)者和消費(fèi)者的屬性設(shè)置,感興趣的可以了解一下2023-11-11
換了最新的idea如何將原來舊版本的idea設(shè)置導(dǎo)進(jìn)新的idea中
這篇文章主要介紹了換了最新的idea如何將原來舊版本的idea設(shè)置導(dǎo)進(jìn)新的idea中,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11
AsyncHttpClient?RequestFilter請(qǐng)求篩選源碼解讀
這篇文章主要為大家介紹了AsyncHttpClient?RequestFilter請(qǐng)求篩選源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
Java中反射的"暴破"機(jī)制(SetAccessible方法)詳解
這篇文章主要為大家詳細(xì)介紹了Java中反射的"暴破"機(jī)制,以及如何利用這一機(jī)制實(shí)現(xiàn)訪問非公有屬性,方法,和構(gòu)造器,文中示例代碼講解詳細(xì),感興趣的可以了解一下2022-08-08
Java如何優(yōu)雅關(guān)閉異步中的ExecutorService
在并發(fā)編程領(lǐng)域,Java的ExecutorService是線程池管理的關(guān)鍵接口,這篇文章主要為大家介紹了如何優(yōu)雅關(guān)閉異步中的ExecutorService,需要的可以了解下2025-02-02
在springboot中實(shí)現(xiàn)個(gè)別bean懶加載的操作
這篇文章主要介紹了在springboot中實(shí)現(xiàn)個(gè)別bean懶加載的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-10-10

