拒绝重复造轮子:利用自定义注解封装POI,实现Java通用Excel解析

拒绝重复造轮子:利用自定义注解封装POI,实现Java通用Excel解析

基于 SSM + Vue 的 POI Excel 导入导出全流程实现

本文详细讲解基于 SSM(Spring + SpringMVC + MyBatis)后端与 Vue + Element UI 前端的 Excel 导入导出功能实现。通过自定义注解的方式,实现通用的 POI 操作流程。


1. 环境准备与依赖引入

首先需要在项目的 pom.xml 中引入 Apache POI 及相关工具类的依赖。

<!-- POI 核心依赖(支持 .xls 格式 - Office 2003) --><dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>4.1.2</version></dependency><!-- POI OOXML(支持 .xlsx 格式 - Office 2007+,目前主流) --><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>4.1.2</version></dependency><!-- Commons BeanUtils:用于简化实体类属性的反射操作 --><dependency><groupId>commons-beanutils</groupId><artifactId>commons-beanutils</artifactId><version>1.9.4</version></dependency><!-- Commons FileUpload:SpringMVC 处理文件上传的核心依赖 --><dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.4</version></dependency>

2. 后端核心架构设计

2.1 自定义注解

通过注解标记实体类与 Excel 表格的映射关系,实现通用解析。

  1. @ExcelField
    • 作用:标注在实体类的字段上。
    • 功能:定义字段在 Excel 表头中对应的中文名称、排序号、是否参与导入/导出等。
    • 元注解@Target(ElementType.FIELD)
  2. @ExcelEntity
    • 作用:标注在实体类上。
    • 功能:定义实体类对应的 Excel 表名(Sheet 名)。
    • 元注解@Target(ElementType.TYPE)
图4:ExcelEntity 注解使用示例
图3:实体类字段使用示例
图2:ExcelField 注解属性
图1:ExcelField 注解定义

2.2 工具类设计

  • ExcelAnnotationUtil:负责解析实体类上的注解,生成“表头-字段”的映射关系。
  • ExcelUtil:核心工具类,负责创建 Workbook、填充数据(导出)以及读取 Workbook、封装实体(导入)。
    • 注:完整工具类代码见文末附录。

3. 业务逻辑层 (Service) 实现

3.1 导入逻辑

思路:Controller 接收文件 -> 调用工具类解析 Excel 为 List<User> -> 调用 Mapper 批量插入数据库。

图5:Service层导入代码示例

3.2 导出逻辑

思路:Mapper 查询所有数据 -> 调用工具类解析注解(获取表头和Sheet名) -> 生成 Excel 二进制流 -> 通过 HttpServletResponse 输出。

图6:Service层导出代码示例

4. SpringMVC 配置与 Controller

4.1 配置文件上传解析器

在 SpringMVC 配置文件中添加 MultipartResolver,否则后端无法接收 MultipartFile

<!-- 配置文件上传解析器 --><beanid="multipartResolver"class="org.springframework.web.multipart.commons.CommonsMultipartResolver"><!-- 最大上传文件大小:10MB (10 * 1024 * 1024) --><propertyname="maxUploadSize"value="10485760"/><!-- 默认编码格式,防止文件名乱码 --><propertyname="defaultEncoding"value="UTF-8"/></bean>

4.2 Controller 编写

定义导入和导出两个接口。

图7:Controller层接口定义

5. 前端实现 (Vue + Element UI)

5.1 界面组件

使用 Element UI 的按钮组件触发操作。

图8:前端界面按钮

5.2 导出功能实现 (Blob 流处理)

核心逻辑

  1. 调用后端接口,设置响应类型为 blob
  2. 接收二进制流,创建 Blob 对象。
  3. 创建临时的 <a> 标签触发下载。
  4. 释放 URL 对象。

Axios 封装

图9:Axios请求封装

业务代码

handleExport(){this.$message.info('正在导出数据,请稍等...');exportUserExcel().then(response=>{// 1. 校验响应是否为有效 blobif(!response.data || response.data.size ===0){this.$message.error('导出失败:无数据可导出');return;}// 2. 创建 Blob 对象,指定 MIME 类型为 Excelconst blob =newBlob([response.data],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});// 3. 创建下载链接const link = document.createElement('a');// 使用 encodeURIComponent 解决中文文件名乱码const fileName =encodeURIComponent('用户列表.xlsx'); link.href = window.URL.createObjectURL(blob); link.download =decodeURIComponent(fileName);// 下载属性设为解码后的文件名// 4. 触发点击并清理 link.click(); window.URL.revokeObjectURL(link.href);// 释放内存this.$message.success('导出成功');}).catch(err=>{// 处理错误信息(注意:Blob 报错时可能需要将 Blob 转回 JSON 才能读取 msg)this.$message.error('导出失败:'+(err.response?.data?.msg || err.message)); console.error(err);});},

5.3 导入功能实现 (FormData 上传)

核心逻辑

  1. 点击按钮触发隐藏的 <input type="file">
  2. handleFileChange 捕获文件。
  3. 前端校验文件格式(.xls/.xlsx)和大小(<10MB)。
  4. 使用 FormData 封装文件并发送请求。
图10:文件选择框隐藏逻辑

业务代码

handleImport(){// 触发隐藏的文件输入框this.$refs.fileInput.click();},handleFileChange(event){const file = event.target.files[0];if(!file)return;// 1. 前端校验文件类型const isExcel = file.type ==='application/vnd.ms-excel'|| file.type ==='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';// 2. 前端校验文件大小 (10MB)const isLt10M = file.size /1024/1024<10;if(!isExcel){this.$message.error('只能上传xls/xlsx格式的Excel文件!'); event.target.value ='';// 清空选择,允许重复选择同名文件return;}if(!isLt10M){this.$message.error('文件大小不能超过10MB!'); event.target.value ='';return;}// 3. 调用导入接口importUserExcel(file).then(()=>{this.$message.success('导入成功');this.fetchUsers();// 刷新列表 event.target.value ='';}).catch(err=>{this.$message.error('导入失败:'+(err.response?.data?.msg || err.message)); console.error(err); event.target.value ='';});},

API 调用

图11:导入API封装

6. 效果展示

导入成功

图14:导入前列表


图15:导入后列表刷新

导出成功

图12:导出文件下载


图13:导出Excel内容

7. 常见问题与解决方案

7.1 导出的 Excel 无法打开 (文件损坏)

图16:Excel文件损坏报错
可能原因解决方案
前端未设置 Blob前端 Axios 请求必须添加 responseType: 'blob',否则二进制流会被当做字符串/JSON 解析导致乱码。
后端流未关闭后端 ExcelUtil 中必须执行 os.flush()os.close(),确保缓冲区数据完全写入。
拦截器干扰项目中的全局响应拦截器(Response Interceptor)可能强制解析 JSON。需添加判断:如果是 Blob 类型则直接返回,不进行 JSON 解析。

7.2 导入 Excel 报 500 错误

可能原因解决方案
未配置解析器检查 SpringMVC 配置文件是否包含 CommonsMultipartResolver Bean。
请求头错误前端必须使用 FormData 对象封装文件,浏览器会自动设置 Content-Type: multipart/form-data
参数名不一致后端 @RequestParam("file") 中的名称必须与前端 formData.append('file', file) 中的 key 保持一致。

8. 附录:核心工具类源码

8.1 ExcelAnnotationUtil.java

用于解析注解,建立映射关系。

/** * @className: ExcelAnnotationUtil * @description: Excel注解解析工具类:解析实体类注解,生成表头/字段映射 */publicclassExcelAnnotationUtil{/** * 解析实体类注解,生成导出用的表头和字段数组 * @param clazz 实体类Class * @return 返回数组:[0]headers(表头名称数组),[1]fields(实体类字段名数组) */publicstatic<T>String[][]parseExportAnnotation(Class<T> clazz){returnparseAnnotation(clazz,true);}/** * 解析实体类注解,生成导入用的字段数组 * @param clazz 实体类Class * @return 返回数组:[0]headers(表头用于校验),[1]fields(实体类字段名用于赋值) */publicstatic<T>String[][]parseImportAnnotation(Class<T> clazz){returnparseAnnotation(clazz,false);}/** * 通用注解解析逻辑 * @param isExport true为导出模式,false为导入模式(根据注解中的 isExport/isImport 属性过滤) */privatestatic<T>String[][]parseAnnotation(Class<T> clazz,boolean isExport){// 获取所有声明的字段Field[] declaredFields = clazz.getDeclaredFields();// 使用 TreeMap 存储字段信息,key 为 sort 值,实现自动按 sort 升序排列Map<Integer,Map<String,String>> fieldMap =newTreeMap<>();for(Field field : declaredFields){ExcelField excelField = field.getAnnotation(ExcelField.class);if(excelField ==null){continue;// 跳过无注解的字段 }// 根据模式过滤字段if(isExport &&!excelField.isExport()){continue;}if(!isExport &&!excelField.isImport()){continue;}// 封装字段名、表头名Map<String,String> fieldInfo =newTreeMap<>(); fieldInfo.put("fieldName", field.getName()); fieldInfo.put("headerName", excelField.name());// 存入 Map 进行排序 fieldMap.put(excelField.sort(), fieldInfo);}// 将 Map 转换为数组返回List<String> headers =newArrayList<>();List<String> fields =newArrayList<>();for(Map<String,String> info : fieldMap.values()){ headers.add(info.get("headerName")); fields.add(info.get("fieldName"));}returnnewString[][]{ headers.toArray(newString[0]), fields.toArray(newString[0])};}/** * 获取字段的注解类型(用于导入时的类型转换) * 例如:Excel 中是文本,但实体类是 Integer,需要获取该类型进行转换 */publicstatic<T>Class<?>getFieldType(Class<T> clazz,String fieldName){try{Field field = clazz.getDeclaredField(fieldName);ExcelField excelField = field.getAnnotation(ExcelField.class);// 默认为 String 类型return excelField ==null?String.class: excelField.type();}catch(NoSuchFieldException e){ e.printStackTrace();returnString.class;}}}

8.2 ExcelUtil.java

POI 操作核心类,包含样式设置、数据读写、类型转换。

/** * 适配注解的 POI 工具类(支持 .xlsx 和类型转换) */publicclassExcelUtil{// ====================== 导出逻辑 ====================== publicstatic<T>voidexportExcel(HttpServletResponse response,List<T> dataList,String[] headers,String[] fields,String fileName){// 1. 创建工作簿 (XSSFWorkbook 支持 .xlsx)Workbook workbook =newXSSFWorkbook();Sheet sheet = workbook.createSheet("数据列表");// 2. 创建表头行并设置样式 Row headerRow = sheet.createRow(0);CellStyle headerStyle =getHeaderStyle(workbook);for(int i =0; i < headers.length; i++){Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(headerStyle); sheet.autoSizeColumn(i);// 简单的自适应列宽}// 3. 写入数据行 if(dataList !=null&&!dataList.isEmpty()){CellStyle contentStyle =getContentStyle(workbook);for(int rowNum =0; rowNum < dataList.size(); rowNum++){Row dataRow = sheet.createRow(rowNum +1);T data = dataList.get(rowNum);// 遍历字段数组,通过反射获取值for(int colNum =0; colNum < fields.length; colNum++){Cell cell = dataRow.createCell(colNum); cell.setCellStyle(contentStyle);try{// 使用 BeanUtils 获取属性值String value =BeanUtils.getProperty(data, fields[colNum]); cell.setCellValue(value ==null?"": value);}catch(Exception e){ cell.setCellValue(""); e.printStackTrace();}}}}// 4. 写入响应流 try{ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");// 防止中文文件名乱码 response.setHeader("Content-Disposition","attachment;filename="+URLEncoder.encode(fileName,"UTF-8"));OutputStream os = response.getOutputStream(); workbook.write(os); os.flush();// 关键:刷新缓冲区 os.close(); workbook.close();}catch(Exception e){ e.printStackTrace();}}// ====================== 导入逻辑 ====================== publicstatic<T>List<T>importExcel(MultipartFile file,Class<T> clazz){List<T> dataList =newArrayList<>();if(file.isEmpty()){return dataList;}// 1. 解析导入注解,获取表头和字段映射 String[][] annotationData =ExcelAnnotationUtil.parseImportAnnotation(clazz);String[] headers = annotationData[0];// Excel表头(用于校验) String[] fields = annotationData[1];// 实体类字段名 try{// WorkbookFactory 自动识别 xls 或 xlsxWorkbook workbook =WorkbookFactory.create(file.getInputStream());Sheet sheet = workbook.getSheetAt(0);if(sheet ==null){return dataList;}// 2. 校验 Excel 表头(可选,确保导入文件格式正确) Row headerRow = sheet.getRow(0);if(headerRow ==null){thrownewRuntimeException("Excel无表头行");}for(int i =0; i < headers.length; i++){Cell cell = headerRow.getCell(i);String cellValue =getCellValue(cell);if(!headers[i].equals(cellValue)){thrownewRuntimeException("Excel表头错误:第"+(i+1)+"列应为【"+ headers[i]+"】,实际为【"+ cellValue +"】");}}// 3. 逐行解析数据 + 类型转换 int lastRowNum = sheet.getLastRowNum();for(int rowNum =1; rowNum <= lastRowNum; rowNum++){Row row = sheet.getRow(rowNum);if(row ==null)continue;T data = clazz.newInstance();// 实例化对象for(int colNum =0; colNum < fields.length; colNum++){Cell cell = row.getCell(colNum);String cellValue =getCellValue(cell);if(cellValue.isEmpty())continue;// 空值跳过 // 核心:根据注解指定的类型转换值 String fieldName = fields[colNum];Class<?> fieldType =ExcelAnnotationUtil.getFieldType(clazz, fieldName);// 使用 ConvertUtils 进行类型转换 (String -> Integer/Date等)Object convertValue =ConvertUtils.convert(cellValue, fieldType);// 赋值给实体类 BeanUtils.setProperty(data, fieldName, convertValue);} dataList.add(data);} workbook.close();}catch(Exception e){ e.printStackTrace();thrownewRuntimeException("导入失败:"+ e.getMessage());}return dataList;}// ====================== 私有工具方法 (样式与单元格读取) ====================== /** 获取表头样式(加粗、居中、浅灰背景、边框) */privatestaticCellStylegetHeaderStyle(Workbook workbook){CellStyle style = workbook.createCellStyle(); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); style.setBorderTop(BorderStyle.THIN); style.setBorderBottom(BorderStyle.THIN); style.setBorderLeft(BorderStyle.THIN); style.setBorderRight(BorderStyle.THIN);Font font = workbook.createFont(); font.setBold(true); font.setFontName("微软雅黑"); font.setFontHeightInPoints((short)12); style.setFont(font); style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND);return style;}/** 获取内容样式(居中、边框) */privatestaticCellStylegetContentStyle(Workbook workbook){CellStyle style = workbook.createCellStyle(); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); style.setBorderTop(BorderStyle.THIN); style.setBorderBottom(BorderStyle.THIN); style.setBorderLeft(BorderStyle.THIN); style.setBorderRight(BorderStyle.THIN);Font font = workbook.createFont(); font.setFontName("微软雅黑"); font.setFontHeightInPoints((short)11); style.setFont(font);return style;}/** 统一获取单元格值(适配 String, Numeric, Boolean, Formula) */privatestaticStringgetCellValue(Cell cell){if(cell ==null)return"";CellType cellType = cell.getCellType();switch(cellType){case STRING:return cell.getStringCellValue().trim();case NUMERIC:// 处理日期格式if(DateUtil.isCellDateFormatted(cell)){return cell.getDateCellValue().toString();}else{// 防止数字变成科学计数法,这里简单转 StringreturnString.valueOf(cell.getNumericCellValue()).trim();}case BOOLEAN:returnString.valueOf(cell.getBooleanCellValue());case FORMULA:// 递归获取公式计算后的值returngetCellValue(cell.getCachedFormulaResultType(), cell);default:return"";}}/** 处理公式单元格的结果 */privatestaticStringgetCellValue(CellType cellType,Cell cell){if(cellType ==CellType.NUMERIC){returnString.valueOf(cell.getNumericCellValue()).trim();}elseif(cellType ==CellType.STRING){return cell.getStringCellValue().trim();}else{return"";}}}

Read more

OpenClaw 技术深度解析从智能助手到自动化引擎的范式革命

OpenClaw 技术深度解析从智能助手到自动化引擎的范式革命

摘要:OpenClaw是您在自己的设备上运行的个人人工智能助手。它会在您已使用的渠道上回答您(WhatsApp、Telegram、Slack、Discord、Google Chat、Signal、iMessage、BlueBubbles、IRC、Microsoft Teams、Matrix、飞书、LINE、Mattermost、Nextcloud Talk、Nostr、Synology Chat、Tlon、Twitch、Zalo、Zalo Personal、WebChat)。它可以在 macOS/iOS/Android 上说话和听,并可以渲染您控制的实时画布。网关只是控制平面——产品是助手。 如果您想要一个感觉本地化、快速且始终在线的个人单用户助手,那么这就是它。 目录 1. OpenClaw 概述 1.1 定义与核心定位 1.2

By Ne0inhk
Docker快速入门上手教程(保姆式),含docker所有常用命令大全(详细)!

Docker快速入门上手教程(保姆式),含docker所有常用命令大全(详细)!

目录 一、Docker 是什么?为什么需要它? 二、核心概念:镜像、容器、仓库 三、"保姆式"实战:5分钟部署你的第一个Nginx网站 步骤一:搜索并拉取 Nginx 镜像 步骤二:查看本地镜像 步骤三:运行容器 步骤四:验证容器运行 步骤五:查看容器日志 步骤六:进入容器内部 步骤七:停止并删除容器 步骤八:删除镜像 四、Docker 常用命令大全(超详细) (一) 镜像相关命令 (Image Commands) (二) 容器相关命令 (Container Commands) (三) 其他常用命令 五、总结

By Ne0inhk
Flutter 组件 r_flutter 的适配 鸿蒙Harmony 实战 - 驾驭资源映射自动化、实现鸿蒙端资产强类型引用与资产冲突静态校验方案

Flutter 组件 r_flutter 的适配 鸿蒙Harmony 实战 - 驾驭资源映射自动化、实现鸿蒙端资产强类型引用与资产冲突静态校验方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 r_flutter 的适配 鸿蒙Harmony 实战 - 驾驭资源映射自动化、实现鸿蒙端资产强类型引用与资产冲突静态校验方案 前言 在鸿蒙(OpenHarmony)的大型 UI 工程开发中,“资源管理”是一个极易产生低级错误的重灾区。面对动辄几百个图标(PNG/SVG)、各种自定义字体文件以及多层级的资源目录。如果我们依然使用硬编码字符串(如 Image.asset('assets/images/home_icon_v2_final.png')),那么不仅毫无代码提示可言,由于文件名拼写错误引发的运行期资源丢失(Missing Asset)更是家常便饭。 我们需要一种“代码即资产”的强类型保护。 r_flutter

By Ne0inhk
MySQL 动态分区管理:自动化与优化实践

MySQL 动态分区管理:自动化与优化实践

个人名片 🎓作者简介:java领域优质创作者 🌐个人主页:码农阿豪 📞工作室:新空间代码工作室(提供各种软件服务) 💌个人邮箱:[[email protected]] 📱个人微信:15279484656 🌐个人导航网站:www.forff.top 💡座右铭:总有人要赢。为什么不能是我呢? * 专栏导航: 码农阿豪系列专栏导航 面试专栏:收集了java相关高频面试题,面试实战总结🍻🎉🖥️ Spring5系列专栏:整理了Spring5重要知识点与实战演练,有案例可直接使用🚀🔧💻 Redis专栏:Redis从零到一学习分享,经验总结,案例实战💐📝💡 全栈系列专栏:海纳百川有容乃大,可能你想要的东西里面都有🤸🌱🚀 目录 * MySQL 动态分区管理:自动化与优化实践 * 一、分区的基本概念 * 二、动态分区的需求 * 三、使用存储过程动态创建分区 * 四、使用事件调度器自动化分区管理 * 五、避免分区冲突

By Ne0inhk