当前位置: 首页 > news >正文

前端vue3+后端spring boot导出数据

有个项目需要提供数据导出功能。

该项目前端用vue3编写,后端是spring boot 2,数据库是mysql8。

工作流程是:

1)前端请求数据导出
2)后端接到请求后,开启一个数据导出线程,然后立刻返回信息到前端
3)前端定期轮询,看导出是否已完成
4)后端的数据导出线程,将数据导出,生成文件,存放在后端
5)前端获知导出完成,请求下载文件
6)后端读取文件内容,以流的方式传输给前端,供前端下载

这里面可以看出,数据导出采用了异步方式。为什么采用异步方式,主要是数据量比较大,差不多200万条。同步方式的话,前端必超时。而且200万条记录,后端导出,生成文件,也不能一次性将200万条记录取出,然后生成文件,而是采用分页的方式,比如每次拿一万条,循环提取,直至取完。另外,数据导出也不应该占用主线程,避免其他业务受影响。

下面是详细介绍。

一、前端

前端一共请求3个接口。一个请求导出,一个导出状态查询,一个下载。首先向后端请求导出,由于是异步的,请求发出后,立即返回;此后定期查询导出状态;发现导出状态已完成后,即向后端请求下载。

1、请求数据导出

点击按钮“开始导出”

<el-button v-if="exportState.ready" type="primary" plain class="float-right"@click="startExport">开始导出</el-button>
import { start as startApi, checkStatus as checkStatusApi, exportCsv } from "@/modules/api/sensor/export.js";async function startExport() {const valid = await form1.value.validate(); // 等待表单验证通过if (valid) {startApi(formState).then((res) => {waiting();const taskId = res.data;checkExportStatus(taskId);//查询导出状态});}
}

2、查询导出状态

import { saveAs } from 'file-saver'; // 或者自己写 blob 下载逻辑function checkExportStatus(taskId) {//使用定时器const timer1 = setInterval(async () => {try {const res = await checkStatusApi(taskId);const { status, filename } = res.data;if (status === 'DONE') {clearInterval(timer1);const response = await exportCsv(filename);//向后端发出下载请求const blob = new Blob([response], { type: 'text/csv;charset=utf-8' });saveAs(blob, getFileName());//保存文件,一个第三方组件done();} else if (status === 'ERROR') {clearInterval(timer1);over();ElMessage.error('导出失败: ' + filename);}} catch (err) {clearInterval(timer1);over();ElMessage.error('导出失败: ' + err.message || '网络异常');}}, 1000);
}

3、下载

上面代码中的exportCsv。

4、向后端请求的API

import { request, requestBlob } from "@/request";const prefix = "/export";export const exportCsv = (filename) => {return requestBlob({url: prefix + "/download/" + filename,method: "get",});
};
export const start = (params) => {console.log(params);return request({url: prefix + "/start",params,method: "post",});
};
export const checkStatus = (taskId) => {return request({url: prefix + "/status/" + taskId,method: "get",});
};

二、后端

后端需要做比较多的工作。为了支持可能数量巨大的数据的下载请求,不致影响主线程性能,同时也避免客户端因为等待超时而断连,需要开辟新线程、异步方式来处理数据导出,因此需要引入线程池和任务管理。

后端的处理导出的流程是,接收到前端的请求后,从数据库中获取数据,如果数据量特别大,还要分页,采用循环多次查找;然后将数据输出到csv格式的文件中,文件保存在服务器。当前端侦察到导出完成,即请求下载,后端就将文件内容读出,以二进制流的形式返回给前端。前端侦察导出状态时,后端会将文件名返回给前端。为什么后端要先生成文件,貌似多此一举呢?原因是整个导出过程是异步的,后端没有办法一步到位将流返回给前端。

1、线程池

首先要注册一个线程池。

@Configuration
@EnableAsync
public class AsyncConfig {@Beanpublic TaskExecutor executor(){ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();executor.setCorePoolSize(10); //核心线程数executor.setMaxPoolSize(20);  //最大线程数executor.setQueueCapacity(1000); //队列大小executor.setKeepAliveSeconds(300); //线程最大空闲时间executor.setThreadNamePrefix("fsx-Executor-"); //指定用于新创建的线程名称的前缀。executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());executor.initialize(); // ✅ 加上这一行return executor;}
}

2、任务管理

@Component
public class TaskManager {// 任务状态:PENDING, DONE, ERRORprivate final Map<String, String> taskStatusMap = new ConcurrentHashMap<>();private final Map<String, String> taskResultMap = new ConcurrentHashMap<>();public void markTaskDone(String taskId, String fileName) {taskStatusMap.put(taskId, "DONE");taskResultMap.put(taskId, fileName);}public void markTaskFailed(String taskId, String errorMsg) {taskStatusMap.put(taskId, "ERROR");taskResultMap.put(taskId, errorMsg);}public String getStatus(String taskId) {return taskStatusMap.getOrDefault(taskId, "PENDING");}public String getResult(String taskId) {return taskResultMap.get(taskId);}public void clearTask(String taskId) {taskStatusMap.remove(taskId);taskResultMap.remove(taskId);}
}

3、控制器

@RestController
@RequestMapping("/export")
public class ExportController {@AutowiredSensorDataService sensorDataService;@Autowiredprivate TaskManager taskManager;//任务管理@Value("${export.path}")private String exportPath;//请求导出@PostMapping("/start")@ResponseBodypublic Result startExport(ExportParam paramObj) {String taskId = UUID.randomUUID().toString();sensorDataService.asyncExportData(taskId, paramObj); // 异步执行return Result.ok().put("data",taskId);}//查询导出状态@GetMapping("/status/{taskId}")@ResponseBodypublic Result checkStatus(@PathVariable String taskId) {String status = taskManager.getStatus(taskId);String filename = taskManager.getResult(taskId);Map<String, String> data = new HashMap<>();data.put("taskId", taskId);data.put("status", status);data.put("filename", filename);//文件名(不含路径)return Result.ok().put("data",data);}//下载导出文件@GetMapping(value = "/download/{fileName:.+}")public void exportFile(@PathVariable String fileName,HttpServletResponse response) {try {// 2. 构建文件路径(确保与写入时一致)String filePath = exportPath + fileName;// 3. 设置响应头response.setContentType("text/csv");response.setCharacterEncoding("utf-8");response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));// 4. 读取文件内容并写入响应输出流try (InputStream inputStream = new FileInputStream(filePath)) {byte[] buffer = new byte[4096];int bytesRead;while ((bytesRead = inputStream.read(buffer)) != -1) {response.getOutputStream().write(buffer, 0, bytesRead);}response.getOutputStream().flush();}} catch (Exception e) {try {response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "文件下载失败:" + e.getMessage());} catch (IOException ex) {ex.printStackTrace();}e.printStackTrace();}}
}

4、service

@Service
public class SensorDataServiceImpl implements SensorDataService {@Value("${export.path}")private String exportPath;@Value("${export.page-size:10000}")private Integer exportPageSize;//每页多少条记录@Override@Async("executor")  // 指定使用定义的线程池public void asyncExportData(String taskId, ExportParam param) {System.out.println("当前线程: " + Thread.currentThread().getName());try {// 执行导出逻辑exportDataToFile(taskId, param);} catch (Exception e) {System.err.println("导出数据时发生异常:");e.printStackTrace();}}// 数据导出主方法private void exportDataToFile(String taskId, ExportParam param) throws Exception {// 1. 定义文件路径(请确保该目录存在且有写权限)String exportDir = exportPath;String fileName = getDownloadDataFileName(param);String filePath = exportDir + fileName;// 2. 创建 CSV 文件并写入表头try (CSVWriter writer = new CSVWriter(new FileWriter(filePath))) {// 获取表头(根据 param 可以动态生成)String[] headers = getHeaders(param);writer.writeNext(headers);// 3. 分页查询数据int pageNumber = 0;int pageSize = exportPageSize; // 每页查询 5000 条boolean hasMore = true;while (hasMore) {String sql = getSqlWithPagination(param, pageSize, pageNumber);List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql);if (rows.isEmpty()) {hasMore = false;} else {for (Map<String, Object> row : rows) {String[] rowData = formatRow(row, headers);writer.writeNext(rowData);}writer.flush(); // 及时刷新,避免内存积压pageNumber++;}}// 4. 导出完成后记录任务状态和文件路径taskManager.markTaskDone(taskId, fileName);} catch (Exception e) {// 记录错误信息taskManager.markTaskFailed(taskId, e.getMessage());throw e;}}// 构建带分页的 SQLprivate String getSqlWithPagination(ExportParam paramObj, int pageSize, int pageNumber) {String baseSql = getSql(paramObj);return (baseSql.length() > 0) ? baseSql + " LIMIT " + pageSize + " OFFSET " + (pageNumber * pageSize) : "";}
}

三、效果

1、组件全貌

在这里插入图片描述

2、点击开始导出

在这里插入图片描述

3、导出成功

在这里插入图片描述

四、小结

有的表数据量特别巨大,一个月有记录几百万条。按分页查找,每页5万条记录处理,下载一个月数据需要2、3分钟。

http://www.xdnf.cn/news/1317493.html

相关文章:

  • 《设计模式》工厂方法模式
  • 【CV 目标检测】Fast RCNN模型②——算法流程
  • 代码随想录算法训练营四十四天|图论part02
  • 【Luogu】每日一题——Day21. P3556 [POI 2013] MOR-Tales of seafaring (图论)
  • 上网行为组网方案
  • 数据结构03(Java)--(递归行为和递归行为时间复杂度估算,master公式)
  • Mac(五)自定义鼠标滚轮方向 LinearMouse
  • Linux软件编程:进程与线程(线程)
  • JVM学习笔记-----StringTable
  • Docker Compose 安装 Neo4j 的详细步骤
  • PostgreSQL导入mimic4
  • go基础学习笔记
  • k8s集群搭建一主多从的jenkins集群
  • Win11 文件资源管理器预览窗格显示 XAML 文件内容教程
  • C++ vector的使用
  • 10 SQL进阶-SQL优化(8.15)
  • 说一下事件委托
  • Java 大视界 -- Java 大数据分布式计算在基因测序数据分析与精准医疗中的应用(400)
  • 【UEFI系列】ACPI
  • 跨越南北的养老对话:为培养“银发中国”人才注入新动能
  • JavaScript 性能优化实战:从评估到落地的全链路指南
  • Spark03-RDD02-常用的Action算子
  • 在鸿蒙中实现深色/浅色模式切换:从原理到可运行 Demo
  • E2B是一个开源基础设施,允许您在云中安全隔离的沙盒中运行AI生成的代码和e2b.dev网站
  • Diamond基础2:开发流程之LedDemo
  • c_str()函数的详细解析
  • 简单的 VSCode 设置
  • (nice!!!)(LeetCode 每日一题) 837. 新 21 点 (动态规划、数学)
  • bash shell 入门
  • 云智智慧停充一体云-allnew全新体验-路内停车源码+路外停车源码+充电桩源码解决方案