Excel 导入导出工具类文档
Excel 导入导出工具类详解:基于 Spring 和 Apache POI 的通用解决方案
本文分享一套功能强大的 Excel 导入导出工具类,基于 Spring 框架和 Apache POI 实现,支持注解驱动的字段映射、数据类型自动转换、样式定制等高级功能。
工具类核心功能
- 智能导入:支持 Excel 数据到 Java 对象的自动转换
- 灵活导出:Java 对象到 Excel 的自动导出
- 注解驱动:通过注解控制字段映射行为
- 类型转换:自动处理常见数据类型(日期、数值、布尔值等)
- 错误处理:导入时跳过错误行,导出时标记错误字段
核心工具类源码
1. Excel 导入工具类 (ExcelImportUtil.java)
package com.xk.toolkit.util.file.excel;import com.xk.toolkit.util.file.excel.property.ExcelIgnore;
import com.xk.toolkit.util.file.excel.property.ExcelProperty;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;/*** Excel导入工具类** <p>提供通用的Excel导入功能,支持基于注解的字段映射和类型转换</p>** <p>功能特点:* <ul>* <li>基于反射机制实现通用导入</li>* <li>支持字段过滤(@ExcelIgnore)</li>* <li>支持自定义列名和日期格式(@ExcelProperty)</li>* <li>自动类型转换(字符串、数值、日期、布尔值)</li>* <li>公式单元格处理</li>* <li>智能列名匹配(支持大小写不敏感匹配)</li>* <li>容错处理(跳过空行和错误行)</li>* </ul>*/
public class ExcelImportUtil {/*** 通用Excel导入方法** @param <T> 数据类型泛型* @param file 上传的Excel文件* @param clazz 目标对象类型* @return 导入成功的数据对象列表* @throws IOException 当文件解析失败时抛出** <p>执行流程:* <ol>* <li>获取字段映射关系</li>* <li>解析Excel文件</li>* <li>构建列索引到字段的映射</li>* <li>遍历数据行并转换为对象</li>* <li>类型转换和字段赋值</li>* <li>返回成功导入的对象列表</li>* </ol>*/public static <T> List<T> importExcel(MultipartFile file, Class<T> clazz) throws IOException {List<T> resultList = new ArrayList<>();// 获取字段映射(列名->字段对象)Map<String, Field> fieldMap = getFieldMap(clazz);// 使用try-with-resources确保流关闭try (InputStream inputStream = file.getInputStream();// 创建XSSFWorkbook解析.xlsx格式Workbook workbook = new XSSFWorkbook(inputStream)) {// 获取第一个工作表Sheet sheet = workbook.getSheetAt(0);// 获取表头行(第0行)Row headerRow = sheet.getRow(0);// 列索引到字段的映射Map<Integer, FieldMapping> columnMapping = new HashMap<>();// 1. 构建列映射关系 =======================================for (int col = 0; col < headerRow.getLastCellNum(); col++) {Cell cell = headerRow.getCell(col);if (cell != null) {// 获取列名并去除空格String columnName = cell.getStringCellValue().trim();// 根据列名查找对应字段Field field = findFieldByColumnName(fieldMap, columnName);if (field != null) {// 存储映射关系:列索引 -> (字段对象, 日期格式)columnMapping.put(col, new FieldMapping(field, getDateFormat(field)));}}}// 2. 处理数据行 ==========================================for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {Row row = sheet.getRow(rowNum);// 跳过空行if (row == null) continue;try {// 通过反射创建对象实例T obj = clazz.getDeclaredConstructor().newInstance();// 标记行是否有有效数据boolean hasData = false;// 遍历所有列for (int col = 0; col < headerRow.getLastCellNum(); col++) {Cell cell = row.getCell(col);// 跳过空单元格if (cell == null) continue;// 获取列映射信息FieldMapping mapping = columnMapping.get(col);if (mapping != null) {// 解析单元格值(根据目标类型和日期格式)Object value = parseCellValue(cell, mapping.field.getType(), mapping.dateFormat);if (value != null) {// 设置字段可访问(私有字段)mapping.field.setAccessible(true);// 给字段赋值mapping.field.set(obj, value);// 标记有有效数据hasData = true;}}}// 如果有有效数据则添加到结果列表if (hasData) {resultList.add(obj);}} catch (Exception e) {// 记录错误行信息(实际应用中应使用日志框架)System.err.printf("导入第 %d 行数据出错: %s%n", rowNum + 1, e.getMessage());}}} catch (Exception e) {throw new IOException("文件解析失败: " + e.getMessage(), e);}return resultList;}/*** 获取字段映射关系** @param clazz 目标类* @return 列名到字段对象的映射Map** <p>映射规则:* <ul>* <li>忽略带@ExcelIgnore注解的字段</li>* <li>优先使用@ExcelProperty的value作为键</li>* <li>无注解时使用字段名作为键</li>* </ul>*/private static Map<String, Field> getFieldMap(Class<?> clazz) {Map<String, Field> fieldMap = new HashMap<>();// 遍历所有声明字段for (Field field : clazz.getDeclaredFields()) {// 跳过忽略字段if (field.isAnnotationPresent(ExcelIgnore.class)) continue;// 获取ExcelProperty注解ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);// 确定映射键:注解值优先,否则使用字段名String key = (annotation != null && !annotation.value().isEmpty()) ?annotation.value() : field.getName();fieldMap.put(key, field);}return fieldMap;}/*** 根据列名查找字段** @param fieldMap 字段映射表* @param columnName Excel列名* @return 匹配的字段对象,未找到返回null** <p>匹配策略:* <ol>* <li>精确匹配(区分大小写)</li>* <li>忽略大小写匹配</li>* </ol>*/private static Field findFieldByColumnName(Map<String, Field> fieldMap, String columnName) {// 1. 精确匹配if (fieldMap.containsKey(columnName)) {return fieldMap.get(columnName);}// 2. 忽略大小写匹配for (String key : fieldMap.keySet()) {if (key.equalsIgnoreCase(columnName)) {return fieldMap.get(key);}}return null;}/*** 获取字段的日期格式** @param field 目标字段* @return 日期格式字符串(默认"yyyy-MM-dd HH:mm:ss")*/private static String getDateFormat(Field field) {ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);// 优先使用注解指定的格式,否则使用默认格式return (annotation != null) ? annotation.dateFormat() : "yyyy-MM-dd HH:mm:ss";}/*** 解析单元格值** @param cell 单元格对象* @param targetType 目标Java类型* @param dateFormat 日期格式(仅对日期类型有效)* @return 转换后的Java对象** <p>处理逻辑:* <ul>* <li>字符串类型 -> convertStringValue()</li>* <li>数值类型 -> 判断是否为日期</li>* <li>布尔类型 -> 直接返回</li>* <li>公式类型 -> 解析公式结果</li>* </ul>*/private static Object parseCellValue(Cell cell, Class<?> targetType, String dateFormat) {// 根据单元格类型分发处理switch (cell.getCellType()) {case STRING:// 字符串类型处理return convertStringValue(cell.getStringCellValue().trim(), targetType, dateFormat);case NUMERIC:// 数值类型:判断是否为日期格式if (DateUtil.isCellDateFormatted(cell)) {// 日期类型处理return convertDateValue(cell.getDateCellValue(), targetType);} else {// 普通数值处理return convertNumericValue(cell.getNumericCellValue(), targetType);}case BOOLEAN:// 布尔类型直接返回return cell.getBooleanCellValue();case FORMULA:// 公式单元格特殊处理return parseFormulaCell(cell, targetType, dateFormat);default:// 其他类型返回nullreturn null;}}/*** 解析公式单元格** @param cell 公式单元格* @param targetType 目标Java类型* @param dateFormat 日期格式* @return 公式计算结果的转换值*/private static Object parseFormulaCell(Cell cell, Class<?> targetType, String dateFormat) {try {// 根据公式结果类型处理switch (cell.getCachedFormulaResultType()) {case STRING:return convertStringValue(cell.getStringCellValue().trim(), targetType, dateFormat);case NUMERIC:if (DateUtil.isCellDateFormatted(cell)) {return convertDateValue(cell.getDateCellValue(), targetType);} else {return convertNumericValue(cell.getNumericCellValue(), targetType);}case BOOLEAN:return cell.getBooleanCellValue();default:return null;}} catch (Exception e) {// 公式解析异常返回nullreturn null;}}/*** 转换字符串值到目标类型** @param value 字符串值* @param targetType 目标Java类型* @param dateFormat 日期格式* @return 转换后的Java对象* @throws IllegalArgumentException 当转换失败时抛出** <p>支持转换类型:* <ul>* <li>String</li>* <li>Integer/int</li>* <li>Long/long</li>* <li>Double/double</li>* <li>Boolean/boolean(支持"是/否"、"YES/NO"、"TRUE/FALSE"、"1/0")</li>* <li>LocalDate(按指定格式)</li>* <li>LocalDateTime(按指定格式)</li>* </ul>*/private static Object convertStringValue(String value, Class<?> targetType, String dateFormat) {if (value.isEmpty()) return null;try {// 字符串类型直接返回if (targetType == String.class) return value;// 整型转换if (targetType == Integer.class || targetType == int.class) return Integer.parseInt(value);// 长整型转换if (targetType == Long.class || targetType == long.class) return Long.parseLong(value);// 双精度转换if (targetType == Double.class || targetType == double.class) return Double.parseDouble(value);// 布尔类型智能转换if (targetType == Boolean.class || targetType == boolean.class) {return "是".equals(value) || "YES".equalsIgnoreCase(value) ||"TRUE".equalsIgnoreCase(value) || "1".equals(value);}// 日期转换if (targetType == LocalDate.class) {return LocalDate.parse(value, DateTimeFormatter.ofPattern(dateFormat));}// 日期时间转换if (targetType == LocalDateTime.class) {return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(dateFormat));}} catch (Exception e) {throw new IllegalArgumentException("值转换失败: " + value + " -> " + targetType.getSimpleName());}// 未匹配类型返回原始字符串return value;}/*** 转换数值到目标类型** @param value 数值* @param targetType 目标Java类型* @return 转换后的Java对象*/private static Object convertNumericValue(double value, Class<?> targetType) {// 整型转换if (targetType == Integer.class || targetType == int.class) return (int) value;// 长整型转换if (targetType == Long.class || targetType == long.class) return (long) value;// 双精度转换if (targetType == Double.class || targetType == double.class) return value;// 单精度转换if (targetType == Float.class || targetType == float.class) return (float) value;// 布尔转换(>0为true)if (targetType == Boolean.class || targetType == boolean.class) return value > 0;// 默认返回原始值return value;}/*** 转换Date对象到目标类型** @param date Java Date对象* @param targetType 目标Java类型* @return 转换后的时间对象*/private static Object convertDateValue(Date date, Class<?> targetType) {if (date == null) return null;// Date类型直接返回if (targetType == Date.class) return date;// 转换为LocalDateTimeif (targetType == LocalDateTime.class) {return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();}// 转换为LocalDateif (targetType == LocalDate.class) {return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();}// 默认返回Datereturn date;}/*** 字段映射辅助类** <p>封装字段和对应的日期格式信息</p>*/private static class FieldMapping {// 目标字段final Field field;// 日期格式(来自@ExcelProperty注解)final String dateFormat;FieldMapping(Field field, String dateFormat) {this.field = field;this.dateFormat = dateFormat;}}
}
2. Excel 导出工具类 (ExcelExportUtil.java)
package com.xk.toolkit.util.file.excel;import com.mnsn.framework.law.util.property.ExcelIgnore;
import com.mnsn.framework.law.util.property.ExcelProperty;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.List;/*** Excel导出工具类** <p>提供通用的Excel导出功能,支持基于注解的字段映射和样式定制</p>** <p>功能特点:* <ul>* <li>基于反射机制实现通用导出</li>* <li>支持字段过滤(@ExcelIgnore)</li>* <li>支持自定义列名(@ExcelProperty)</li>* <li>自动设置表头和数据样式</li>* <li>自动调整列宽适应内容</li>* <li>支持常见数据类型(数值、字符串、布尔值)</li>* <li>响应流直接输出,避免内存溢出</li>* </ul>*/
public class ExcelExportUtil {/*** 通用Excel导出方法** @param <T> 数据类型泛型* @param response Http响应对象,用于输出Excel文件流* @param dataList 要导出的数据列表* @param fileName 导出文件名(不含扩展名)* @param sheetName 工作表名称* @param clazz 数据对象类型(用于反射获取字段信息)* @throws IOException 当文件导出失败时抛出** <p>执行流程:* <ol>* <li>创建工作簿和工作表</li>* <li>创建带样式的表头行</li>* <li>遍历数据列表填充数据行</li>* <li>自动调整列宽</li>* <li>设置响应头并输出Excel文件流</li>* </ol>*/public static <T> void exportExcel(HttpServletResponse response,List<T> dataList,String fileName,String sheetName,Class<T> clazz) throws IOException {// 使用XSSFWorkbook创建.xlsx格式的Excel工作簿(支持大文件)try (Workbook workbook = new XSSFWorkbook()) {// 创建工作表Sheet sheet = workbook.createSheet(sheetName);// 获取所有声明的字段(包括私有字段)Field[] fields = clazz.getDeclaredFields();// 创建表头行(第0行)Row headerRow = sheet.createRow(0);// 创建表头样式CellStyle headerStyle = createHeaderStyle(workbook);// 列索引计数器int colNum = 0;// 遍历所有字段for (Field field : fields) {// 设置可访问私有字段field.setAccessible(true);// 检查是否忽略该字段if (!field.isAnnotationPresent(ExcelIgnore.class)) {// 获取列名(优先使用注解值,无注解则使用字段名)String headerName = getHeaderName(field);// 创建表头单元格Cell cell = headerRow.createCell(colNum++);// 设置表头文本cell.setCellValue(headerName);// 应用表头样式cell.setCellStyle(headerStyle);}}// 创建数据单元格样式CellStyle dataStyle = createDataStyle(workbook);// 行索引计数器(从第1行开始)int rowNum = 1;// 遍历数据列表for (T data : dataList) {// 创建数据行Row row = sheet.createRow(rowNum++);// 重置列索引colNum = 0;// 遍历所有字段for (Field field : fields) {field.setAccessible(true);// 检查是否忽略该字段if (!field.isAnnotationPresent(ExcelIgnore.class)) {Cell cell;try {// 通过反射获取字段值Object value = field.get(data);// 创建数据单元格cell = row.createCell(colNum++);// 设置单元格值(根据类型处理)setCellValue(cell, value);// 应用数据样式cell.setCellStyle(dataStyle);} catch (IllegalAccessException e) {// 字段访问异常处理cell = row.createCell(colNum++);cell.setCellValue("[ERROR]");}}}}// 自动调整列宽(根据内容)for (int i = 0; i < colNum; i++) {sheet.autoSizeColumn(i);}// 设置响应内容类型为Excelresponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");// 设置下载文件名(UTF-8编码处理中文)response.setHeader("Content-Disposition", "attachment; filename=" +URLEncoder.encode(fileName + ".xlsx", "UTF-8"));// 将工作簿写入响应输出流workbook.write(response.getOutputStream());}}/*** 创建表头样式** @param workbook Excel工作簿对象* @return 配置好的表头单元格样式** <p>样式特点:* <ul>* <li>深蓝色背景</li>* <li>白色粗体文字</li>* <li>居中对齐</li>* </ul>*/private static CellStyle createHeaderStyle(Workbook workbook) {// 创建单元格样式CellStyle style = workbook.createCellStyle();// 创建字体Font font = workbook.createFont();// 设置粗体font.setBold(true);// 设置白色字体font.setColor(IndexedColors.WHITE.getIndex());// 应用字体style.setFont(font);// 设置深蓝色背景style.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex());// 设置实心填充style.setFillPattern(FillPatternType.SOLID_FOREGROUND);// 设置水平居中style.setAlignment(HorizontalAlignment.CENTER);return style;}/*** 创建数据行样式** @param workbook Excel工作簿对象* @return 配置好的数据单元格样式** <p>样式特点:* <ul>* <li>四周边框</li>* <li>无特殊背景色</li>* </ul>*/private static CellStyle createDataStyle(Workbook workbook) {CellStyle style = workbook.createCellStyle();// 设置上边框(细线)style.setBorderTop(BorderStyle.THIN);// 设置下边框(细线)style.setBorderBottom(BorderStyle.THIN);// 设置左边框(细线)style.setBorderLeft(BorderStyle.THIN);// 设置右边框(细线)style.setBorderRight(BorderStyle.THIN);return style;}/*** 获取字段的列名** @param field 实体类字段* @return 列名字符串(优先返回注解值)*/private static String getHeaderName(Field field) {// 检查是否有ExcelProperty注解if (field.isAnnotationPresent(ExcelProperty.class)) {// 返回注解的value值return field.getAnnotation(ExcelProperty.class).value();}// 默认返回字段名return field.getName();}/*** 设置单元格值(根据Java类型处理)** @param cell 目标单元格* @param value 要设置的值** <p>处理规则:* <ul>* <li>null值 -> 空字符串</li>* <li>数值类型 -> 转换为Double</li>* <li>布尔类型 -> 直接设置</li>* <li>其他类型 -> toString()结果</li>* </ul>*/private static void setCellValue(Cell cell, Object value) {if (value == null) {cell.setCellValue(""); // 空值处理} else if (value instanceof Number) {// 数值类型转换为Doublecell.setCellValue(((Number) value).doubleValue());} else if (value instanceof Boolean) {// 布尔类型直接设置cell.setCellValue((Boolean) value);} else {// 其他类型使用字符串表示cell.setCellValue(value.toString());}}
}
3. 注解系统
Excel 属性注解 (ExcelProperty.java)
package com.xk.toolkit.util.file.excel.property;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;// 列映射注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelProperty {// 列名String value() default "";// 日期格式(仅对时间类型有效)String dateFormat() default "yyyy-MM-dd HH:mm:ss";
}
Excel 忽略注解 (ExcelIgnore.java)
package com.xk.toolkit.util.file.excel.property;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;// 忽略字段注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelIgnore {
}
Excel 列宽注解 (ExcelColumnWidth.java)
package com.xk.toolkit.util.file.excel.property;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;// ExcelColumnWidth.java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelColumnWidth {int value() default 20; // 列宽(字符数)
}
使用示例
1. 定义实体类
public class Employee {@ExcelProperty("员工ID")private Long id;@ExcelProperty("员工姓名")private String name;@ExcelProperty(value = "入职日期", dateFormat = "yyyy年MM月dd日")private LocalDate hireDate;@ExcelProperty("薪资")private Double salary;@ExcelProperty("是否在职")private Boolean active;@ExcelIgnoreprivate String password;// 省略 getter/setter
}
2. 导入 Excel 数据
@PostMapping("/import")
public ResponseEntity<String> importEmployees(@RequestParam("file") MultipartFile file) {try {List<Employee> employees = ExcelImportUtil.importExcel(file, Employee.class);employeeService.saveAll(employees);return ResponseEntity.ok("成功导入 " + employees.size() + " 条数据");} catch (IOException e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("导入失败: " + e.getMessage());}
}
3. 导出 Excel 数据
@GetMapping("/export")
public void exportEmployees(HttpServletResponse response) throws IOException {List<Employee> employees = employeeService.getAllEmployees();ExcelExportUtil.exportExcel(response,employees,"员工数据报表","员工信息",Employee.class);
}
技术要点解析
-
智能类型转换:
- 字符串自动转换为目标类型(数值、日期、布尔值)
- 支持多种布尔值表示形式(是/否、YES/NO、TRUE/FALSE、1/0)
- 日期格式自定义(通过注解)
-
容错处理:
- 导入时自动跳过空行
- 错误行记录到控制台(生产环境应替换为日志框架)
- 导出时错误字段标记为"[ERROR]"
-
性能优化:
- 使用 try-with-resources 确保资源关闭
- 反射字段缓存提高性能
- 流式输出避免内存溢出
-
扩展性:
- 支持自定义样式(修改 createHeaderStyle/createDataStyle 方法)
- 支持自定义列宽(通过 ExcelColumnWidth 注解)
- 支持多种数据类型扩展
依赖配置
<!-- Spring Boot Web -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>3.2.0</version>
</dependency><!-- Apache POI -->
<dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>5.2.5</version>
</dependency>
<dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.5</version>
</dependency><!-- Jakarta EE (兼容 Spring Boot 3.x) -->
<dependency><groupId>jakarta.servlet</groupId><artifactId>jakarta.servlet-api</artifactId><version>6.0.0</version><scope>provided</scope>
</dependency>
总结
本文分享的 Excel 导入导出工具类具有以下优势:
- 开箱即用:简单注解配置即可实现复杂导入导出功能
- 高度可定制:支持自定义列名、日期格式、列宽等
- 智能类型转换:自动处理常见数据类型转换
- 企业级健壮性:完善的错误处理和容错机制
- 性能优化:适合处理中小规模数据(<10万行)
在实际项目中,您可以根据业务需求进一步扩展功能,例如:
- 添加数据校验逻辑
- 支持多Sheet页导出
- 添加导出进度提示
- 集成数据字典转换
希望这套工具类能帮助您提升开发效率,欢迎在评论区交流使用心得!