ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- JDBC 数据访问 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- POI 解析与生成 Excel -- dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId /dependency这个依赖组合的逻辑是spring-boot-starter-jdbc 帮我们把数据源和 JdbcTemplate 都配好了不需要额外写 Beanpoi-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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver # 关键不指定的话 application-local.yml 不生效 profiles: active: local这里的参数有几个值得注意。url 里的serverTimezoneAsia/Shanghai在 MySQL 8.x 下是必须的不写就会报时区异常useUnicodetruecharacterEncodingutf8是给导入的中文数据兜底防止乱码。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(ListMapString, 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()); ListMapString, String batch rows.subList(i, end); int[] results jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { Override public void setValues(PreparedStatement ps, int index) throws SQLException { MapString, 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数据库往返次数太多太大比如 5000PreparedStatement 和事务日志会吃掉大量内存。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. 查询数据库 ListExportRow 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.activelocal才会被识别。还有就是 IDEA 里启动时没有指定 spring profile或者 Maven 编译没把 resources 目录过滤进去导致 yml 压根没出现在 target 里。解决先把spring.profiles.active: local显式写进主配置文件或启动参数--spring.profiles.activelocal这种方式在命令行和 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 导入导出需求都会强制自己走一遍「导入→导出→对比记录数」的闭环导出的那张表就是导入数据的镜子错位、丢行、类型损坏都在这一轮现形。希望帮到你。本文还有配套的精品资源点击获取
返回列表