FastExcel + Java:打造高效灵活的Excel数据导入导出解决方案

作者:后端小肥肠

🍇 我写过的文章中的相关代码放到了gitee,地址:xfc-fdw-cloud: 公共解决方案

🍊 有疑问可私信或评论区联系我。

🥑  创作不易未经允许严禁转载。

姊妹篇:

基于AOP的数据字典实现:实现前端下拉框的可配置更新_数据字典下拉框怎么写-CSDN博客

目录

1. 前言

2. Fastexcel介绍

3. 技术实现

3.1. 表结构及实体类说明 

3.2. 适配PostgresSQL中text[]类型handler编写

3.3. 注解编写

3.4. 动态设置下拉框核心工具类编写

 3.5. 制作多选下拉框exel模板

3.6. 导出模板方法 

3.7 导入数据方法

4. 源码地址

5. 结语


1. 前言

在当今的软件开发中,数据的导入与导出是常见的需求,尤其是在企业级应用中,Excel文件作为数据交互的一种重要形式被广泛使用。传统的Excel导入导出功能虽然基本满足需求,但在处理大数据量或需要动态配置时,往往显得效率低下且灵活性不足。

本文将围绕在Java中基于实体类的高效Excel数据导入导出展开,介绍如何利用FastExcel这一库实现高性能和灵活的Excel处理。同时,结合动态下拉框多选下拉框的设置,使得数据的导入导出不仅高效,还能具备较强的可定制性和交互性。通过本篇文章,你将能够掌握在Java中使用实体类驱动Excel导入导出的技术,并学会如何在系统中动态生成下拉框和多选框的配置

2. Fastexcel介绍

FastExcel 是一个高性能的 Java 库,旨在提供高效的 Excel 文件操作,尤其是在处理大数据量时,其性能远超常见的POI库。相比其他 Excel 处理库,FastExcel 采用了更加优化的内存管理和流式处理方式,使得它在内存占用和速度上具有显著优势。尤其在导入导出大量数据时,FastExcel 可以有效避免内存溢出和性能瓶颈,保证程序的稳定运行。

FastExcel 的特点:

  • 高性能: 快速的读取和写入速度,特别适用于大数据量的 Excel 文件。
  • 低内存占用: 采用流式读取和写入的方式,极大地减少了内存的使用。
  • 简洁易用: 相较于其他复杂的Excel操作库,FastExcel提供了简洁的API接口,易于上手。
  • Excel文件格式支持: 支持 .xlsx 格式的文件,且兼容大部分常见的 Excel 文件操作需求。
  • 动态功能扩展: 可以灵活地与 Java 实体类进行绑定,支持动态生成表头、表格内容和格式设置。

为什么选择FastExcel?

在实际开发中,Excel文件的导入导出经常用于大规模的数据交换,尤其是在财务、报表等领域。对于这些场景,传统的 Excel 处理库(如 Apache POI)可能在面对大数据量时会出现性能瓶颈,尤其是在需要频繁进行读写操作的情况下。而FastExcel通过采用流式读取和写入的方式,有效解决了这一问题,并且通过内存管理优化,使得应用能够在处理大量数据时仍保持高效运行。

由于FastExcel的这些优势,它成为了许多Java开发者在实现Excel数据导入导出时的首选工具,尤其是对于需要处理海量数据或需要提高导入导出性能的应用场景。

3. 技术实现

模拟需求:本案例模拟导出学生的相关信息,包括学号、姓名、性别、父母职业类型和家庭住址所属区域等内容。具体来说,学生信息包含以下字段:

  • 学号唯一标识学生的编号,作为数据的主键。
  • 姓名学生的姓名,文本类型字段。
  • 性别此字段通过下拉框进行选择,支持男、女等选项,方便用户快速选择。
  • 父母职业类型此字段也是一个下拉框,列出了多种常见的职业类型,便于系统自动识别父母的职业分类。
  • 家庭住址所属区域该字段设置为多选下拉框,支持学生家庭地址涉及多个区域的情况。例如,假设某些富裕学生的家庭可能在不同城市或区域拥有多个房产,因此可以选择多个区域。此设置充分考虑了复杂的地址情况,提高了数据录入的灵活性。

本文将实现excel文件下拉框基于java程序动态设置,并支持多选下拉框。

动态下拉框的实现步骤如下:

1. 创建支持宏的xlsm模板文件

2. 编写vba代码

3. 基于java代码动态写入下拉框数据

4. 导出动态设置下拉框的excel模板

3.1. 表结构及实体类说明 

1.  PostgreSQL数据库表结构(SQL)

我这里创建了一个比较简单的数据字典表存储下拉框数据,完整版数据字典请移步:基于AOP的数据字典实现:实现前端下拉框的可配置更新_数据字典下拉框怎么写-CSDN博客

CREATE TABLE students (
    student_id VARCHAR(20) PRIMARY KEY,  -- 学号
    name VARCHAR(100) NOT NULL,          -- 姓名
    gender VARCHAR(10),                  -- 性别
    parent_occupation VARCHAR(100),      -- 父母职业类型
    home_area TEXT[]                     -- 家庭住址所属区域 (使用数组类型来存储多个区域)
);

-- 创建一个独立的字典表,用于存储性别、父母职业类型等下拉框选项
CREATE TABLE gender_options (
    id SERIAL PRIMARY KEY,
    gender VARCHAR(10) NOT NULL
);

CREATE TABLE parent_occupation_options (
    id SERIAL PRIMARY KEY,
    occupation VARCHAR(100) NOT NULL
);

-- 插入默认的字典数据
INSERT INTO gender_options (gender) VALUES
    ('男'),
    ('女');

INSERT INTO parent_occupation_options (occupation) VALUES
    ('教师'),
    ('医生'),
    ('工程师'),
    ('律师'),
    ('其他');

在student表中插入10条数据:

INSERT INTO students (student_id, name, gender, parent_occupation, home_area)
VALUES
('S10001', '张三', '男', '教师', '{"北京", "上海"}'),
('S10002', '李四', '女', '医生', '{"广州", "深圳"}'),
('S10003', '王五', '男', '工程师', '{"北京"}'),
('S10004', '赵六', '女', '律师', '{"杭州", "南京"}'),
('S10005', '孙七', '男', '商人', '{"上海", "广州"}'),
('S10006', '周八', '女', '公务员', '{"北京", "武汉"}'),
('S10007', '吴九', '男', '教师', '{"成都"}'),
('S10008', '郑十', '女', '护士', '{"重庆", "成都"}'),
('S10009', '冯十一', '男', '程序员', '{"深圳", "上海"}'),
('S10010', '陈十二', '女', '医生', '{"北京", "上海", "广州"}');

2. 实体类

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="Students对象", description="")
@TableName(value = "students",autoResultMap = true)
public class Students implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "student_id", type = IdType.ASSIGN_ID)
    @ExcelProperty(value = "学号",index = 0)
    private String studentId;

    @ExcelProperty(value = "姓名",index = 1)
    private String name;

    @ExcelProperty(value = "性别",index = 2)
    @DropDownSetField(source = {"男", "女"})
    private String gender;

    @ExcelProperty(value = "父母职业", index = 3)
    @DropDownSetField(dynamicSource = ParentOccupationOptions.class)
    private String parentOccupation;

    @ExcelProperty(value = "所属区域",index = 4,converter = SimpleStringToListConverter.class)
    @DropDownSetField(source = {"东城区", "西城区", "海淀区", "朝阳区", "丰台区", "石景山区", "门头沟区", "房山区", "通州区", "顺义区", "昌平区", "大兴区", "怀柔区", "平谷区", "密云区", "延庆区"})
    @TableField(typeHandler = StringArrayTypeHandler.class)
    private List<String> homeArea;

}

3.2. 适配PostgresSQL中text[]类型handler编写

@ConditionalOnClass({BaseTypeHandler.class})
@MappedTypes({List.class})
public class StringArrayTypeHandler extends BaseTypeHandler<List<String>> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType)
            throws SQLException {
        Connection conn = ps.getConnection();
        Array array = conn.createArrayOf("text", parameter.toArray(new String[0]));
        ps.setArray(i, array);
    }

    @Override
    public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        Array array = rs.getArray(columnName);
        return array == null ? null : Arrays.asList((String[]) array.getArray());
    }

    @Override
    public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        Array array = rs.getArray(columnIndex);
        return array == null ? null : Arrays.asList((String[]) array.getArray());
    }

    @Override
    public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        Array array = cs.getArray(columnIndex);
        return array == null ? null : Arrays.asList((String[]) array.getArray());
    }
}

这个类是一个 MyBatis 的类型处理器(TypeHandler),主要功能是:

  • 数据转换:实现 PostgreSQL 数据库中的数组类型与 Java 中的 List<String> 类型之间的双向转换
  • Java -> DB: List<String> 转换为 PostgreSQL 的 text[] 数组类型
  • DB -> Java:PostgreSQL  text[] 数组类型转换为 List<String>
  • 应用场景:适用于需要在单个字段中存储多个值的情况,如学生所属区域(可以属于多个区域)的存储和读取
  • 技术特点:
  • 继承自 BaseTypeHandler<List<String>>
  • 使用 @MappedTypes 注解指定处理 List 类型
  • 使用 @ConditionalOnClass 实现条件化配置

3.3. 注解编写

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DropDownSetField {
    String[] source() default {};
    String value() default "";
    Class<?>[] dynamicSource() default {};
}

3.4. 动态设置下拉框核心工具类编写

@Component
@Slf4j
public class ExchangeSheetUtils {
    @Autowired
    private IDropDownDataService dropDownDataService;

    private static final int MAX_EXCEL_ROWS = 65536;
    private static final String HIDDEN_SHEET_NAME = "字典sheet";
    private final char[] alphabet = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
            'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};

    // 使用ThreadLocal来确保线程安全
    private final ThreadLocal<List<String>> dropDownArrays = ThreadLocal.withInitial(ArrayList::new);
    private final ThreadLocal<Map<Integer, List<String>>> dropDownMap = ThreadLocal.withInitial(HashMap::new);

    // 在方法结束时清理ThreadLocal
    public void clearThreadLocals() {
        dropDownArrays.get().clear();
        dropDownMap.get().clear();
    }

    /**
     * 根据实体类解析字段,并获取动态或固定的下拉数据。
     */
    public void getEntityField(Class<?> clazz) {
        try {
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                    processDropDownField(field);
            }
        } catch (Exception e) {
            log.error("处理实体类字段失败", e);
            clearThreadLocals();
            throw new RuntimeException("处理实体类字段失败", e);
        }
    }

    /**
     * 处理下拉框字段
     */
    private void processDropDownField(Field field) {
        ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
        if (excelProperty == null || excelProperty.value().length == 0) {
            log.warn("字段 {} 缺少 ExcelProperty 注解或 value 为空", field.getName());
            return;
        }
        
        String columnName = excelProperty.value()[0];
        dropDownArrays.get().add(columnName);

        DropDownSetField dropDownSetField = field.getAnnotation(DropDownSetField.class);
        if (dropDownSetField != null) {
            List<String> dropDownOptions = new ArrayList<>();

            if (dropDownSetField.dynamicSource().length > 0) {
                dropDownOptions = getDropDownDataFromDynamicSource(dropDownSetField.dynamicSource());
            } else if (dropDownSetField.source().length > 0) {
                dropDownOptions = Arrays.asList(dropDownSetField.source());
            }

            if (!dropDownOptions.isEmpty()) {
                int columnIndex = dropDownArrays.get().size() - 1;
                dropDownMap.get().put(columnIndex, dropDownOptions);
            }
        }

    }

    /**
     * 从动态数据源获取下拉数据
     */
    private List<String> getDropDownDataFromDynamicSource(Class<?>[] dynamicSourceClasses) {
        List<String> dropDownOptions = new ArrayList<>();
        for (Class<?> dynamicSourceClass : dynamicSourceClasses) {
            try {
                // 调用动态数据源的接口获取下拉数据(例如通过远程接口)
                List<String> data = dropDownDataService.fetchDynamicDropDownData(dynamicSourceClass);
                dropDownOptions.addAll(data);
            } catch (Exception e) {
                log.error("获取动态下拉框数据失败,错误信息: {}", e.getMessage());
            }
        }
        return dropDownOptions;
    }

    

    /**
     * 创建并更新隐藏Sheet页,添加下拉框
     */
    public void updateHiddenSheet(Sheet curSheet, Workbook templateWorkbook) {
        if (dropDownMap.get().isEmpty()) {
            return; // 如果没有下拉框数据则不进行处理
        }
        DataValidationHelper helper = curSheet.getDataValidationHelper();
        String hiddenSheetName = HIDDEN_SHEET_NAME;
        Sheet hiddenSheet = templateWorkbook.createSheet(hiddenSheetName);

        hideOtherSheets(templateWorkbook);

        clearOldNamedRanges(templateWorkbook);

        // 填充隐藏Sheet的数据
        Set<Map.Entry<Integer, List<String>>> entrySet = dropDownMap.get().entrySet();
        for (Map.Entry<Integer, List<String>> entry : entrySet) {
            createDropDownList(helper, hiddenSheet, entry);
        }
    }

    /**
     * 隐藏所有除第一个外的Sheet
     */
    private void hideOtherSheets(Workbook templateWorkbook) {
        int totalSheets = templateWorkbook.getNumberOfSheets();
        for (int i = 1; i < totalSheets; i++) {
            templateWorkbook.setSheetHidden(i, true);
        }
    }

    /**
     * 清除之前的命名范围
     */
    private void clearOldNamedRanges(Workbook templateWorkbook) {
        for (int i = 0; i < 26; i++) {
            Name workbookName = templateWorkbook.getName("dict" + i);
            if (workbookName != null) {
                templateWorkbook.removeName(workbookName); // 使用 Name 对象删除
            }
        }
    }

    /**
     * 创建并配置下拉框
     */
    private void createDropDownList(DataValidationHelper helper, Sheet hiddenSheet, Map.Entry<Integer, List<String>> entry) {
        Integer column = entry.getKey();
        List<String> values = entry.getValue();

        // 填充数据到隐藏sheet
        int rowLen = values.size();
        for (int i = 0; i < rowLen; i++) {
            Row row = hiddenSheet.getRow(i);
            if (row == null) {
                row = hiddenSheet.createRow(i);
            }
            Cell cell = row.createCell(column);
            cell.setCellValue(values.get(i));
        }

        String excelColumn = getExcelColumn(column);
        String refersTo = HIDDEN_SHEET_NAME + "!$" + excelColumn + "$1:$" + excelColumn + "$" + rowLen;

        // 创建命名范围
        Name name = hiddenSheet.getWorkbook().createName();
        name.setNameName("dict" + column);
        name.setRefersToFormula(refersTo);

        // 获取第一个sheet(主sheet)
        Sheet mainSheet = hiddenSheet.getWorkbook().getSheetAt(0);

        // 创建数据验证
        DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + column);
        CellRangeAddressList addressList = new CellRangeAddressList(1, MAX_EXCEL_ROWS, column, column);
        DataValidation validation = helper.createValidation(constraint, addressList);

        // 设置验证属性
        validation.setSuppressDropDownArrow(true);
        validation.setShowErrorBox(true);
        validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
        validation.createErrorBox("提示", "此值与单元格定义格式不一致!");

        // 将验证添加到主sheet
        mainSheet.addValidationData(validation);
    }
    /**
     * 将数字列转化为字母列
     */
    private String getExcelColumn(int num) {
        int len = alphabet.length;
        int first = num / len;
        int second = num % len;

        if (num < len) {
            return String.valueOf(alphabet[num]);
        } else {
            return String.valueOf(alphabet[first - 1]) + alphabet[second - 1];
        }
    }

    /**
     * 设置数据Sheet页的初始化
     */
    public void setDataSheet(Sheet sheet, Workbook templateWorkbook) {
        Row row = sheet.createRow(0);
        List<String> arrays = dropDownArrays.get();
        for (int i = 0; i < arrays.size(); i++) {
            row.createCell(i).setCellValue(arrays.get(i));
        }
    }

}

ExchangeSheetUtils 核心方法说明

1. getEntityField

public void getEntityField(Class<?> clazz)
  • 功能:解析实体类的字段注解,收集下拉框配置信息
  • 处理流程
    • 获取类的所有字段
    • 通过 processDropDownField 处理每个字段的注解
    • 将下拉框数据存入 ThreadLocal

2. processDropDownField

private void processDropDownField(Field field)
  • 功能:处理单个字段的下拉框配置
  • 处理流程
    • 读取 @ExcelProperty 注解获取列名
    • 读取 @DropDownSetField 注解获取下拉选项
    • 将数据保存到 dropDownArrays 和 dropDownMap

3. updateHiddenSheet

public void updateHiddenSheet(Sheet curSheet, Workbook templateWorkbook)
  • 功能:创建和配置隐藏的数据字典sheet
  • 处理流程
    • 创建隐藏sheet
    • 清理旧的命名范围
    • 通过 createDropDownList 设置下拉框

4. createDropDownList

private void createDropDownList(DataValidationHelper helper, Sheet hiddenSheet, Map.Entry<Integer, List<String>> entry)
  • 功能:创建Excel下拉框
  • 处理流程
    • 在隐藏sheet中填充下拉选项
    • 创建命名范围(Named Range)
    • 设置数据验证规则
    • 配置下拉框和错误提示

这些方法通过 ThreadLocal 实现线程安全,通过 POI 提供的 API 实现 Excel 的各种操作,最终生成一个带有下拉框的 Excel 模板文件。

 3.5. 制作多选下拉框exel模板

1. 新建.xlsx文件

2. 点击顶部【文件】后点击【选项】

3. 在弹出的弹窗中,点击【信任中心】选项页中的【信任中心设置】按钮

4.  开启宏

5. 另存为.xlsm文件 

6. 编写VBA代码

选中Sheet,右键弹出菜单,选择【查看代码】,将下面代码粘进去

Sub Worksheet_Change(ByVal Target As Range)
    ' 让数据有效性选择可以多选,且不可重复
    Dim rngDV As Range
    Dim oldVal As String
    Dim newVal As String

    ' 如果修改的范围超过1个单元格,则退出
    If Target.Count > 1 Then GoTo exitHandler

    On Error Resume Next
    Set rngDV = Cells.SpecialCells(xlCellTypeAllValidation)
    On Error GoTo exitHandler

    If rngDV Is Nothing Then GoTo exitHandler

    If Intersect(Target, rngDV) Is Nothing Then
        ' 如果目标单元格不在数据验证区域,什么都不做
    Else
        Application.EnableEvents = False
        newVal = Target.Value

        ' 假设字段映射如下:
        ' 第3列是 "gender" (性别)
        ' 第4列是 "parentOccupation" (父母职业类型)
        ' 第5列是 "homeArea" (家庭住址所属区域)
        ' 如果修改的是 "gender" 或 "parentOccupation",则是单选,直接替换
        ' 如果修改的是 "homeArea",则是多选,去重并追加

        If Target.Column = 3 Or Target.Column = 4 Then
            ' 对性别(gender)和父母职业类型(parentOccupation)做单选处理
            Application.Undo
            oldVal = Target.Value
            Target.Value = newVal

            ' 如果原值与新值不同,直接替换
            If oldVal <> newVal Then
                Target.Value = newVal
            End If
        ElseIf Target.Column = 5 Then
            ' 对家庭住址所属区域(homeArea)做多选处理
            Application.Undo
            oldVal = Target.Value
            Target.Value = newVal

            If oldVal = "" Then
                ' 如果原值为空,直接返回
            Else
                If newVal = "" Then
                    ' 如果新值为空,什么都不做
                Else
                    ' 去除重复项
                    If InStr(1, oldVal, newVal) <> 0 Then
                        ' 如果新值在旧值中已存在
                        If InStr(1, oldVal, newVal) + Len(newVal) - 1 = Len(oldVal) Then
                            ' 如果是最后一个选项重复,则删除
                            Target.Value = Left(oldVal, Len(oldVal) - Len(newVal) - 1)
                        Else
                            ' 否则删除逗号后面的重复值
                            Target.Value = Replace(oldVal, newVal & ",", "")
                        End If
                    Else
                        ' 如果是新选项,则追加
                        Target.Value = oldVal & "," & newVal
                    End If
                End If
            End If
        End If
    End If

exitHandler:
    Application.EnableEvents = True
End Sub


这是一个 Excel 工作表的 Worksheet_Change 事件处理程序,主要实现了单元格数据验证的自定义处理逻辑:

  • 功能目标:实现单选和多选下拉框的不同处理逻辑
  • 具体实现:
  • 第3列(性别)和第4列(父母职业)实现单选功能,新值直接替换旧值
  • 第5列(所属区域)实现多选功能:
  • 允许多个选项,用逗号分隔
  • 自动去重(避免重复选择)
  • 支持取消选择(点击已选项可移除)
  • 使用 Application.Undo 和 Application.EnableEvents 确保操作的原子性和避免事件循环

该代码通过 VBA 扩展了 Excel 默认的下拉框功能,使其支持更复杂的业务需求,特别是实现了多选下拉框的去重和动态更新功能。

将上述步骤保存,支持动态下拉框的模板文件(.xlsm)就制作完成了。

3.6. 导出模板方法 

    public void exportTemplate(HttpServletResponse response) {
        Workbook templateWorkbook = null;
        FileInputStream fileInputStream = null;
        try {
            // 设置响应头
            response.setContentType("application/vnd.ms-excel.sheet.macroEnabled.12");
            response.setCharacterEncoding("utf-8");
            String name = "学生数据模板";
            response.setHeader("Content-Disposition", "attachment; filename=" +
                    java.net.URLEncoder.encode(name, "UTF-8") + ".xlsm");

            // 读取模板文件
            File file = new File("D:/学生数据模板.xlsm");
            fileInputStream = new FileInputStream(file);
            templateWorkbook = WorkbookFactory.create(fileInputStream);

            // 获取数据 - 这三个方法的调用顺序不能变
            exchangeSheetUtils.getEntityField(Students.class);

            Sheet outputSheet = templateWorkbook.getSheetAt(0);
            templateWorkbook.setSheetName(0, name);

            exchangeSheetUtils.updateHiddenSheet(outputSheet, templateWorkbook);
            exchangeSheetUtils.setDataSheet(outputSheet, templateWorkbook);

            // 输出文件
            templateWorkbook.write(response.getOutputStream());
        } catch (Exception e) {
            log.error("导出学生模板失败:"+e.getMessage(), e);
            throw new RuntimeException("导出模板失败", e);
        } finally {
            // 清理 ThreadLocal 资源
            exchangeSheetUtils.clearThreadLocals();

            // 关闭其他资源
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    log.error("关闭文件流失败", e);
                }
            }
            if (templateWorkbook != null) {
                try {
                    templateWorkbook.close();
                } catch (IOException e) {
                    log.error("关闭工作簿失败", e);
                }
            }
        }
    }

这是一个用于导出 Excel 模板文件的方法,其核心功能是:

读取预设的 Excel 模板文件(D:/student.xlsm),通过 ExchangeSheetUtils 工具类解析 Students 实体类的注解信息(@ExcelProperty 和 @DropDownSetField),设置下拉框和数据验证,最后将处理好的模板文件(包含表头、下拉框配置和 VBA 代码)以 .xlsm 格式输出到 HTTP 响应流中。整个过程包含了完整的资源管理(使用 try-finally 确保资源正确关闭)和线程安全处理(通过 clearThreadLocals 清理 ThreadLocal 资源)。

关键步骤:

  1. 设置响应头(.xlsm 格式)
  2. 读取模板文件
  3. 处理下拉框配置
  4. 输出文件
  5. 清理资源

3.7 导入数据方法

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String importData(MultipartFile file) throws IOException {
        try {
            final List<Students> studentsList = new ArrayList<>();

            // 使用Map方式读取数据
            EasyExcel.read(file.getInputStream())
                    .sheet(0)
                    .headRowNumber(1)  // 将表头行设置为1,因为第0行是表头
                    .registerReadListener(new AnalysisEventListener<Map<Integer, String>>() {
                        @Override
                        public void invoke(Map<Integer, String> data, AnalysisContext context) {
                            log.info("读取到一行数据: {}", JSON.toJSONString(data));
                            // 手动转换为Students对象,使用正确的key
                            Students student = new Students();
                            student.setStudentId(data.get(0));      // 学号
                            student.setName(data.get(1));           // 姓名
                            student.setGender(data.get(2));         // 性别
                            student.setParentOccupation(data.get(3));  // 家长职业
                            
                            // 使用与SimpleStringToListConverter相同的逻辑处理homeArea
                            String areaStr = data.get(4);
                            if (areaStr != null && !areaStr.trim().isEmpty()) {
                                student.setHomeArea(Arrays.asList(areaStr.split(",")));
                            }
                            
                            studentsList.add(student);
                        }

                        @Override
                        public void doAfterAllAnalysed(AnalysisContext context) {
                            log.info("所有数据解析完成!共读取到 {} 条数据", studentsList.size());
                        }
                    })
                    .doRead();

            if (CollectionUtils.isEmpty(studentsList)) {
                return "Excel中没有数据";
            }

            // 保存数据
            this.saveBatch(studentsList);
            return "导入成功,共导入 " + studentsList.size() + " 条数据";
        } catch (Exception e) {
            log.error("导入失败:", e);
            throw e;
        }
    }

这个 importData 函数的主要功能是导入Excel文件中的学生数据。具体流程如下:

  • 使用 @Transactional 注解确保数据导入的事务性,如果出现异常会自动回滚
  • 创建一个 studentsList 列表用于存储解析后的数据
  • 使用 EasyExcel 读取上传的 Excel 文件:
  • 读取第一个 sheet(sheet(0))
  • 设置表头行号为1(headRowNumber(1))
  • 使用 Map 方式读取数据,其中 key 是列索引(0-4),value 是单元格内容
  • 在 invoke 方法中处理每一行数据:
  • Map 数据手动转换为 Students 对象
  • 特别处理 homeArea 字段,将字符串用逗号分割转换为 List
  • 最后批量保存数据到数据库(saveBatch
  • 如果过程中出现异常,会记录错误日志并抛出异常触发事务回滚

4. 源码地址

源码里面有导出模板,导入数据和导出数据三个接口,实现了功能闭环,完整代码:

xfc-fdw-cloud: 公共解决方案

5. 结语

本文介绍了如何通过 FastExcel 实现高效的 Excel 数据导入导出,基于实体类动态设置excel下拉框(支持多选),解决了实际开发中的常见需求。如有疑问,欢迎在评论区留言,我看到都会回复。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/968689.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

解决IDEA中gitlab登录只有token选项,没有账号密码选项

如图&#xff0c;当点击gitlab账户登录的时候&#xff0c;只显示server和token&#xff0c;而没有账号选项。期望通过账号密码登录。 解决方式&#xff1a; 插件 - GitLab - 禁用即可。

AI语言模型的技术之争:DeepSeek与ChatGPT的架构与训练揭秘

云边有个稻草人-CSDN博客 目录 第一章&#xff1a;DeepSeek与ChatGPT的基础概述 1.1 DeepSeek简介 1.2 ChatGPT简介 第二章&#xff1a;模型架构对比 2.1 Transformer架构&#xff1a;核心相似性 2.2 模型规模与参数 第三章&#xff1a;训练方法与技术 3.1 预训练与微调…

PHP 中的除以零错误

除以零错误&#xff08;Division by zero&#xff09;是指数字除以零的情况&#xff0c; 这在数学上是未定义的。在 PHP 中&#xff0c;处理这种错误的方式取决于 PHP 版本&#xff1a; PHP 7&#xff1a; 使用 / 运算符会产生一个警告 (E_WARNING) 并返回 false。 使用 intd…

【设计模式】01- 一文理解常用设计模式-“创建型模式”篇

一、前言 最近在复习设计模式&#xff0c;撰写、整理了内容和代码片段&#xff0c;和大家一起交流学习。 设计模式是软件设计中常见问题的典型解决方案。 二、模式分类 模式可以根据其意图或目的来分类。常见的设计模式包括&#xff1a; 创建型模式提供创建对象的机制&#x…

数据结构-链式二叉树

文章目录 一、链式二叉树1.1 链式二叉树的创建1.2 根、左子树、右子树1.3 二叉树的前中后序遍历1.3.1前(先)序遍历1.3.2中序遍历1.3.3后序遍历 1.4 二叉树的节点个数1.5 二叉树的叶子结点个数1.6 第K层节点个数1.7 二叉树的高度1.8 查找指定的值(val)1.9 二叉树的销毁 二、层序…

游戏引擎学习第99天

仓库:https://gitee.com/mrxiao_com/2d_game_2 黑板&#xff1a;制作一些光场(Light Field) 当前的目标是为游戏添加光照系统&#xff0c;并已完成了法线映射&#xff08;normal maps&#xff09;的管道&#xff0c;但还没有创建可以供这些正常映射采样的光场。为了继续推进&…

LSTM变种模型

GRU GRU简介 门控循环神经网络 (Gated Recurrent Neural Network&#xff0c;GRNN) 的提出&#xff0c;旨在更好地捕捉时间序列中时间步距离较大的依赖关系。它通过可学习的门来控制信息的流动。其中&#xff0c;门控循环单元 (Gated Recurrent Unit &#xff0c; GRU) 是…

业务开发 | 基础知识 | Maven 快速入门

Maven 快速入门 1.Maven 全面概述 Apache Maven 是一种软件项目管理和理解工具。基于项目对象模型的概念&#xff08;POM&#xff09;&#xff0c;Maven 可以从中央信息中管理项目的构建&#xff0c;报告和文档。 2.Maven 基本功能 因此实际上 Maven 的基本功能就是作为 Ja…

新一代SCADA: 宏集Panorama Suite 2025 正式发布,提供更灵活、符合人体工学且安全的应用体验

宏集科技宣布正式推出全新Panorama Suite 2025 SCADA软件&#xff01;全新版本标志着 Panorama Suite的一个重要里程碑&#xff0c;代表了从 Panorama Suite 2022 开始并跨越三个版本&#xff08;2022、2023、2025&#xff09;的开发过程的顶峰。 此次重大发布集中在六个核心主…

PAT乙级真题 — 1080 MOOC期终成绩(java)【测试点3超时】

对于在中国大学MOOC&#xff08;http://www.icourse163.org/ &#xff09;学习“数据结构”课程的学生&#xff0c;想要获得一张合格证书&#xff0c;必须首先获得不少于200分的在线编程作业分&#xff0c;然后总评获得不少于60分&#xff08;满分100&#xff09;。总评成绩的计…

【Oracle篇】浅谈执行计划中的多表连接(含内连接、外连接、半连接、反连接、笛卡尔连接五种连接方式和嵌套、哈希、排序合并三种连接算法)

&#x1f4ab;《博主介绍》&#xff1a;✨又是一天没白过&#xff0c;我是奈斯&#xff0c;从事IT领域✨ &#x1f4ab;《擅长领域》&#xff1a;✌️擅长阿里云AnalyticDB for MySQL(分布式数据仓库)、Oracle、MySQL、Linux、prometheus监控&#xff1b;并对SQLserver、NoSQL(…

TCP 端口号为何位于首部前四个字节?协议设计的智慧与启示

知乎的一个问题很有意思&#xff1a;“为什么在TCP首部中要把TCP的端口号放入最开始的四个字节&#xff1f;” 这种问题很适合我这种搞历史的人&#xff0c;大年初一我给出了一个简短的解释&#xff0c;但仔细探究这个问题&#xff0c;我们将会获得 TCP/IP 被定义的过程。 文…

oracle表分区--范围分区

文章目录 oracle表分区分区的原因分区的优势oracle表分区的作用oracle表分区类型一、范围分区二、 创建分区表和使用&#xff1a;1、按照数值范围划分2、按照时间范围3、MAXVALUE2. 向现有表添加新的分区3、 分区维护和重新组织&#xff08;合并/删除&#xff09; oracle表分区…

蓝桥杯(B组)-每日一题(求最大公约数最小公倍数)

题目&#xff1a; 代码展现&#xff1a; #include<iostream> using namespace std; int main() {int m,n,x,y;cin>>m>>n;//输入两个整数int b;bm%n;//取余数xm;//赋值yn;while(b)//当余数不为0的时候{xy;//辗转相除求最小公约数yb;bx%y;}cout<<y<&…

基于STM32的学习环境控制系统设计

&#x1f91e;&#x1f91e;大家好&#xff0c;这里是5132单片机毕设设计项目分享&#xff0c;今天给大家分享的是学习环境控制。 设备的详细功能见网盘中的文章《21、基于STM32的学习环境控制系统设计》&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1uWSZX2zbZwy9sY…

WPS接入DeepSeek模型

1.wps 下载安装 WPS-支持多人在线协作编辑Word、Excel和PPT文档_WPS官方网站 &#xff08;最好是安装最新的wps&#xff09; 2.offieceAi工具下载安装 软件下载 | OfficeAI助手 下载后安装下载下来的两个工具。安装路径可以自行修改 3.打开WPS,点击文件-》 选项-》信任中心 勾…

4. React 中的 CSS

用例中的干净的脚手架的创建可以参考另一篇文章&#xff1a;3.React 组件化开发React官方并没有给出在React中统一的样式风格&#xff1a; 由此&#xff0c;从普通的css&#xff0c;到css modules&#xff0c;再到css in js&#xff0c;有几十种不同的解决方案&#xff0c;上百…

Unity进阶教程AOI算法原理详解

最新课程《全栈双客户端(Unity/Cocos) TurnKey方案》更新了AOI专题&#xff0c;今天分享一下AOI算法的实现原理。 AOI的功能和作用 在MMORPG网路游戏当中&#xff0c;单服同时在线一般都会有几千人。当有个玩家执行一个操作&#xff0c;理想情况下要把玩家的操作广播同步给单…

w~大模型~合集30

我自己的原文哦~ https://blog.51cto.com/whaosoft/13284996 #VideoMamba 视频理解因大量时空冗余和复杂时空依赖&#xff0c;同时克服两个问题难度巨大&#xff0c;CNN 和 Transformer 及 Uniformer 都难以胜任&#xff0c;Mamba 是个好思路&#xff0c;让我们看看本文是…

【ThreeJS Basics 1-3】Hello ThreeJS,实现第一个场景

文章目录 环境创建一个项目安装依赖基础 Web 页面概念解释编写代码运行项目 环境 我的环境是 node version 22 创建一个项目 首先&#xff0c;新建一个空的文件夹&#xff0c;然后 npm init -y , 此时会快速生成好默认的 package.json 安装依赖 在新建的项目下用 npm 安装依…