前端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.pswp.cn/diannao/95905.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/95905.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/95905.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

基于RK3588的微電網協調控制器:實現分布式能源的智能調控與優化運行

微電網協調控制器方案通過集成先進算法和實時數據技術&#xff0c;實現分布式能源的光伏、儲能、風電等設備的智能協調與優化運行?12。關鍵功能包括&#xff1a;?協同優化調度?&#xff1a;采用模型預測控制&#xff08;MPC&#xff09;動態調整光伏出力、儲能充放電策略和負…

機器學習——TF-IDF文本特征提取評估權重 + Jieba 庫進行分詞(以《紅樓夢》為例)

使用 Jieba 庫進行 TF-IDF 關鍵詞提取&#xff08;以《紅樓夢》為例&#xff09;在中文文本分析中&#xff0c;TF-IDF&#xff08;Term Frequency - Inverse Document Frequency&#xff09; 是最常用的關鍵詞提取方法之一。它通過評估詞在單個文檔中的出現頻率和在所有文檔中的…

一周學會Matplotlib3 Python 數據可視化-多子圖及布局實現

鋒哥原創的Matplotlib3 Python數據可視化視頻教程&#xff1a; 2026版 Matplotlib3 Python 數據可視化 視頻教程(無廢話版) 玩命更新中~_嗶哩嗶哩_bilibili 課程介紹 本課程講解利用python進行數據可視化 科研繪圖-Matplotlib&#xff0c;學習Matplotlib圖形參數基本設置&…

Spark執行計劃與UI分析

文章目錄1.Spark任務階段劃分1.1 job&#xff0c;stage與task1.2 job劃分1.3 stage和task劃分2.任務執行時機3.task內部數據存儲與流動4.根據sparkUI了解Spark執行計劃4.1查看job和stage4.2 查看DAG圖4.3查看task1.Spark任務階段劃分 1.1 job&#xff0c;stage與task 首先根據…

16-docker的容器監控方案-prometheus實戰篇

文章目錄一.前置知識1.監控與報警2.監控系統的設計3.監控系統的分類二、prometheus概述1.什么是prometheus2.prometheus的歷史3.為什么要學習prometheus4.prometheus的使用場景5.prometheus的宏觀架構圖6.prometheus軟件下載地址三、部署prometheus server監控軟件1.同步集群時…

集成電路學習:什么是Image Processing圖像處理

Image Processing,即圖像處理,是計算機視覺、人工智能、多媒體等領域的重要基礎。它利用計算機對圖像進行分析、加工和處理,以達到預期目的的技術。以下是對圖像處理的詳細解析: 一、定義與分類 定義: 圖像處理是指用計算機對圖像進行分析,以達到所需結果的技術,又稱…

基于Android的隨身小管家APP的設計與實現/基于SSM框架的財務管理系統/android Studio/java/原生開發

基于Android的隨身小管家APP的設計與實現/基于SSM框架/android Studio/java/原生開發

Web 開發 16

1 在 JavaScript&#xff08;包括 JSX&#xff09;中&#xff0c;函數體的寫法和返回值處理在 JavaScript&#xff08;包括 JSX&#xff09;中&#xff0c;函數體的寫法和返回值處理確實有一些簡潔的語法規則&#xff0c;尤其是在箭頭函數中。這些規則常常讓人混淆&#xff0c;…

超高車輛碰撞預警系統如何幫助提升城市立交隧道安全?

超高車輛帶來的安全隱患立交橋和隧道的設計通常基于常規車輛的高度標準。然而&#xff0c;隨著重型運輸業和超高貨車的增加&#xff0c;很多超高車輛會誤入這些限高區域&#xff0c;造成潛在的安全隱患。超高車輛與立交橋梁或隧道頂蓋發生碰撞時&#xff0c;可能導致結構受損&a…

三種變量類型在局部與全局作用域的區別

一、基本概念作用域&#xff08;Scope&#xff09;&#xff1a; 全局作用域&#xff1a;定義在所有函數外部的變量或函數&#xff0c;具有文件作用域&#xff0c;生命周期為整個程序運行期間。局部作用域&#xff1a;定義在函數、塊&#xff08;如 {}&#xff09;或類內部的變量…

InfluxDB 數據遷移工具:跨數據庫同步方案(二)

六、基于 API 的同步方案實戰6.1 API 原理介紹InfluxDB 提供的 HTTP API 是實現數據遷移的重要途徑。通過這個 API&#xff0c;我們可以向 InfluxDB 發送 HTTP 請求&#xff0c;以實現數據的讀取和寫入操作。在數據讀取方面&#xff0c;使用GET請求&#xff0c;通過指定數據庫名…

JVM安全點輪詢匯編函數解析

OpenJDK 17 源碼的實現邏輯&#xff0c;handle_polling_page_exception 函數在方法返回時的調用流程如下&#xff1a;調用流程分析&#xff1a;棧水印檢查觸發跳轉&#xff1a;當線程執行方法返回前的安全點輪詢時&#xff08;MacroAssembler::safepoint_poll 中 at_returntrue…

Linux怎么查看服務器開放和啟用的端口

在 Linux 系統中&#xff0c;可以通過以下方法查看 服務器開放和啟用的端口。以下是詳細的步驟和工具&#xff0c;適用于不同場景。1. 使用 ss 查看開放的端口ss 是一個現代化工具&#xff0c;用于顯示網絡連接和監聽的端口。1.1 查看正在監聽的端口運行以下命令&#xff1a;ba…

XF 306-2025 阻燃耐火電線電纜檢測

近幾年隨著我國經濟快速的發展&#xff0c;電氣火災呈現高發趨勢&#xff0c;鑒于電線電纜火災的危險性&#xff0c;國家制定了阻燃&#xff0c;耐火電線電纜的標準&#xff0c;為企業&#xff0c;建設方&#xff0c;施工方等的生產&#xff0c;選材提供了指引。XF 306-2025 阻…

【Java|第二十篇】面向對象(十)——枚舉類

目錄 &#xff08;四&#xff09;面向對象&#xff1a; 12、枚舉類&#xff1a; &#xff08;1&#xff09;概述&#xff1a; &#xff08;2&#xff09;枚舉類的定義格式&#xff1a; &#xff08;3&#xff09;編譯與反編譯&#xff1a; &#xff08;4&#xff09;Enum類…

第二十一天-OLED顯示實驗

一、OLED顯示原理1、OLED名詞解釋OLED可以自發光&#xff0c;無需背光光源。2、正點原子OLED模塊模塊總體概述模塊接口模式選擇MCU與模塊外部連接8080并口讀寫過程OLED顯存因為要進行顯示&#xff0c;所以需要有顯存。顯存容量為128 x 8 byte&#xff0c;一個點用一位表示。SSD…

會議系統核心流程詳解:創建、加入與消息交互

一、系統架構概覽 會議系統采用"主進程線程池進程池"的分層架構&#xff0c;實現高并發與業務隔離&#xff1a; #mermaid-svg-fDJ5Ja5L3rqPkby0 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-fDJ5Ja5L3r…

Spring 創建 Bean 的 8 種主要方式

Spring&#xff08;尤其是 Spring Boot&#xff09;提供了多種方式來讓容器創建和管理 Bean。Component、Configuration Bean、EnableConfigurationProperties 都是常見方式。 下面我為你系統地梳理 Spring 創建 Bean 的所有主要方式&#xff0c;并說明它們的使用場景和區別。…

React 第七十節 Router中matchRoutes的使用詳解及注意事項

前言 matchRoutes 是 React Router v6 提供的一個核心工具函數&#xff0c;主要用于匹配路由配置與當前路徑。它在服務端渲染&#xff08;SSR&#xff09;、數據預加載、權限校驗等場景中非常實用。下面詳細解析其用法、注意事項和案例分析&#xff1a; 1、基本用法 import { m…

iSCSI服務配置全指南(含服務器與客戶端)

iSCSI服務配置全指南&#xff08;含服務器與客戶端&#xff09;一、iSCSI簡介 1. 概念 互聯網小型計算機系統接口&#xff08;Internet Small Computer System Interface&#xff0c;簡稱iSCSI&#xff09;是一種基于TCP/IP的協議&#xff0c;其核心功能是通過IP網絡仿真SCSI高…