springboot+Apache POI 写共导入导出

发布于:2025-07-05 ⋅ 阅读:(17) ⋅ 点赞:(0)

SpringBoot + Apache POI 实现数据导入导出

功能特点:

  1. 智能列匹配:
    支持精确列名匹配
    支持忽略大小写的列名匹配
    自动匹配字段名(当未指定@ExcelProperty时)
    强大的类型转换:
    支持基本数据类型(Integer/Long/Double等)
    支持日期类型(Date/LocalDate/LocalDateTime)
    支持自定义日期格式
    自动处理公式单元格
    支持布尔值智能转换(“是/否”、“1/0”、“true/false”)
  2. 容错处理:
    跳过空行
    记录错误行信息
    单行错误不影响其他数据导入
    支持严格/宽容两种模式
  3. 扩展性:
    支持通用导入接口(通过类名指定目标类型)
    返回详细导入结果(成功数据+错误信息)
    可扩展支持CSV等格式

1.公共导入接口开发

使用示例:

  1. 准备Excel文件(首行为列名):
| 用户ID | 用户名 | 注册日期   | 最后登录时间       | 账户状态 |
|--------|--------|------------|-------------------|----------|
| 1      | 张三   | 2023-01-15 | 2023/06/30 09:30 ||
| 2      | 李四   | 2023-02-20 | 2023/06/29 14:15 ||
  1. 通过HTML页面上传文件
  2. 服务端返回导入结果:
{
  "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": []
}

注意事项:

  1. 大文件处理建议:
// 使用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;
    
    @ExcelIgnore
    private 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);
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到