一、注解驱动与 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 方法的实现:
这个方法是模板导出的核心,主要完成了三件事:
- 设置 HTTP 响应的内容类型为 xlsx 格式,告诉浏览器这是一个 Excel 文件
- 设置字符编码为 UTF-8,解决中文乱码问题
- 初始化 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();
}
初始化过程清晰地分为几个步骤:
- 处理数据列表(模板导出时为 null)
- 保存基本参数(工作表名、标题、操作类型等)
- 收集并处理需要导出的字段信息
- 创建 Excel 工作簿和工作表
- 生成标题行和子列表头
步骤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;
}
这个方法的设计非常灵活,支持:
- 同时处理当前类和父类的字段
- 支持 @Excel 单注解和 @Excels 多注解
- 支持字段过滤(通过 excludeFields 配置)
- 支持嵌套对象(集合类型字段)的导出
- 支持按操作类型(导入 / 导出)过滤字段
步骤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);
}
}
RuoYi Excel导出全流程解析
3817

被折叠的 条评论
为什么被折叠?



