SpringBoot集成EasyExcel 3.x:高效实现Excel数据的优雅导入与导出

在现代企业应用中,Excel作为数据交换的重要工具,几乎无处不在。如何高效且优雅地实现Excel数据的导入与导出,是每个开发者都需要面对的问题。EasyExcel是阿里巴巴开源的一个高性能Excel处理库,它可以大大简化Excel操作。在本文中,我将介绍如何在SpringBoot项目中集成EasyExcel 3.x,并实现Excel数据的导入与导出。

1. 项目依赖配置 (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.0</version><relativePath/></parent><groupId>com.example</groupId><artifactId>excel-demo</artifactId><version>1.0.0</version><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.2</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency></dependencies>
</project>

2. 实体类定义

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;/*** @Description: 用于Excel导入导出数据映射* @date 2025/9/15 15:34*/
@Data
public class TerminalImportVO {@ExcelProperty(value = "IMEI", index = 0)@NotBlank(message = "IMEI不能为空")@Size(min = 3, max = 20, message = "IMEI必须为3-15位数字")@Pattern(regexp = "\\d+", message = "IMEI必须为数字")@ColumnWidth(20)private String imei;
}

3. Excel监听器(用于导入数据处理)

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cetcnav.entity.TerminalInfo;
import com.cetcnav.exception.ExcelImportException;
import com.cetcnav.service.TerminalInfoService;
import com.cetcnav.vo.TerminalImportVO;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;import java.util.ArrayList;
import java.util.List;
import java.util.Set;/*** @Description: Excel导入数据监听器 处理终端数据的读取和验证* @date 2025-09-15 15:37*/
@Slf4j
public class TerminalExcelListener implements ReadListener<TerminalImportVO> {/*** 每隔100条存储数据库,然后清理list,方便内存回收*/private static final int BATCH_COUNT = 100;/*** 缓存的数据*/private List<TerminalInfo> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);private final TerminalInfoService terminalInfoService;public TerminalExcelListener(TerminalInfoService terminalInfoService) {this.terminalInfoService = terminalInfoService;}/*** 验证器*/private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();/*** 错误信息收集*/private final List<String> errors = new ArrayList<>();/*** 总行数(包括表头)*/@Getterprivate int totalRowNum = 0;/*** 成功处理的数据行数* -- GETTER --* 获取成功处理的数量*/@Getterprivate int successCount = 0;/*** 是否已经抛出异常(防止重复抛出)*/private boolean hasThrownException = false;/*** 每一条数据解析都会来调用*/@Overridepublic void invoke(TerminalImportVO terminalImportVO, AnalysisContext analysisContext) {totalRowNum++;// Excel行号从1开始int currentRowNum = analysisContext.readRowHolder().getRowIndex() + 1;log.info("解析到第{}行数据: {}", currentRowNum, terminalImportVO);try {// 1. 基本数据校验if (terminalImportVO.getImei() == null) {errors.add(String.format("第%d行%s", currentRowNum, "终端imei不能为空"));return;}// 2. 数据校验 注解校验Set<ConstraintViolation<TerminalImportVO>> violations = validator.validate(terminalImportVO);if (!violations.isEmpty()) {for (ConstraintViolation<TerminalImportVO> violation : violations) {String fieldName = violation.getPropertyPath().toString();String errorMsg = violation.getMessage();errors.add(String.format("第%d行%s", currentRowNum, errorMsg));}// 校验失败,跳过该行数据return;}// 3. 业务逻辑校验if (!validateBusinessRules(terminalImportVO, currentRowNum)) {return;}// 4. 数据去重校验(防止重复导入)if (isDuplicateUser(terminalImportVO, currentRowNum)) {return;}log.info("第{}行数据校验通过", currentRowNum);// 校验通过,添加到缓存列表TerminalInfo terminalInfo = new TerminalInfo();BeanUtils.copyProperties(terminalImportVO, terminalInfo);cachedDataList.add(terminalInfo);successCount++;// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOMif (cachedDataList.size() >= BATCH_COUNT) {saveData();// 存储完成清理 listcachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);}} catch (Exception e) {errors.add(String.format("第%d行%s", currentRowNum, "数据处理异常"));log.error("第{}行数据处理异常", currentRowNum);}}/*** 所有数据解析完成后会来调用** @param analysisContext 分析上下文*/@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {try {// 这里也要保存数据,确保最后遗留的数据也存储到数据库saveData();log.info("Excel解析完成!总行数: {}, 成功: {}, 失败: {}", totalRowNum - 1, successCount, errors.size());// 如果有错误,抛出包含所有错误信息的异常if (!errors.isEmpty() && !hasThrownException) {hasThrownException = true;// 减1是排除表头行throw new ExcelImportException(errors, totalRowNum - 1);}} catch (Exception e) {log.error("数据最终处理异常:{}", e.getMessage());if (!hasThrownException) {hasThrownException = true;throw new RuntimeException("数据处理异常: " + e.getMessage());}}}/*** 保存数据到数据库*/private void saveData() {if (cachedDataList.isEmpty()) {return;}try {log.info("开始保存{}条数据到数据库", cachedDataList.size());terminalInfoService.saveBatch(cachedDataList);log.info("存储数据库成功!");} catch (Exception e) {log.error("数据保存失败:{}", e);// 记录批量保存失败的错误for (TerminalInfo terminalInfo : cachedDataList) {int estimatedRowNum = totalRowNum - cachedDataList.size() + cachedDataList.indexOf(terminalInfo) + 1;errors.add(String.format("第%d行%s", estimatedRowNum, "数据保存失败"));}// 从成功计数中减去这些失败的数据successCount -= cachedDataList.size();}}/*** 业务规则校验** @param terminalImportVO 终端数据* @param rowNum           行号* @return 是否通过校验*/private boolean validateBusinessRules(TerminalImportVO terminalImportVO, int rowNum) {LambdaQueryWrapper<TerminalInfo> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(TerminalInfo::getImei, terminalImportVO.getImei());queryWrapper.eq(TerminalInfo::getDelFlag, 0);TerminalInfo terminalInfo = terminalInfoService.getOne(queryWrapper);if (ObjectUtil.isNotEmpty(terminalInfo)) {errors.add(String.format("第%d行%s", rowNum,  String.format("终端imei: %s 已存在!", terminalImportVO.getImei())));return false;}return true;}/*** 检查是否重复终端imei** @param terminalImportVO 终端数据* @param rowNum           行号* @return 是否重复*/private boolean isDuplicateUser(TerminalImportVO terminalImportVO, int rowNum) {for (TerminalInfo terminalInfo : cachedDataList) {if (terminalInfo.getImei().equals(terminalImportVO.getImei())) {errors.add(String.format("第%d行%s", rowNum, "终端imei重复: " + terminalImportVO.getImei()));return true;}}return false;}/*** 获取错误信息列表** @return 错误信息列表*/public List<String> getErrors() {return new ArrayList<>(errors);}/*** 获取失败数量** @return 失败数量*/public int getFailedCount() {return errors.size();}
}

4. 控制器层

@GetMapping("/template")@Operation(summary = "下载导入模板")public void downloadTemplate(HttpServletResponse response) throws IOException {try {// 设置响应头setExcelResponseHeader(response, "终端导入模板");// 创建一个空的用户列表(只有表头)List<TerminalImportVO> emptyList = List.of();// 写入ExcelEasyExcel.write(response.getOutputStream(), TerminalImportVO.class).excelType(ExcelTypeEnum.XLSX).sheet("终端数据").doWrite(emptyList);log.info("模板下载成功");} catch (Exception e) {log.error("模板下载失败", e);response.reset();response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().println("{\"code\":500,\"message\":\"模板下载失败:\" + e.getMessage() + \"\"}");}}/*** 导入Excel数据*/@PostMapping("/import")@Operation(summary = "导入Excel数据")public ApiResponse<ExcelImportResponse> importExcel(@RequestParam("file") MultipartFile file) throws IOException {if (file.isEmpty()) {return ApiResponse.error("文件为空!");}// 检查文件类型String filename = file.getOriginalFilename();if (filename == null || (!filename.endsWith(".xlsx") && !filename.endsWith(".xls"))) {return ApiResponse.error("请上传Excel文件(.xlsx或.xls)");}TerminalExcelListener listener = new TerminalExcelListener(terminalInfoService);try {// 读取Excel文件EasyExcel.read(file.getInputStream(), TerminalImportVO.class, listener).sheet().doRead();// 如果没有错误,返回成功响应ExcelImportResponse response = new ExcelImportResponse(listener.getTotalRowNum(), listener.getSuccessCount(), listener.getFailedCount(), listener.getErrors());return ApiResponse.success(response.getSummaryMessage(), response);} catch (ExcelImportException e) {// 捕获自定义异常,返回详细的错误信息ExcelImportResponse response = new ExcelImportResponse(e.getTotalRows(), e.getSuccessRows(), e.getFailedRows(), e.getErrors());return ApiResponse.error(response.getSummaryMessage(), HttpStatusConstant.ERROR, response);} catch (Exception e) {log.error("导入失败", e);return ApiResponse.error("导入失败: " + e.getMessage());}}/*** 导出Excel数据*/@GetMapping("/export")@Operation(summary = "导出Excel数据")public void exportExcel(HttpServletResponse response) throws IOException {try {// 设置响应头setExcelResponseHeader(response, "终端数据导出");// 获取数据List<TerminalInfo> terminalInfoList = terminalInfoService.list();// 写入ExcelEasyExcel.write(response.getOutputStream(), TerminalInfo.class).excelType(ExcelTypeEnum.XLSX).sheet("终端数据").doWrite(terminalInfoList);log.info("导出成功,共{}条数据", terminalInfoList.size());} catch (Exception e) {log.error("导出失败", e);response.reset();response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().println("{\"code\":500,\"message\":\"导出失败:\" + e.getMessage() + \"\"}");}}/*** 设置Excel响应头*/private void setExcelResponseHeader(HttpServletResponse response, String rawFileName) throws IOException {String fileName = URLEncoder.encode(rawFileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");}

5. 全局异常处理

import com.cetcnav.dto.ApiResponse;
import com.cetcnav.dto.ExcelImportResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;import java.util.List;
import java.util.stream.Collectors;/*** @Description: 全局异常处理器* @date 2025-09-15 14:59*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** 处理Excel导入异常*/@ExceptionHandler(ExcelImportException.class)public ApiResponse<Object> handleExcelImportException(ExcelImportException e) {log.warn("Excel导入数据校验失败: {}", e.getErrorSummary());return ApiResponse.error(e.getMessage(), 400);}/*** 处理文件大小超过限制异常*/@ExceptionHandler(MaxUploadSizeExceededException.class)public ApiResponse<Object> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {log.warn("文件大小超过限制", e);return ApiResponse.error("文件大小不能超过10MB");}/*** 处理所有其他异常*/@ExceptionHandler(Exception.class)public ApiResponse<Object> handleException(Exception e) {log.error("系统异常", e);return ApiResponse.error("系统异常: " + e.getMessage());}
}

6.Excel导入响应对象

import lombok.Getter;
import java.util.List;/*** @Description: Excel导入响应对象* @date 2025-09-15 16:24*/
@Getter
public class ExcelImportException extends RuntimeException {private final List<String> errors;private final int totalRows;private final int successRows;private final int failedRows;public ExcelImportException(List<String> errors, int totalRows) {super("Excel导入完成,但有部分数据校验失败,请查看详细信息");this.errors = errors;this.totalRows = totalRows;this.successRows = totalRows - errors.size();this.failedRows = errors.size();}/*** 获取简化的错误摘要信息(用于日志等)*/public String getErrorSummary() {if (errors.isEmpty()) {return "无错误";}StringBuilder summary = new StringBuilder();summary.append("共").append(errors.size()).append("处错误:");// 按错误类型统计errors.forEach(message -> summary.append(message).append("; "));return summary.toString();}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/97971.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/97971.shtml
英文地址,请注明出处:http://en.pswp.cn/web/97971.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Ruby编程实践:20个实用练习

1、编写一个程序,计算一年有多少小时。 以下是两种实现方式的代码: 方式一: puts 24*365方式二: puts 24*365 puts "(or #{24*366} on a leap year)"2、编写一个程序,计算十年中有多少分钟。 以下两种实现方式: 简单计算(未考虑闰年数量差异): ru…

逻辑回归(二):从原理到实战 - 训练、评估与应用指南

引言&#xff1a; 上期我们讲了什么是逻辑回归&#xff0c;了解了它如何利用Sigmoid函数将线性回归的输出转化为概率&#xff0c;并通过最大似然估计来寻找最佳参数。今天&#xff0c;我们将继续这段旅程&#xff0c;学习如何训练这个 模型、如何评估它的表现&#xff0c;以及如…

9.8C++作业

思维导图#include <iostream> #include <vector> #include <fstream> using namespace std;class Stu {friend ofstream &operator<<(ofstream &ofs,const Stu &stu); private:string name;string id;int age;double score; public:Stu(){…

Linux内存管理章节十六:非均匀的内存访问:深入Linux NUMA架构内存管理

引言 在传统的SMP&#xff08;对称多处理&#xff09;系统中&#xff0c;所有CPU核心通过一条共享总线访问同一块内存&#xff0c;所有内存访问延迟是均匀的&#xff08;UMA&#xff09;。然而&#xff0c;随着CPU核心数量的增加&#xff0c;共享总线成为了巨大的性能和 scalab…

【论文翻译】Seg-Zero: Reasoning-Chain Guided Segmentation via Cognitive Reinforcement

0. 摘要Traditional methods for reasoning segmentation rely on supervised fine-tuning with categorical labels and simple descriptions, limiting its out-of-domain generalization and lacking explicit reasoning processes. To address these limitations, we propo…

Playwright MCP浏览器自动化教程

你是否曾厌倦在编程软件和浏览器之间反复切换&#xff0c;只为了检查AI生成的代码能否正常运行&#xff1f;现在&#xff0c;有了Playwright MCP&#xff08;Model Context Protocol&#xff09;&#xff0c;你可以直接让AI自己操作浏览器&#xff0c;查看自己写的代码运行效果…

矩阵中遍历某个点周围的九个点

又是学习新知识的一天,以下为Java版本部分关键代码int[] neighbors {0, 1, -1};int rows board.length;int cols board[0].length;int[][] copyBoard new int[rows][cols];for (int row 0; row < rows; row) {for (int col 0; col < cols; col) {int liveNeighbors…

单例模式:只有一个对象

目录 什么是单例模式 能解决什么问题 使用场景 如何实现 __new__ 方法&#xff1a;经典又直接 装饰器&#xff1a;不改类本身&#xff0c;也能单例 模块本身就是单例 注意事项 总结 你有没有过这样的困扰&#xff1a; “为什么我明明只创建了一次数据库连接&#xff0…

AI大模型学习(6)Yolo V8神经网络的基础应用

Yolo V8神经网络的基础应用2024-2025年最火的目标检测神器&#xff0c;一篇文章让你彻底搞懂&#xff01;&#x1f929;大家好呀&#xff01;今天我们要聊一聊计算机视觉领域的「明星模型」——YOLO神经网络&#xff01;&#x1f3af; 如果你对「目标检测」这个词还比较陌生&am…

C++:imagehlp库

imagehlp库1. 简介2. 主要函数与用途2.1PE 文件解析相关2.2 符号处理相关2.3 崩溃转储相关2.4 版本资源相关3. 使用示例3.1 解析内存地址对应的函数名和行号3.2 创建目录使用示例1. 简介 imagehlp 是 Windows 系统提供的一个图像处理与调试辅助 API 库&#xff08;Image Helpe…

如何在Anaconda中配置你的CUDA Pytorch cuNN环境(2025最新教程)

目录 一、简介 二、下载CUDA 三、下载Pytorch-GPU版本 四、下载CUDNN 五、总结 六、测试代码 一、简介 啥是Anaconda?啥是CUDA?啥是CUDNN&#xff1f;它们和Pytorch、GPU之间有啥关系? 怎么通俗解释它们三者的用途和关系&#xff1f; 1.GPU(图形处理单元&#xff09…

算法面试(1)-----目标检测和图像分类、语义分割的区别

操作系统&#xff1a;ubuntu22.04 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 目标检测&#xff08;Object Detection&#xff09;、图像分类&#xff08;Image Classification&#xff09;、语义分割&#xff08;Semantic Segmentation&#xff09; 是计算机视…

电脑散热风扇有噪音怎么解决

一、初步检查与清理断电并拆机关闭电脑并拔掉电源&#xff0c;打开机箱侧板&#xff08;笔记本需先拆除后盖螺丝&#xff09;。操作前建议佩戴防静电手环&#xff0c;避免静电损坏硬件。清理风扇及散热片灰尘使用压缩空气罐从风扇进风口吹走灰尘&#xff0c;或用软毛刷轻轻刷去…

SeaweedFS深度解析(九):k8s环境使用helm部署Seaweedfs集群

上一篇&#xff1a;《SeaweedFS深度解析&#xff08;八&#xff09;&#xff1a;k8s环境使用Operator部署Seaweedfs集群》 链接: link #作者&#xff1a;闫乾苓 文章目录k8s环境使用helm部署Seaweedfs集群准备镜像seaweed-master-localpv-storageclass.yamlseaweed-volume-lo…

MATLAB绘制一个新颖的混沌图像(新四翼混沌系统)

新四翼混沌系统:dx/dt a(y - x) yz dy/dt cx - y - xz dz/dt -bz xyMATLAB代码:function plot_novel_chaotic_system() % 参数设置 a 10; b 8/3; c 28;% 初始条件 x0 [1, 1, 1];% 时间范围 tspan [0 100];% 求解微分方程 [t, x] ode45((t, x) chaotic_system(t, x, …

金融数据---获取股票日线数据

获取股票日线的数据方式有很多&#xff0c;包括东方财富&#xff0c;同花顺&#xff0c;tushare&#xff0c;这里我们就利用东方财富的数据&#xff0c;是免费的开源获取&#xff0c;第一步先安装akshare&#xff0c;pip安装就可以py -m pip install akshareAkshare 股票数据获…

Mac 真正多显示器支持:TESmart USB-C KVM(搭载 DisplayLink 技术)如何实现

多显示器已经不再是奢侈品&#xff0c;而是专业人士提升生产力的必需工具。无论是创意设计师、股票交易员还是软件开发人员&#xff0c;多屏幕都能让工作流程更高效、更有条理。 然而&#xff0c;Mac 用户长期以来面临一个主要障碍&#xff1a;macOS 原生不支持多流传输&#x…

【实时Linux实战系列】静态链接与libc选择:musl vs glibc的时延权衡

背景与重要性 在实时系统开发中&#xff0c;选择合适的C标准库&#xff08;libc&#xff09;和链接方式对系统的启动时间、线程性能和内存分配效率有着显著影响。glibc和musl是两种流行的C标准库实现&#xff0c;它们在设计目标和性能表现上存在差异。通过对比这两种libc在启动…

Altium Designer(AD24)的三种文件组织形式,工程文件,自由文件与存盘文件

🏡《专栏目录》 目录 1,概述 2,工程文件 3,自由文件 4,存盘文件 5,文件转换 5.1,工程文件于自由文件互转换 5.2,工程文件于存盘文件互转换 6,注意事项 1,概述 本文介绍Altium Designer 24软件(后文简称AD24或软件)的三种文件组织形式,工程文件,自由文件和存盘文…

Python+Selenium实现自动化测试

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快安装selenium打开命令控制符输入&#xff1a;pip install -U selenium火狐浏览器安装firebug&#xff1a;www.firebug.com&#xff0c;调试所有网站语言&#xff0…