【Springboot7】ApachePOI文件导入导出

一、添加依赖pom.xml

XML 复制代码
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.0.1</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.0.1</version>
</dependency>

二、静态模板下载

直接去读取项目目录下的静态 Excel 模板文件,并以文件流的形式返回给前端。

java 复制代码
/**
     * 下载 Excel 导入模板
*/
@GetMapping("/downloadTemplate")
public void downloadTeamplate(HttpServletResponse response){
    // 直接指定相对路径
    String fileLocation = "template/基本仪器信息模板.xlsx";
    // 调用父类/底层封装的下载方法,将文件写入 response 输出流
    this.downloadTemplateFile(response,fileLocation); 
}

三、PxFileUtils 通用工具类

Excel 的表头通常是中文(如"仪器名称"),而 Java 实体的属性是英文。需要在这个方法里维护一个巨大的字典,根据传入的表名返回对应的映射关系。

java 复制代码
/**
     * 获取表头中文与实体类英文属性的映射关系
     */
    public static Map<String,String> getImportTitle(String tableType){
        Map<String,String> titleMap = new HashMap<>();
        
        if ("t_equipment".equals(tableType)){ // 仪器设备表映射
            titleMap.put("固定资产编号","eqCode");
            titleMap.put("仪器名称","eqName");
            titleMap.put("部门名称","deptName");
            titleMap.put("设备序列号","factoryNo");
            titleMap.put("生产日期","manufactureDate");
            // ... 添加更多字段
        } 
        // 扩展其他表的映射...
        else if ("t_user".equals(tableType)){
            titleMap.put("登录名","userName");
            titleMap.put("真实名称","realName");
            // ...
        }
        return titleMap;
    }

版本、解析表头、动态创建对象、反射赋值与类型强转

java 复制代码
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.*;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

public class PxFileUtils {

    /**
     * 核心:解析Excel并转换为对应的 Java Bean 列表
     * @param inputStream Excel文件流
     * @param fileName    文件名(用于判断后缀)
     * @param tableName   表名标志(用于获取映射字典)
     * @param clazz       目标对象的 Class
     * @param sheetNumer  要解析的 Sheet 页索引
     */
    public static <T> List<T> getExcelDatasByTitle(InputStream inputStream, String fileName, String tableName, Class<T> clazz, Integer sheetNumer){
        if (sheetNumer == null){
            sheetNumer = 0;
        }
        // 1. 获取表头映射关系
        Map<String,String> titleMap = getImportTitle(tableName);
        Workbook wb;
        InputStream is = null;
        try {
            is = new BufferedInputStream(inputStream);
            String subfix = fileName.substring(fileName.lastIndexOf("."));
            if (".xls".equalsIgnoreCase(subfix)) {
                wb = new HSSFWorkbook(is); // 2003版本
            } else if (".xlsx".equalsIgnoreCase(subfix)) {
                wb = new XSSFWorkbook(is); // 2007版本
            } else {
                return new ArrayList<>();
            }

            DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            DataFormatter formatter = new DataFormatter();
            
            Sheet sheet = wb.getSheetAt(sheetNumer);
            if (sheet == null) return null;

            List<T> allDatas = new ArrayList<>();
            Iterator<Row> rowIterator = sheet.iterator();
            Map<Integer,String> columnMap = new HashMap<>();
            int rowNum = 0;
            int allCellCount = 0;

            // 2. 逐行解析
            while (rowIterator.hasNext()) {
                Row row = rowIterator.next();
                // 2.1 处理第一行表头
                if(rowNum == 0){
                    if(row == null) throw new RuntimeException("表头不能为空!");
                    allCellCount = row.getLastCellNum();
                    // 将 Excel 列索引与 Java 属性名绑定
                    columnMap = getTitles(titleMap, row, dateFormat, formatter, allCellCount);
                    rowNum++;
                    continue;
                }

                // 2.2 处理数据行,通过反射实例化对象
                T obj = clazz.getDeclaredConstructor().newInstance();
                for (int colIndex = 0; colIndex < allCellCount; colIndex++) {
                    String header = columnMap.get(colIndex);
                    if (header == null || header.isEmpty()) continue;
                    
                    String cellValue = getCellValue(row.getCell(colIndex), dateFormat, formatter);
                    if (cellValue == null || cellValue.isEmpty()) continue;

                    // 2.3 通过反射根据属性名设置对象值
                    Field field = getField(clazz, header);
                    if (field != null) {
                        field.setAccessible(true);
                        setFieldValue(obj, field, cellValue, dateTimeFormatter);
                    }
                }
                allDatas.add(obj);
            }
            return allDatas;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("Excel解析失败!");
        } finally {
            if (is != null){
                try { is.close(); } catch (IOException e) {}
            }
        }
    }

    // 内部方法:获取表头对应的 Java 属性名
    private static Map<Integer,String> getTitles(Map<String, String> titleMap, Row row, DateFormat dateFormat, DataFormatter formatter, int allCellCount){
        Map<Integer,String> maps = new HashMap<>();
        for(int i = 0; i < allCellCount; i++){
            Cell cell = row.getCell(i);
            if(cell == null) continue;
            String cellValue = getCellValue(cell, dateFormat, formatter);
            // 处理类似 "规格型号(必填)" 带有括号的表头
            if (cellValue != null && cellValue.contains("(")){
                cellValue = cellValue.substring(0, cellValue.indexOf("("));
            }
            if(cellValue != null && !cellValue.isEmpty()){
                String key = titleMap.get(cellValue);
                if(key != null && !key.isEmpty()){
                    maps.put(i, key);
                }
            }
        }
        return maps;
    }

    // 内部方法:递归/下划线转驼峰获取 Field
    private static Field getField(Class<?> clazz, String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
            fieldName = convertToCamelCase(fieldName);
            try { field = clazz.getDeclaredField(fieldName); } catch (NoSuchFieldException ignored) {}
        }
        return field;
    }

    // 内部方法:下划线转驼峰
    private static String convertToCamelCase(String fieldName) {
        StringBuilder camelCaseString = new StringBuilder();
        boolean nextUpperCase = false;
        for (int i = 0; i < fieldName.length(); i++) {
            char c = fieldName.charAt(i);
            if (c == '_') {
                nextUpperCase = true;
            } else {
                camelCaseString.append(nextUpperCase ? Character.toUpperCase(c) : Character.toLowerCase(c));
                nextUpperCase = false;
            }
        }
        return camelCaseString.toString();
    }

    // 内部方法:根据字段类型强转并赋值
    private static void setFieldValue(Object obj, Field field, String value, DateTimeFormatter dateTimeFormatter) throws IllegalAccessException {
        Class<?> type = field.getType();
        if (type == String.class) {
            field.set(obj, value);
        } else if (type == Integer.class || type == int.class) {
            field.set(obj, Integer.parseInt(value));
        } else if (type == Long.class || type == long.class) {
            field.set(obj, Long.parseLong(value));
        } else if (type == Double.class || type == double.class) {
            field.set(obj, Double.parseDouble(value));
        } else if (type == LocalDateTime.class) {
            field.set(obj, LocalDateTime.parse(value, dateTimeFormatter));
        } else if (type == LocalDate.class ) {
            field.set(obj, LocalDate.parse(value));
        } else {
            field.set(obj, value);
        }
    }

    // 内部方法:智能读取单元格内容,处理数字后面的 ".0"
    private static String getCellValue(Cell cell, DateFormat dateFormat, DataFormatter formatter){
        if(cell == null) return "";
        String value;
        switch (cell.getCellType()) {
            case NUMERIC:
                if (DateUtil.isCellDateFormatted(cell)) {
                    value = dateFormat.format(cell.getDateCellValue());
                } else {
                    value = formatter.formatCellValue(cell);
                    if(value != null && value.endsWith(".0")){
                        value = value.replace(".0", "");
                    }
                }
                break;
            case BLANK:
                value = null;
                break;
            default:
                value = formatter.formatCellValue(cell);
        }
        return value == null ? null : value.replace("\n", "");
    }
}

Service 层实现

java 复制代码
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class EquipmentServiceImpl {

    public JsonResult importExcel(MultipartFile file) {
        try {
            // 1. 调用引擎,一键获取解析后的实体列表
            List<EquipmentBean> importList = PxFileUtils.getExcelDatasByTitle(
                    file.getInputStream(), 
                    file.getOriginalFilename(),
                    "t_equipment", // 对应映射字典的 Key
                    EquipmentBean.class, 
                    0
            );

            // 2. 基础数据校验
            for (EquipmentBean bean : importList){
                if (bean.getEqCode() == null || bean.getEqCode().isEmpty()){
                    throw new RuntimeException("【固定资产编号】不能为空!");
                }
            }

            // 3. 性能优化:批量查询关联表,防止循环查库
            // 提取所有涉及的部门名称去重
            List<String> deptNames = importList.stream()
                .map(EquipmentBean::getDeptName)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
                
            Map<String, DepartmentBean> deptMap = new HashMap<>();
            if(!deptNames.isEmpty()){
                List<DepartmentBean> deptList = departmentService.getMapper().selectBeanByNames(deptNames);
                deptList.forEach(t -> deptMap.put(t.getName(), t));
            }

            // 4. 数据补全与状态初始化
            LocalDateTime now = LocalDateTime.now();
            for (EquipmentBean bean : importList){
                bean.setId(UUID.randomUUID().toString().replace("-", "")); // 生成主键
                
                // 将中文部门名称翻译为系统的部门 ID
                if (bean.getDeptName() != null){
                    DepartmentBean dept = deptMap.get(bean.getDeptName());
                    if (dept != null){
                        bean.setDeptId(dept.getId());
                        bean.setOrgId(dept.getOrgId());
                    }
                }
                
                bean.setCreateTime(now);
                bean.setStatus("0"); // 默认状态
            }

            // 5. 批量插入数据库 (MyBatis-Plus 提供)
            if (!importList.isEmpty()) {
                getMapper().insertList(importList);
            }
            
            return new JsonResult(200, "成功导入" + importList.size() + "条数据!");
        } catch (Exception e) {
            e.printStackTrace();
            return new JsonResult(1001, "导入失败:" + e.getMessage());
        }
    }
}
相关推荐
i220818 Faiz Ul2 小时前
教育资源共享平台|基于springboot + vue教育资源共享平台系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·教育资源共享平台
编程大师哥2 小时前
VSCode中如何搭建JAVA+MAVEN
java·vscode·maven
不会写DN2 小时前
SQL 单表操作全解
java·服务器·开发语言·数据库·sql
Devin~Y2 小时前
大厂 Java 面试实战:从电商微服务到 AI 智能客服(含 Spring 全家桶、Redis、Kafka、RAG/Agent 解析)
java·spring boot·redis·elasticsearch·spring cloud·docker·kafka
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第十五期 - 策略模式】策略模式 —— 算法封装与动态替换实现、优缺点与适用场景
java·后端·设计模式·软件工程·策略模式
珍朱(珠)奶茶2 小时前
Spring Boot3整合FreeMark、itextpdf 5/7 实现pdf文件导出及注意问题
java·spring boot·后端·pdf·itextpdf
Leon-Ning Liu2 小时前
Oracle 26ai新特性:SQL Firewall(SQL 防火墙)的使用方法
数据库·sql·oracle
大数据新鸟2 小时前
微服务之Spring Cloud LoadBalancer
java·spring cloud·微服务
杜子不疼.2 小时前
AI Agent 智能体开发入门:AutoGen 多智能体协作实战教程
java·人工智能·spring