news 2026/10/8 5:38:39

Spring Boot基于POI实现Excel导入导出:从工程搭建到避坑实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot基于POI实现Excel导入导出:从工程搭建到避坑实战

简介:面向Spring Boot开发者的Java解析Excel与数据库双向交互代码示例包,聚焦“Excel文件批量导入数据库”和“数据库数据导出为Excel”两大高频需求,适合后端开发、数据管理场景中的初学者及需要快速实现报表导出的项目组。资源共22个文件,压缩包约40KB,包含10个Java源码文件、2个xlsx测试数据、SQL脚本、application-local.yml配置文件、README说明、Postman接口集合等,其中Java代码覆盖POI解析、Excel导出、MyBatis操作数据库等核心逻辑,SQL文件和yml便于直接搭建运行环境。已有7582人学习下载。包内含README详细操作步骤,配合Postman请求集合和示例Excel,可在SpringBoot项目中快速启动并验证功能。作者还提供了maven配置和常见问题联系邮箱,适合初学者参考或作为项目基础模板,节省从头搭建与调试的时间。

1. 这套 Spring Boot Excel 导入导出工程:先跑通再谈优化

别把 Excel 导入导出想成「读个文件再存一下」的小事。真实项目里,Excel 要解析、要校验、还要批量落库,数据入库之后又经常要倒回去生成 Excel 给业务方用。这套 java 解析 Excel 文件并把数据存入数据库和导出数据为 excel 文件的 SpringBoot 代码示例,就是把两条链路做成了一个能直接跑的完整工程。解压后的 excelhandle 项目里,测试 Excel、SQL 脚本、Postman 请求集合都是齐的,你只需要改一下数据库连接、配好 Maven,启动后照着接口调一遍就能看到数据落库和文件生成的全过程。适合刚接手 Excel 数据接入需求的后端开发,也适合想搞懂 POI 和 Spring Boot 到底怎么配合的从业者。

2. 工程结构与解析库选型:excelhandle 里装了什么,POI 该用哪一套

2.1 先看 zip 里的骨架:一个标准的 Spring Boot 工程长什么样

我拿到 excelhandle.zip 之后做的第一件事,不是解压,而是先看整体结构。这套资源虽然演示的是导入导出,但工程本身是一个完整的 Spring Boot 项目,不是某个单独的类或脚本,这对想照着抄的人其实是好事——你可以直接把它当成一个最小可运行项目来研究,而不是从零拼代码。

路径或文件作用
pom.xmlMaven 依赖入口,Spring Boot 工程的核心配置
mvnw / mvnw.cmd免安装 Maven 的启动脚本,环境缺 Maven 时直接用
src/main/resources/application-local.yml本地数据库连接配置,运行前必须改
other/excel导入测试.xlsx导入接口的测试数据文件
other/excel导出测试.xlsx导出接口的输出对照文件
other/excel.sql建表与初始化数据的 SQL,直接导入数据库
other/excel相关.postman_collection.json预置好的接口请求,Postman 一键导入
README.md操作步骤说明,建议第一步就打开它

解压之后用tree或者 IDEA 直接打开都能看清目录,命令行下的目录结构大概是这样的:

excelhandle/ ├── pom.xml ├── mvnw ├── mvnw.cmd ├── .gitignore ├── src/main/ │ ├── java/... # 控制层、服务层、解析逻辑 │ └── resources/ │ └── application-local.yml └── other/ ├── excel导入测试.xlsx ├── excel导出测试.xlsx ├── excel.sql └── excel相关.postman_collection.json

这里要说明的是,README.md 在资源里被放在根目录,它相当于整个操作流程的地图。我一般会建议第一次打开项目的人按这个顺序走:先看 README → 改 yml → 导入 SQL → 导入 Postman collection → 启动 → 发请求。这套流程本身比看懂代码更重要,因为导入导出的报错有相当一部分不是代码问题,而是环境没对齐。

2.2 解析库选型:XSSF 够用,但你要知道 SXSSF 和 EventModel 的存在

POI 解析 Excel 有几种写法,很多新人一上来就搜索「POI 解析 Excel」,结果看到一堆 HSSF、XSSF、SXSSF、EventModel 的术语就晕了。这里先把它们的边界说清楚:HSSF 处理的是xls老格式,XSSF 处理xlsx格式,SXSSF 是 XSSF 的流式版本,专门处理大数据量导出,EventModel 则是纯 SAX 事件模式,适合几十万行级别的读取。

这套 excelhandle 工程里演示的解析方式是常规的 Workbook + Sheet + Row 遍历,也就是 XSSFWorkbook 这一套写法,同时也能兼容 HSSF。它的优点是代码直观、好调试,适合绝大多数业务表,缺点在于文件大时内存占用高。我个人的判断标准是:单 sheet 超过五万行或者文件超过 20MB,就要开始考虑换 SXSSF 或者直接上事件模式;如果只是几千行,常规遍历完全够用,别为了「高性能」把代码搞复杂。

顺带提一下,工程里用的是 Spring Boot 自带的数据访问方式,读写数据库用的是 JdbcTemplate,而不是 MyBatis。对这套代码来说这是个合理选择,导入导出场景本质上是批量数据操作,JdbcTemplate 的 batchUpdate 写起来更直接。你如果平时更习惯 MyBatis,看代码时注意别去找 Mapper XML,这套工程没有那层东西。

2.3 依赖与配置文件:pom.xml 和 application-local.yml 里容易被忽略的参数

pom.xml 是整个工程能不能跑起来的源头。大多数导入导出报错,最后都归结为 POI 依赖版本不对:要么缺了 poi-ooxml,要么版本和你手头的 JDK 或者 Spring Boot 不匹配。工程里 pom 依赖的核心是这几样,注意注释里标注了它们各自的作用。

<!-- Web 支持,接口入口 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- JDBC 数据访问 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- POI 解析与生成 Excel --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> </dependency>

这个依赖组合的逻辑是:spring-boot-starter-jdbc 帮我们把数据源和 JdbcTemplate 都配好了,不需要额外写 Bean;poi-ooxml 这个坐标同时包含了 XSSF 和 HSSF 的能力,不用再多引一个 poi 主包。版本号我这里不写死,因为工程里锁定了 Spring Boot 的父依赖,由它统一管理 POI 版本。实际修改时你自己按需调整,比如 MySQL 驱动在 8.x 和 5.x 下的 url 写法是不同的,这属于最常见的第一道坑。

然后是 application-local.yml。这个文件名带有 local 后缀,说明它走的是 profile 机制,启动时如果没有指定激活哪个 profile,你会发现数据库配置根本不生效。

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/excel_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver # 关键:不指定的话 application-local.yml 不生效 profiles: active: local

这里的参数有几个值得注意。url 里的serverTimezone=Asia/Shanghai在 MySQL 8.x 下是必须的,不写就会报时区异常;useUnicode=true&characterEncoding=utf8是给导入的中文数据兜底,防止乱码。driver-class-name 如果是旧版 MySQL 驱动,要写成com.mysql.jdbc.Driver,新版才是cj结尾。profiles.active 那段是我建议你自己补上的,生效的 profile 名一定要和文件名后缀对得上,这是最容易被忽略的配置文件细节。

3. 导入链路:解析 Excel 数据并校验落库的三个核心步骤

3.1 从 Workbook 到 Row:读取 Excel 的骨架代码

导入链路的起点是拿到上传的 Excel 文件,然后解析成 Workbook。这里有一个关键认知:Workbook 不等于表格数据,它只是 POI 对 Excel 文件的内存映射,真正读数据要经过 Sheet → Row → Cell 三层。解析的骨架代码是这样写的:

// 从 MultipartFile 构建 Workbook,统一处理 xls 和 xlsx try (InputStream is = file.getInputStream(); Workbook workbook = WorkbookFactory.create(is)) { // 默认读第一个 Sheet,也可以按名称取 Sheet sheet = workbook.getSheetAt(0); // 注意:这里取的是物理行数,最后一行编号要减一 int rowCount = sheet.getPhysicalNumberOfRows(); for (int i = 0; i < rowCount; i++) { Row row = sheet.getRow(i); if (row == null) { continue; // 跳过完全空白的行 } // 取单元格的值,具体逻辑在 3.2 的取值方法里 String name = getCellStringValue(row.getCell(0)); String amount = getCellStringValue(row.getCell(1)); System.out.println("第 " + (i + 1) + " 行: " + name + ", " + amount); } } catch (IOException e) { throw new RuntimeException("Excel 文件解析失败: " + e.getMessage(), e); }

这里有两个细节值得细看。第一个是WorkbookFactory.create(is),这个方法会自动根据文件头判断是 xls 还是 xlsx,比手动判断文件后缀再分别 new HSSFWorkbook 或 XSSFWorkbook 稳妥得多。第二个是getPhysicalNumberOfRows()和getLastRowNum()的区别:前者返回实际有内容的行数,是 1-based 逻辑;后者返回最后一行的索引编号,是 0-based。代码里取的是物理行数,因为遍历场景下物理行数更不容易产生「最后一行读不到」的问题。

参数层面,getRow(i)是按索引取行,如果某一行被格式设置过但没有数据,POI 也会把它当成一个 Row 对象返回,这就是 5.2 节要讲的空行坑。所以代码里if (row == null)的判断只能挡住完全未初始化的行,挡不住「有格式无数据」的行,真正严谨的写法是在业务层再判断这一行的单元格是否全为空。

3.2 DataFormatter 与日期处理:单元格读出来到底是什么类型

很多人在解析 Excel 时翻车,翻在同一个地方:单元格里的内容看起来是日期,读出来却是一串数字;看起来是文本,读出来却带了一堆小数位。这里的原因在于 POI 的 Cell 读取默认按数据类型分派,数字就是 double,日期在内存里也是数值。我用的统一取值方法是这样处理的:

private static final DataFormatter FORMATTER = new DataFormatter(); public static String getCellStringValue(Cell cell) { if (cell == null) { return ""; } // 日期类型优先,识别单元格的日期格式化属性 if (cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) { Date date = cell.getDateCellValue(); return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); } // 其他类型统一走 DataFormatter,保留单元格原本的格式 return FORMATTER.formatCellValue(cell).trim(); }

这个方法的逻辑分两层。第一层是单独处理日期:先用getCellType() == CellType.NUMERIC判断是不是数值型,再用DateUtil.isCellDateFormatted(cell)判断这个数值是不是被格式化成日期的。两个条件同时成立才走日期分支,缺一不可,因为 Excel 里日期本质上是序列号,没有格式化属性它就是普通数字。第二层是其他类型统一交给FORMATTER.formatCellValue(cell),这一步能把数字的千分位、百分号、文本空格都按单元格显示格式转换成字符串。

参数上的注意点有两个。第一,DataFormatter最好定义成类级别的静态常量,因为它内部有缓存和区域设置,每次 new 一个既浪费又不稳定。第二,trim()务必要加,Excel 单元格里前导空格和尾随空格很常见,尤其从别的系统导出的文件,空格是数据错位的头号元凶。如果你要拿这个值去数据库做唯一性校验,建议在写入前再做一次 trim 和空串转换,这个""的默认返回值后面在批量入库时有意义。

3.3 batchUpdate 批量入库:参数设置与分批策略

单条 insert 在数据量小的时候无所谓,一旦 Excel 里有几千行,逐条执行 SQL 的性能会很难看。资源里的做法是用 JdbcTemplate 的 batchUpdate 做批量写入,关键代码大概是这个形态:

public int importRows(List<Map<String, String>> rows) { String sql = "INSERT INTO excel_import(name, amount, remark, import_time) VALUES (?, ?, ?, NOW())"; // 按 500 条一批拆分,避免一次性攒太多导致内存抖动 int batchSize = 500; int total = 0; for (int i = 0; i < rows.size(); i += batchSize) { int end = Math.min(i + batchSize, rows.size()); List<Map<String, String>> batch = rows.subList(i, end); int[] results = jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int index) throws SQLException { Map<String, String> row = batch.get(index); ps.setString(1, row.get("name")); ps.setString(2, row.get("amount")); ps.setString(3, row.get("remark")); } @Override public int getBatchSize() { return batch.size(); } }); total += Arrays.stream(results).sum(); } return total; }

这里要拆开讲三个点。第一,SQL 里用?占位符,参数全部通过setString传入,避免了字符串拼接 SQL 的注入风险,这是批量导入场景下必须要守住的底线。第二,BatchPreparedStatementSetter的getBatchSize()返回的是这一批的实际大小,不是固定 500,因为最后一批可能不足 500,写死的话会报数组越界。第三,batchUpdate的返回结果是一个 int 数组,每个元素表示一条语句影响的行数,累加这个数组能得到本次导入成功的总条数,这个数值在后面做数据一致性校验时有用。

分批参数batchSize = 500是一个经验值。太小,比如 50,数据库往返次数太多;太大,比如 5000,PreparedStatement 和事务日志会吃掉大量内存。500 到 1000 之间是我用下来比较稳的区间。另外注意一个事务边界问题:如果整个导入是一笔大的方法论,应该在方法入口加@Transactional,但分批 batchUpdate 本身已经具备批量执行能力,加不加事务取决于业务对「部分成功」的容忍度。常见的做法是加事务,导入失败就全部回滚,避免 Excel 和数据库出现一半对一半不对的脏数据。

4. 导出链路:查询数据库并生成 Excel 文件的实现细节

4.1 数据准备与导出响应:不要把导出逻辑写在 Controller 里

导出链路和导入正好相反:从数据库查出数据,逐行写入 Excel 对象,最后把文件流写到 HTTP 响应里。这个流程里最容易犯的错是把所有代码堆在 Controller 里,几百行挤在一起,既没法复用也没法测试。我一般会把导出拆成两层:Controller 只负责接收请求和写响应,Service 负责查数据、生成 Workbook,并把 Workbook 交给一个独立的输出方法。

// 导出接口:查数据库 -> 生成 Excel -> 写回 Response @GetMapping("/export") public void export(HttpServletResponse response) { // 1. 查询数据库 List<ExportRow> list = exportService.queryAllForExport(); // 2. 生成 Workbook try (Workbook workbook = exportService.buildWorkbook(list)) { // 3. 设置响应头,告诉浏览器这是一个需要下载的文件 String fileName = URLEncoder.encode("导出数据_" + System.currentTimeMillis(), "UTF-8"); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + ".xlsx\""); // 4. 把 Workbook 写出到响应输出流 workbook.write(response.getOutputStream()); response.flushBuffer(); } catch (IOException e) { log.error("导出 Excel 失败", e); } }

这个流程里值得注意的不是 Workbook 的写入,而是响应头的设置。Content-Type必须是application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,这是 xlsx 的标准 MIME 类型,写错的话浏览器可能直接在内页打开乱码而不是下载。文件名用了URLEncoder.encode做编码,避免中文文件名在下载时变成一串乱码。Content-Disposition的attachment关键字决定浏览器是弹下载还是尝试预览。

为什么说不要把生成逻辑放在 Controller 里?因为buildWorkbook这个方法在导出接口里调用完,将来可能在定时任务里也要调用,比如每天早上自动生成报表发邮件。如果生成逻辑混在 Controller 里,定时任务没法复用。把查询、建 Workbook、写响应拆成三个独立方法,是这套代码里最有复用价值的设计。

4.2 样式与性能取舍:表头样式、大数据量选型

生成 Excel 不只是把数据填进去就行。直接 new 一个 XSSFWorkbook 然后逐行写入,出来的文件能用,但表头没有底色、列宽也不对,业务方拿到手大概率会要求返工。常规做法是先建表头样式,再把列宽按内容长度设置好。样式相关代码一般是这个套路:

// 创建表头样式,对同一个 Workbook 只创建一次 Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("导出数据"); CellStyle headerStyle = workbook.createCellStyle(); Font headerFont = workbook.createFont(); // 关键参数:加粗 + 背景色 + 边框 headerFont.setBold(true); headerStyle.setFont(headerFont); headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); // 表头行,注意样式对象要复用 Row headerRow = sheet.createRow(0); String[] headers = {"ID", "名称", "金额", "备注", "导入时间"}; for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(headerStyle); } // 按列内容长度粗略设置列宽,单位是 1/256 个字符宽度 sheet.setColumnWidth(0, 10 * 256); sheet.setColumnWidth(1, 20 * 256); sheet.setColumnWidth(2, 15 * 256);

样式这块有两个容易踩的参数陷阱。第一,setFillForegroundColor必须配合setFillPattern(SOLID_FOREGROUND)才生效,只设置颜色不设置填充图案,背景色不会出现。第二,CellStyle 对象一定要复用,每行的单元格都createCellStyle()的话,Workbook 里会堆积大量样式对象,导出万行级别的数据时文件体积和内存都会失控,甚至打开文件时提示样式过多需要修复。

如果数据量到了十万行这个级别,XSSFWorkbook 会把所有行对象都在内存里维护,GC 压力很大。这时候就该换成 SXSSFWorkbook,它只保留窗口内的行数据,其余的写进临时文件。切换方式很简单:把new XSSFWorkbook()换成new SXSSFWorkbook(100),100 表示内存里保留的窗口行数,其余行刷入磁盘。代价是 SXSSF 生成的临时文件需要在 finally 里执行dispose()清理,而且像getSheetAt、getLastRowNum这类随机访问方法不再可靠,只适合顺序写的导出场景。所以我的建议是:万行以内用 XSSF 没问题,超过五万行就切 SXSSF,别犹豫。

5. 避坑与排查:五个高频翻车现场与对应修法

5.1 日期字段读出来是数字串

现象:Excel 里明明写着2025-01-15,导入后数据库里变成了45200或者2025-01-15 00:00:00.0,格式完全不对。

原因:POI 在底层把日期存成数值,只有配合单元格的日期格式属性才能识别为日期。纯数字单元格和日期单元格在 getCellType 上都是 NUMERIC,如果你直接getNumericCellValue()再 toString,拿到的自然就是天数序列号。

解决:统一走 3.2 里那个取值方法,先DateUtil.isCellDateFormatted(cell)判断,再getDateCellValue()取 Date,最后用 SimpleDateFormat 转成你需要的格式。注意isCellDateFormatted对某些自定义日期格式可能识别失效,遇到这类文件,可以再加一道保险:根据单元格的getCellStyle().getDataFormatString()是否包含y或m或d字符来二次判断。

5.2 最后几行读不到,或者空行一大堆

现象:解析出来的行数比实际数据少,末尾几行凭空消失;另一种情况相反,明明只有几十条数据,遍历出来两三百行,全是空值。

原因:第一种情况是用了getLastRowNum()当循环上限,它返回的是最后一个有内容的行的索引,但你从 0 开始循环时把它当成了行数去用,天然少一行。第二种情况是 Sheet 里那些被设置过格式、没填数据的行,POI 也把它们算作物理行,getPhysicalNumberOfRows()会把它们当数。

解决:计数时用getPhysicalNumberOfRows(),同时保留row == null判断;更稳妥的是再加一个非空校验,判断这一行的所有关键列是否都有值。我常用的做法是写一个isEmptyRow(Row row)方法,遍历 0 到row.getLastCellNum()的所有单元格,全部为空字符串就视为空行,直接 continue,这样格式空行就被过滤掉了。

5.3 batchUpdate 内存溢出和数据错位

现象:导入几千行时报 OOM,或者导入后数据库里的数据和 Excel 对不上,错位一行、少了一块。

原因:OOM 往往是 List 里一次性放了几万行解析结果,又一次性传给 batchUpdate。数据错位则通常来自两个地方:一是单元格取值顺序和 SQL 里参数顺序不一致,二是 Excel 中某一行的单元格数为空,导致 getCell(索引) 返回 null,后续所有行的索引都往前错了一位。

解决:解析和入库都按 500 条拆批,分批解析、分批提交,避免全量积压在内存。取值顺序固定为getCell(0)到getCell(n),每个值都走统一取值方法,返回""而不是 null。写入 SQL 时按取值顺序从 1 到 n 排列占位符,最后对照 Excel 和数据库抽查三行:首行、中间行、末行,确认没有错位。

5.4 导出的 Excel 打开后提示「需要修复」或「文件损坏」

现象:下载下来的文件能打开,但一打开 Excel 就弹「文件已损坏,是否尝试修复」的对话框,或者直接打不开。

原因:大概率是响应流和 Workbook 的写出顺序出了问题,常见的是先response.getOutputStream()关闭了,再调workbook.write();还有一种是workbook.write()之后没有把 Workbook 关掉,导致文件结尾不完整。POI 生成的文件需要正常 close 才会收尾。

解决:用 try-with-resources 包裹 Workbook,确保写完后自动 close。然后严格按顺序执行:先workbook.write(response.getOutputStream()),再flushBuffer(),最后退出 try 块。另一个细节是导出前检验response.getOutputStream()有没有被前面代码提前getWriter()占用,Servlet 里 getOutputStream 和 getWriter 不能混用,混了也会导致输出流异常。

5.5 数据库连接配置改了却启动就报错,或者配置不生效

现象:明明改了 application-local.yml 里的数据库地址和密码,启动时还是连到旧的库,或者直接报Failed to configure a DataSource。

原因:这在大多数情况下是配置文件没被加载。Spring Boot 默认加载 application.yml 或者 application.properties,带后缀的 application-local.yml 需要设置spring.profiles.active=local才会被识别。还有就是 IDEA 里启动时没有指定 spring profile,或者 Maven 编译没把 resources 目录过滤进去,导致 yml 压根没出现在 target 里。

解决:先把spring.profiles.active: local显式写进主配置文件或启动参数,--spring.profiles.active=local这种方式在命令行和 IDEA 的 Program arguments 里都能用。然后用启动日志确认拿到的 url 是什么,日志里通常直接打出数据源 URL,看到你改的那条连接串就说明 profile 生效了。还要检查 yml 文件的缩进,Spring Boot 对缩进很敏感,datasource少缩进一格就识别成别的配置节点。

6. 用 Postman 跑通导入导出闭环:顺手做一次数据一致性校验

这套资源里配好了 Postman 请求集合,路径在other/excel相关.postman_collection.json。我用它跑通一次完整闭环的顺序是这样的:先导入请求,选择 body 为 form-data,文件字段选other/excel导入测试.xlsx,发送后看返回是否提示成功和受影响行数;再调用导出请求,Postman 里点 Send and Download 把响应存成 xlsx 文件。这一步跑通并不意味着万事大吉,我习惯紧接着手动做两次验证。

第一次验证是数据一致性。导入接口返回的行数,和用 SQL 查SELECT COUNT(*) FROM excel_import得到的数量必须一致。然后把导出的 xlsx 再通过 Postman 导入一次,这次走一条独立校验:对比两次导入后数据库的记录数增量是否正确,增量应该是第一次导入的行数。如果能对上,说明导入没漏行、导出没丢数据,整个链路是闭环的。

第二次验证是内容抽查,重点看两类数据:日期字段是否还是yyyy-MM-dd HH:mm:ss格式,金额字段有没有带上E的科学计数法。如果导出文件里日期正常、金额正常,说明 3.2 的取值方法和 4.2 的样式设置都生效了。这套工程里还有一个excel导出测试.xlsx放在 other 目录,你可以直接拿它和数据库查询结果逐行比对,它相当于是导出结果的参照物,两边的列数和行数对得上,基本上代码就没问题了。

最后说一个真实经历。之前有个同事接手了别人写的导入需求,连着两天都跟我说「数据导进去了但是好像少了几行」。他少做的一件事就是把导出的文件再导回去做对照。后来我让他把导入流程里每一批 batchUpdate 返回的影响行数累加,和解析出来的行数对账,立刻发现是空行判断漏了,把带格式的空行也算成有数据,最终导致的不是文件问题,而是结果没法自洽。从那以后,我每次接 Excel 导入导出需求,都会强制自己走一遍「导入→导出→对比记录数」的闭环,导出的那张表就是导入数据的镜子,错位、丢行、类型损坏都在这一轮现形。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 5:38:33

Java课设:影碟出租管理系统(Swing+MySQL)开发指南

简介&#xff1a;这是一份面向Java课程设计的影碟出租管理系统源码&#xff0c;采用GUI/Swing界面配合MySQL数据库&#xff0c;适合正在完成课设或需要快速搭建可运行Demo的高校学生。资源包共49个文件&#xff0c;压缩包大小仅153KB&#xff0c;内容涵盖Java源代码、数据库初始…

作者头像 李华
网站建设 2026/10/8 5:37:50

从巨型Prompt到技能包:AI Agent开发中的Skills机制实践

刚接触 Agent 开发的时候&#xff0c;我对skills这个词的理解还停留在"把常用的提示词存成模板"这个层面。真正让我改变想法的&#xff0c;是一个被几千行 system prompt 逼疯的下午。指令越堆越长&#xff0c;模型输出越来越飘&#xff0c;改一句话要全量重发一遍上…

作者头像 李华
网站建设 2026/10/8 5:37:12

Agent-Reach:生产级Agent的架构、并发与安全实践

做Agent-Reach这个项目&#xff0c;起因是一个很实在的痛点&#xff1a;市面上的Agent演示&#xff0c;绝大多数都停在“能聊天”这一步。你跟它聊得再流畅&#xff0c;一旦要它去查数据库、调第三方接口、操作内部系统、把任务闭环跑完&#xff0c;它就露馅了。关键词Agent听起…

作者头像 李华
网站建设 2026/10/8 5:34:54

AI应用开发安全:从Prompt注入到生产级纵深防御

1. 这不是“加个防火墙”就能搞定的事&#xff1a;AI应用开发安全的底层逻辑变了 “AI应用开发安全方案大全&#xff1a;从代码落地到生产级纵深防御”——这个标题里每个词都不是虚的。我带团队做过7个从0到1上线的AI应用&#xff0c;其中3个在灰度期就被发现存在提示注入、模…

作者头像 李华
网站建设 2026/10/8 5:34:18

游戏引擎对象与资源管理:从组件模式到ECS架构

1. 从“一个对象”到“一套系统”&#xff1a;游戏对象模型的设计演进聊到游戏对象&#xff0c;很多刚入行的朋友第一反应就是“类呗&#xff0c;写个 GameObject 类&#xff0c;里面有 Transform、Mesh、Material&#xff0c;再挂个脚本”。这种思路本身没错&#xff0c;但真正…

作者头像 李华
网站建设 2026/10/8 5:33:48

游戏引擎架构深度解析:对象生命周期与资源管理实战

写这篇游戏引擎架构深度解析的第四篇时&#xff0c;我一直在想一个很实际的问题&#xff1a;很多引擎初学者能熟练摆弄场景里的物体&#xff0c;却说不清一个游戏对象从创建到销毁经历了什么&#xff0c;更别说背后那套资源管理体系是怎么支撑起整个世界的。游戏对象和资源管理…

作者头像 李华