SpringBoot异步导出文件的实现步骤

 更新时间:2025年09月04日 09:14:08   作者:月生_  
本文主要介绍了SpringBoot异步导出文件的实现步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

序言

在工作当中经常会碰到文件下载的功能,当文件较大时,如果使用原来的下载方式会导致下载进度慢,甚至有可能存在请求超时的情况,所以封装异步下载文件的功能是非常有必要的。我私下尝试了,可以实现,代码已经提交到我仓库

思路

既然是要实现异步下载,那么在发送下载文件的请求,页面就不应该处于等待状态,而是给一个提示,比如 开始导出文件 之类的,然后后台开始执行下载文件的代码,下载完成后上传到 Minio 中,然后将这访问路径存储到数据库表中,前段再去请求这个下载路径就可以实现了,

代码实现

前端发送文件下载的请求时,后端返回一个 processId 给前端,然后前端根据这个id一直轮询查询这个流程的状态,直到这个流程的状态为完成时结束轮询,然后就可以根据后端返回的访问 url 地址来下载文件。

上面是一个大概的思路,下面来看实现:

自定义注解

首先需要定义两个注解 : ProcessRunnerProcessHandle

@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 {
        /**
         * 进程类型
         */
        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;
        }
    }
}

这里的两个注解主要用来做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 {
        // 插入流程到数据库表
        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 重复" + processHandle.processType().toString());
                }
                Parameter[] parameters = declaredMethod.getParameters();
                // 校验processId参数是否存在
                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 参数不存在," + 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("任务更新状态成功");
    }

    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 来结尾的 Bean 对象,然后找到带有 ProcessHandle 注解的方法,构建成 Invoker对象存到 INVOKER_MAP 集合中。在这个里面不可以使用 Resource这类依赖注入的注解,即便使用了 Spring 也无法注入对象,因为 ProcessManager 实现了 PriorityOrdered 接口(实现这个接口是为了保证最先被初始化),这个 Bean 被实例化的时候,其他的 Bean 还没被创建,注入一直都是空对象。

服务层代码实现

@Override
@ProcessRunner(processName = "异步导出excel", processType = ProcessType.EXCEL_TYPE)
public ProcessDTO startProcess() {
    HashMap<String, Object> hashMap = new HashMap<>();
    hashMap.put("userId", 1L);
    Process process = new Process();
    process.setProcessName("异步导出excel");
    hashMap.put("bean", process);
    return ProcessDTO.createProcessDTO(IdUtil.getSnowflakeNextId(), hashMap);
}

@Override
@ProcessHandle(value = "异步导出excel", processType = ProcessType.EXCEL_TYPE)
public String downloadExcel(Long processId, Map<String, Object> data) {
    // 这里模拟下载excel文件,我就直接找一个本地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());
}

定时任务

服务层的代码实现完毕,我们需要定义一个定时任务去后台执行文件的下载操作,后台下载完成后,会把 Minio 的可访问 url 同步到数据库中,前端根据这个 url 下载就可以了,具体代码如下:

/**
 * 定时任务
 * 表示上一次执行完毕后,间隔2秒执行下一次
 */
@Scheduled(fixedDelay = 2000)
public void run() {
    log.info("开始执行定时任务");
    processService.run();
}

定时使用 HttpUtil 工具类去请求

@Override
public void run() {
    // 查询出之前未开始的任务
    Process process = processMapper.listByStatus(0);
    if (process == null) {
        return;
    }
    // 开始执行任务
    HashMap<String, Object> map = new HashMap<>();
    map.put("processId", process.getProcessId());
    HttpUtil.get("http://127.0.0.1:8080/processManager/startProcess?processId=" + process.getProcessId());
}

请求的具体方法如下

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

这里下载完成后,数据库表中任务的状态和 url 地址就会更新

总结

我这里并没有写前端代码,最后和大家口述一下大概的流程:

  1. 前端发送一个下载文件的请求,后端结构返回一个 processId 值,并提示 “开始下载文件”
  2. 使用 AOP 切面,将这个 processId 存储到数据库,并记录开始时间和任务状态
  3. 后台维护定时任务去定时查询数据库中未开始的下载任务,根据时间升序排序
  4. 如果存在未完成的定时任务,则请求 processManager 去触发任务的下载,下载完成后会将任务的状态标记为已完成,同时更新文件的可访问 url
  5. 前端触发下载的请求后,根据返回的 processId 一直走接口去查询任务的状态吗,如果返回的数据状态为已完成,则直接访问 url 就可以触发浏览器的下载操作。

到此这篇关于SpringBoot异步导出文件的实现步骤的文章就介绍到这了,更多相关SpringBoot异步导出 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java代码实现C盘文件统计工具

    java代码实现C盘文件统计工具

    今天周末,给大家分享基于java代码实现C盘文件统计工具,在这小编使用的版本是Maven-3.9.9,jdk1.8,代码简单易懂,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2021-07-07
  • kafka springBoot配置的实现

    kafka springBoot配置的实现

    本文主要介绍了kafka springBoot配置的实现,通过详细解析Spring Boot for Apache Kafka的配置选项,以及如何优化Kafka生产者和消费者的属性设置,感兴趣的可以了解一下
    2023-11-11
  • 换了最新的idea如何将原来旧版本的idea设置导进新的idea中

    换了最新的idea如何将原来旧版本的idea设置导进新的idea中

    这篇文章主要介绍了换了最新的idea如何将原来旧版本的idea设置导进新的idea中,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • AsyncHttpClient RequestFilter请求筛选源码解读

    AsyncHttpClient RequestFilter请求筛选源码解读

    这篇文章主要为大家介绍了AsyncHttpClient RequestFilter请求筛选源码解读,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12
  • Java中反射的"暴破"机制(SetAccessible方法)详解

    Java中反射的"暴破"机制(SetAccessible方法)详解

    这篇文章主要为大家详细介绍了Java中反射的"暴破"机制,以及如何利用这一机制实现访问非公有属性,方法,和构造器,文中示例代码讲解详细,感兴趣的可以了解一下
    2022-08-08
  • Java如何优雅关闭异步中的ExecutorService

    Java如何优雅关闭异步中的ExecutorService

    在并发编程领域,Java的ExecutorService是线程池管理的关键接口,这篇文章主要为大家介绍了如何优雅关闭异步中的ExecutorService,需要的可以了解下
    2025-02-02
  • mybatis-plus中BaseMapper入门使用

    mybatis-plus中BaseMapper入门使用

    本文主要介绍了mybatis-plus中BaseMapper入门使用,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • 2018年java技术面试题整理

    2018年java技术面试题整理

    小编为大家整理了2018年最新的关于java技术相关的面试题,以及给出了最简简答方式,学习下吧。
    2018-02-02
  • 在springboot中实现个别bean懒加载的操作

    在springboot中实现个别bean懒加载的操作

    这篇文章主要介绍了在springboot中实现个别bean懒加载的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-10-10
  • 如何通过Java实现修改视频分辨率

    如何通过Java实现修改视频分辨率

    Java除了可以修改图片的分辨率,还可以实现修改视频的分辨率,这篇文章就将带大家学习如果编写这一工具类,感兴趣的同学可以了解一下
    2021-12-12

最新评论