RuoYi Excel 导出接口解析:从初始化到响应流写入全流程

RuoYi Excel导出全流程解析

一、注解驱动与 POI 库的协同

Excel 导出的实现依赖两大核心技术:注解驱动的字段配置和POI 库的 Excel 操作能力。​

  • 注解驱动:通过自定义@Excel和@Excels注解,在SysUser实体类中标记需要导出的字段,配置列名、排序、样式等规则,实现 “配置即导出” 的灵活扩展。​
  • POI 库:采用SXSSFWorkbook(POI 的大数据量导出实现)处理 Excel 文件,通过内存缓存与临时文件结合的方式,避免大数据量导出时的内存溢出问题。

二、代码实现

步骤 1:接收请求,触发导出功能

首先我们来看控制器层的代码,这是整个导出功能的入口:

    @PostMapping("/importTemplate")
    public void importTemplate(HttpServletResponse response)
    {
        //创建ExcelUtil工具类实例,指定泛型为SysUser
        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
        //生成并导出模板文件
        util.importTemplateExcel(response, "用户数据");
    }

步骤2:响应设置与初始化

接下来我们看 importTemplateExcel 方法的实现:

这个方法是模板导出的核心,主要完成了三件事:

  1. 设置 HTTP 响应的内容类型为 xlsx 格式,告诉浏览器这是一个 Excel 文件
  2. 设置字符编码为 UTF-8,解决中文乱码问题
  3. 初始化 Excel 相关参数并导出文件
    /**
     * 对list数据源将其里面的数据导入到excel表单
     * 
     * @param sheetName 工作表的名称
     * @param title 标题
     * @return 结果
     */
    public void importTemplateExcel(HttpServletResponse response, String sheetName, String title)
    {
        // 设置响应内容类型为Excel 2007+格式(.xlsx)
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        // 设置响应字符编码为UTF-8,避免文件名或内容中文乱码
        response.setCharacterEncoding("utf-8");
        // 初始化Excel参数:数据列表为null(模板无需数据)、工作表名、标题、类型为导入模板
        this.init(null, sheetName, title, Type.IMPORT);
        // 将初始化好的Excel模板写入响应流,返回给客户端
        exportExcel(response);
    }

步骤3:初始化流程,构建 Excel 基础结构

init 方法负责初始化 Excel 的基础结构,是整个工具类的核心方法之一:

    public void init(List<T> list, String sheetName, String title, Type type)
    {
        if (list == null)
        {
            list = new ArrayList<T>();
        }
        this.list = list;
        this.sheetName = sheetName;
        this.type = type;
        this.title = title;
        //得到所有定义字段
        createExcelField();
        //创建一个工作簿
        createWorkbook();
        //创建excel第一行标题
        createTitle();
        //创建对象的子列表名称
        createSubHead();
    }

初始化过程清晰地分为几个步骤:

  1. 处理数据列表(模板导出时为 null)
  2. 保存基本参数(工作表名、标题、操作类型等)
  3. 收集并处理需要导出的字段信息
  4. 创建 Excel 工作簿和工作表
  5. 生成标题行和子列表头

步骤4:字段处理,注解驱动的灵活配置

字段处理是 Excel 导出的关键环节,通过注解可以灵活配置导出的列信息:

    /**
     * 得到所有定义字段
     */
    private void createExcelField()
    {
        this.fields = getFields();
        //对ExcelUtil工具类中收集到的字段列表(fields)按照@Excel注解的sort()属性进行排序
        this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
        this.maxHeight = getRowHeight();
    }

getFields () 方法则负责收集所有需要导出的字段:

 /**
     * 获取字段注解信息
     */
    public List<Object[]> getFields()
    {
        List<Object[]> fields = new ArrayList<Object[]>();
        List<Field> tempFields = new ArrayList<>();
        // 收集父类的所有声明字段
        tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
        // 收集当前类的所有声明字段
        tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
        for (Field field : tempFields)
        {
            // 排除需要排除的字段(excludeFields中配置的字段名)
            if (!ArrayUtils.contains(this.excludeFields, field.getName()))
            {
                // 处理单注解@Excel
                if (field.isAnnotationPresent(Excel.class))
                {
                    Excel attr = field.getAnnotation(Excel.class);
                    // 注解不为空,且注解的type匹配当前操作类型(ALL或指定类型,如IMPORT)
                    if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
                    {
                        field.setAccessible(true);// 暴力访问私有字段
                        fields.add(new Object[] { field, attr });// 存入结果列表
                    }
                    // 处理集合类型字段(如List<子对象>,需解析子对象的注解)
                    if (Collection.class.isAssignableFrom(field.getType()))
                    {
                        subMethod = getSubMethod(field.getName(), clazz);// 获取子对象的getter方法
                        // 获取集合的泛型类型(如List<UserRole>中的UserRole)
                        ParameterizedType pt = (ParameterizedType) field.getGenericType();
                        Class<?> subClass = (Class<?>) pt.getActualTypeArguments()[0];
                        // 收集子对象中标记了@Excel的字段
                        this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class);
                    }
                }

                // 处理多注解@Excels(一个字段可能对应多个Excel列)
                if (field.isAnnotationPresent(Excels.class))
                {
                    Excels attrs = field.getAnnotation(Excels.class);
                    Excel[] excels = attrs.value();// 获取@Excels中包含的多个@Excel注解
                    for (Excel attr : excels)
                    {
                        // 排除子字段中需要排除的字段(格式:父字段名.子字段名)
                        if (!ArrayUtils.contains(this.excludeFields, field.getName() + "." + attr.targetAttr())
                                && (attr != null && (attr.type() == Type.ALL || attr.type() == type)))
                        {
                            field.setAccessible(true);
                            fields.add(new Object[] { field, attr });
                        }
                    }
                }
            }
        }
        return fields;
    }

这个方法的设计非常灵活,支持:

  1. 同时处理当前类和父类的字段
  2. 支持 @Excel 单注解和 @Excels 多注解
  3. 支持字段过滤(通过 excludeFields 配置)
  4. 支持嵌套对象(集合类型字段)的导出
  5. 支持按操作类型(导入 / 导出)过滤字段

步骤5:工作簿与表头创建,构建 Excel 骨架

createWorkbook 方法负责创建 Excel 工作簿和工作表:

    /**
     * 创建一个工作簿
     */
    public void createWorkbook()
    {
        //创建 SXSSFWorkbook 实例
        this.wb = new SXSSFWorkbook(500);
        // 创建一个新的工作表(Sheet)
        this.sheet = wb.createSheet();
        // 为第0个工作表设置名称(sheetName即传入的"用户数据")
        wb.setSheetName(0, sheetName);
        //创建单元格样式集合
        this.styles = createStyles(wb);
    }

这里使用了 SXSSFWorkbook 而不是 XSSFWorkbook,这是因为 SXSSFWorkbook 是 POI 库中用于处理大数据量的类,它会将超出指定行数的数据写入临时文件,从而减少内存占用。

createTitle 和 createSubHead 方法则负责创建 Excel 的标题和表头:

    /**
     * 创建excel第一行标题
     */
    public void createTitle()
    {
        if (StringUtils.isNotEmpty(title))
        {
            subMergedFirstRowNum++;
            subMergedLastRowNum++;
            // 初始值:主字段的最后一列索引(从0开始)
            int titleLastCol = this.fields.size() - 1;
            if (isSubList()) // 如果存在子列表(嵌套对象的字段)
            {
                // 加上子字段的列数,计算总结束列
                titleLastCol = titleLastCol + subFields.size() - 1;
            }
            // 确定标题行的位置:如果是第一行(rownum==0),则创建后自增rownum;否则固定在第0行
            Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0);
            // 设置行高为30磅(使标题更醒目)
            titleRow.setHeightInPoints(30);
            // 在标题行的第0列创建单元格
            Cell titleCell = titleRow.createCell(0);
            // 应用预定义的“标题样式”(如加粗、居中、大字体)
            titleCell.setCellStyle(styles.get("title"));
            // 设置标题内容
            titleCell.setCellValue(title);
            //合并标题单元格
            sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), titleRow.getRowNum(), titleLastCol));
        }
    }
    /**
     * 创建对象的子列表名称
     */
    public void createSubHead()
    {
        //判断是否存在嵌套子列表的方法
        if (isSubList())
        {
            // 子列表合并区域的起始行号+1
            subMergedFirstRowNum++;
            // 子列表合并区域的结束行号+1
            subMergedLastRowNum++;
            // 创建子列表的表头行(rownum是当前行号计数器,确保在正确位置创建)
            Row subRow = sheet.createRow(rownum);
            // 列索引计数器,从0开始
            int excelNum = 0;
            // fields是主字段列表(通过getFields()获取)
            for (Object[] objects : fields)
            {
                Excel attr = (Excel) objects[1];
                // 在子表头行创建当前列的单元格
                Cell headCell1 = subRow.createCell(excelNum);
                // 设置单元格内容为主字段的注解名称
                headCell1.setCellValue(attr.name());
                // 应用表头样式
                headCell1.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor())));
                // 列索引+1,处理下一列
                excelNum++;
            }
            // 计算主字段表头的最后一列索引
            int headFirstRow = excelNum - 1;
            // 计算子字段的最后一列索引
            int headLastRow = headFirstRow + subFields.size() - 1;
            // 如果子字段数量>0(即存在子字段),则合并主字段表头单元格与子字段列
            if (headLastRow > headFirstRow)
            {
                sheet.addMergedRegion(new CellRangeAddress(rownum, rownum, headFirstRow, headLastRow));
            }
            rownum++;
        }
    }

这些方法处理了复杂的表头结构,包括标题合并、多级表头展示等,使导出的 Excel 更加美观和易用。

步骤6:导出文件,写入响应流

最后,exportExcel 方法将构建好的 Excel 写入响应流:

    /**
     * 对list数据源将其里面的数据导入到excel表单
     * 
     * @return 结果
     */
    public void exportExcel(HttpServletResponse response)
    {
        try
        {
            writeSheet();
            wb.write(response.getOutputStream());
        }
        catch (Exception e)
        {
            log.error("导出Excel异常{}", e.getMessage());
        }
        finally
        {
            IOUtils.closeQuietly(wb);
        }
    }

gitee项目笔记:https://gitee.com/boring8/ruo-yi-annotation.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值