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

作者:后端小肥肠

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

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

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

姊妹篇:

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

目录

[1. 前言](#1. 前言)

[2. Fastexcel介绍](#2. Fastexcel介绍)

[3. 技术实现](#3. 技术实现)

[3.1. 表结构及实体类说明](#3.1. 表结构及实体类说明)

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

[3.3. 注解编写](#3.3. 注解编写)

[3.4. 动态设置下拉框核心工具类编写](#3.4. 动态设置下拉框核心工具类编写)

[3.5. 制作多选下拉框exel模板](#3.5. 制作多选下拉框exel模板)

[3.6. 导出模板方法](#3.6. 导出模板方法)

[3.7 导入数据方法](#3.7 导入数据方法)

[4. 源码地址](#4. 源码地址)

[5. 结语](#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博客

sql 复制代码
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条数据:

sql 复制代码
INSERT INTO students (student_id, name, gender, parent_occupation, home_area)
VALUES
('S10001', '张三', '男', '教师', '{"北京", "上海"}'),
('S10002', '李四', '女', '医生', '{"广州", "深圳"}'),
('S10003', '王五', '男', '工程师', '{"北京"}'),
('S10004', '赵六', '女', '律师', '{"杭州", "南京"}'),
('S10005', '孙七', '男', '商人', '{"上海", "广州"}'),
('S10006', '周八', '女', '公务员', '{"北京", "武汉"}'),
('S10007', '吴九', '男', '教师', '{"成都"}'),
('S10008', '郑十', '女', '护士', '{"重庆", "成都"}'),
('S10009', '冯十一', '男', '程序员', '{"深圳", "上海"}'),
('S10010', '陈十二', '女', '医生', '{"北京", "上海", "广州"}');
  1. 实体类
java 复制代码
@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编写

java 复制代码
@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:PostgreSQLtext[]**数组类型转换为 List<String>
  • 应用场景:适用于需要在单个字段中存储多个值的情况,如学生所属区域(可以属于多个区域)的存储和读取
  • 技术特点:
  • 继承自 BaseTypeHandler<List<String>>
  • 使用 @MappedTypes 注解指定处理 List 类型
  • 使用 @ConditionalOnClass实现条件化配置

3.3. 注解编写

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

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

java 复制代码
@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

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

2. processDropDownField

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

3. updateHiddenSheet

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

4. createDropDownList

java 复制代码
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文件
  1. 点击顶部【文件】后点击【选项】
  1. 在弹出的弹窗中,点击【信任中心】选项页中的【信任中心设置】按钮
  1. 开启宏
  1. 另存为.xlsm文件
  1. 编写VBA代码

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

vbnet 复制代码
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.UndoApplication.EnableEvents确保操作的原子性和避免事件循环

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

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

3.6. 导出模板方法

java 复制代码
    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 导入数据方法

java 复制代码
    @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下拉框(支持多选),解决了实际开发中的常见需求。如有疑问,欢迎在评论区留言,我看到都会回复。

相关推荐
不亭4 分钟前
python自动化测试之Pytest断言及Allure报告定制
开发语言·python·pytest
txwtech8 分钟前
QT使用QAbstractTableModel 0x8读取访问权限冲突
开发语言·数据库·qt
众乐乐_200810 分钟前
Spring实现AOP功能的原理:代理模式(JDK动态代理与CGLIB字节码生成技术代理)的理解
java·spring·代理模式
.Net Core 爱好者10 分钟前
基于Flask搭建AI应用,本地私有化部署开源大语言模型
人工智能·后端·python·语言模型·自然语言处理·flask
柳鲲鹏14 分钟前
QT 5.15.2 开发地图ArcGIS 100.15.6(ArcGIS Runtime SDK for Qt)
开发语言·qt·arcgis
不会玩技术的技术girl21 分钟前
使用Python爬虫获取1688 App原数据API接口
开发语言·爬虫·python
zhenryx30 分钟前
前端-导出png,jpg,pptx,svg
开发语言·前端·javascript
m0_7482513530 分钟前
JAVA (Springboot) i18n国际化语言配置
java
琛哥的程序33 分钟前
【计算机毕业设计】Spring Boot教师人事档案管理系统功能说明
java·毕业设计·课程设计
硕风和炜37 分钟前
【LeetCode: 8. 字符串转换整数 (atoi) + 模拟】
java·算法·leetcode·面试·模拟