SpringBoot + Apache POI 实现数据导入导出
功能特点:
- 智能列匹配:
支持精确列名匹配
支持忽略大小写的列名匹配
自动匹配字段名(当未指定@ExcelProperty时)
强大的类型转换:
支持基本数据类型(Integer/Long/Double等)
支持日期类型(Date/LocalDate/LocalDateTime)
支持自定义日期格式
自动处理公式单元格
支持布尔值智能转换(“是/否”、“1/0”、“true/false”) - 容错处理:
跳过空行
记录错误行信息
单行错误不影响其他数据导入
支持严格/宽容两种模式 - 扩展性:
支持通用导入接口(通过类名指定目标类型)
返回详细导入结果(成功数据+错误信息)
可扩展支持CSV等格式
1.公共导入接口开发
使用示例:
- 准备Excel文件(首行为列名):
| 用户ID | 用户名 | 注册日期 | 最后登录时间 | 账户状态 |
|--------|--------|------------|-------------------|----------|
| 1 | 张三 | 2023-01-15 | 2023/06/30 09:30 | 是 |
| 2 | 李四 | 2023-02-20 | 2023/06/29 14:15 | 否 |
- 通过HTML页面上传文件
- 服务端返回导入结果:
{"totalCount": 2,"successCount": 2,"successData": [{"id": 1,"username": "张三","registerDate": "2023-01-15","lastLogin": "2023-06-30T09:30","active": true},{"id": 2,"username": "李四","registerDate": "2023-02-20","lastLogin": "2023-06-29T14:15","active": false}],"errorMessages": []
}
注意事项:
- 大文件处理建议:
// 使用SXSSFWorkbook处理大文件
Workbook workbook = new SXSSFWorkbook(new XSSFWorkbook(inputStream), 100);
1.增强自定义注解(添加日期格式支持)
import java.lang.annotation.*;// 列映射注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelProperty {// 列名String value() default "";// 日期格式(仅对时间类型有效)String dateFormat() default "yyyy-MM-dd HH:mm:ss";
}// 忽略字段注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelIgnore {}
2.创建通用导入工具类
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.format.DateTimeFormatter;
import java.util.*;public class ExcelImportUtil {/*** 通用Excel导入方法* @param file 上传的文件* @param clazz 目标对象类型* @return 导入结果对象列表*/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 (InputStream inputStream = file.getInputStream();Workbook workbook = new XSSFWorkbook(inputStream)) {Sheet sheet = workbook.getSheetAt(0);Row headerRow = sheet.getRow(0);// 1. 构建列名到字段的映射Map<Integer, FieldMapping> columnMapping = new HashMap<>();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;}// 获取字段映射(列名->字段)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 annotation = field.getAnnotation(ExcelProperty.class);String key = (annotation != null && !annotation.value().isEmpty()) ? annotation.value() : field.getName();fieldMap.put(key, field);}return fieldMap;}// 根据列名查找字段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;}// 获取日期格式(如果有注解指定)private static String getDateFormat(Field field) {ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);return (annotation != null) ? annotation.dateFormat() : "yyyy-MM-dd HH:mm:ss";}// 解析单元格值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:return null;}}// 处理公式单元格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) {return null;}}// 字符串类型转换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;}// 数值类型转换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;if (targetType == Boolean.class || targetType == boolean.class) return value > 0;return value;}// 日期类型转换private static Object convertDateValue(Date date, Class<?> targetType) {if (date == null) return null;if (targetType == Date.class) return date;if (targetType == LocalDateTime.class) {return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();}if (targetType == LocalDate.class) {return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();}return date;}// 字段映射辅助类private static class FieldMapping {final Field field;final String dateFormat;FieldMapping(Field field, String dateFormat) {this.field = field;this.dateFormat = dateFormat;}}
}
3.实体类示例(带日期格式)
public class User {@ExcelProperty("用户ID")private Long id;@ExcelProperty("用户名")private String username;@ExcelProperty(value = "注册日期", dateFormat = "yyyy-MM-dd")private LocalDate registerDate;@ExcelProperty(value = "最后登录时间", dateFormat = "yyyy/MM/dd HH:mm")private LocalDateTime lastLogin;@ExcelProperty("账户状态")private Boolean active;@ExcelIgnoreprivate String password;// 构造方法/getters/setters
}
4.创建导入控制器
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import java.util.List;@RestController
@RequestMapping("/import")
public class ImportController {@PostMapping("/users")public List<User> importUsers(@RequestParam("file") MultipartFile file) throws IOException {if (file.isEmpty()) {throw new IllegalArgumentException("请选择上传文件");}// 检查文件类型String fileName = file.getOriginalFilename();if (fileName == null || !fileName.toLowerCase().endsWith(".xlsx")) {throw new IllegalArgumentException("仅支持.xlsx格式文件");}return ExcelImportUtil.importExcel(file, User.class);}// 通用导入接口(适用于任何实体类)@PostMapping("/data")public <T> List<T> importData(@RequestParam("file") MultipartFile file,@RequestParam("className") String className) throws Exception {Class<T> clazz = (Class<T>) Class.forName(className);return ExcelImportUtil.importExcel(file, clazz);}
}