跳到主要内容

流式导出Excel工具

声明函数式接口,接收自定义分页查询函数

提前总结

  • 支持大批量数据导出
  • 支持自定义sheet最大行数,并进行多sheet页数据生成
  • 支持自适应表头宽度(默认最长列宽,也可自定义列宽)
  • 支持自定义表头、表数据样式处理

自定义Excel注解

@Excel注解

import java.lang.annotation.ElementType;  
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.math.BigDecimal;

/**
* 自定义导出Excel数据注解
*
* @author Cheems
*/@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Excel
{
/**
* 导出时在excel中排序
*/
public int sort() default Integer.MAX_VALUE;

/**
* 导出到Excel中的名字.
*/ public String name() default "";

/**
* 日期格式, 如: yyyy-MM-dd
*/ public String dateFormat() default "";

/**
* 如果是字典类型,请设置字典的type值 (如: sys_user_sex)
*/ public String dictType() default "";

/**
* 读取内容转表达式 (如: 0=男,1=女,2=未知)
*/ public String readConverterExp() default "";

/**
* 分隔符,读取字符串组内容
*/
public String separator() default ",";

/**
* BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化)
*/ public int scale() default -1;

/**
* BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN
*/ public int roundingMode() default BigDecimal.ROUND_HALF_EVEN;

/**
* 导出类型(0数字 1字符串)
*/
public ColumnType cellType() default ColumnType.STRING;

/**
* 导出时在excel中每个列的高度 单位为字符
*/
public double height() default 14;

/**
* 导出时在excel中每个列的宽 单位为字符
*/
public double width() default 16;

/**
* 文字后缀,如% 90 变成90%
*/ public String suffix() default "";

/**
* 当值为空时,字段的默认值
*/
public String defaultValue() default "";

/**
* 提示信息
*/
public String prompt() default "";

/**
* 设置只能选择不能输入的列内容.
*/ public String[] combo() default {};

/**
* 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写.
*/ public boolean isExport() default true;

/**
* 另一个类中的属性名称,支持多级获取,以小数点隔开
*/
public String targetAttr() default "";

/**
* 是否自动统计数据,在最后追加一行统计数据总和
*/
public boolean isStatistics() default false;

/**
* 导出字段对齐方式(0:默认;1:靠左;2:居中;3:靠右)
*/
Align align() default Align.AUTO;

public enum Align
{
AUTO(0), LEFT(1), CENTER(2), RIGHT(3);
private final int value;

Align(int value)
{
this.value = value;
}

public int value()
{
return this.value;
}
}

/**
* 字段类型(0:导出导入;1:仅导出;2:仅导入)
*/
Type type() default Type.ALL;

public enum Type
{
ALL(0), EXPORT(1), IMPORT(2);
private final int value;

Type(int value)
{
this.value = value;
}

public int value()
{
return this.value;
}
}

public enum ColumnType
{
NUMERIC(0), STRING(1);
private final int value;

ColumnType(int value)
{
this.value = value;
}

public int value()
{
return this.value;
}
}
}

@Excel注解的使用

需要导出的属性需要设置@Excel注解; 不需要导出的属性需要设置@ExcelIgnore注解; @Excel的sort字段可不填,但如果需要自定义某列的宽度时,本对象内的所有@Excel注解的sort字段必填(用于定位列位置)

import com.alibaba.excel.annotation.ExcelIgnore;   
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
public class BizWorkLineMachineDTO {

@ApiModelProperty(value = "作业票号")
@Excel(name = "作业票号", sort = 1)
private String workTicketNo;

@ApiModelProperty(value = "作业线名称")
@Excel(name = "作业线名称", sort = 2)
private String workLineName;

@ApiModelProperty(value = "班次")
@Excel(name = "班次", sort = 3)
private String classNoName;

@ApiModelProperty(value = "作业状态")
@Excel(name = "作业状态", sort = 4)
private String workStatus;

@ApiModelProperty(value = "作业类型名称")
@Excel(name = "作业类型", width=10, sort = 5)
private String workTypeName;

@ApiModelProperty(value = "作业类型")
@ExcelIgnore
private Integer workType;
}

@Excel注解的处理

声明一个注解处理类,处理目标对象,提取@Excel注解相关信息

  
import cn.hutool.core.util.ReflectUtil;

import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;

/**
* @author Cheems
* @since 2025-02-25 14:52
*/public class ExcelFieldFilter {
public static List<Field> getAnnotatedFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
Class<?> current = clazz;
while (current != null) {
for (Field field : current.getDeclaredFields()) {
if (field.isAnnotationPresent(Excel.class)) {
fields.add(field);
}
}
current = current.getSuperclass();
}
return fields.stream()
.sorted(Comparator.comparing(f ->
f.getAnnotation(Excel.class).sort()))
.collect(Collectors.toList());
}


public static Map<Integer, Field> getFieldMap(Class<?> clazz) {
Map<Integer, Field> map = new LinkedHashMap<>();
Field[] fields = ReflectUtil.getFields(clazz);
List<Field> fieldList = Arrays.stream(fields).filter(e -> e.isAnnotationPresent(Excel.class))
.sorted(Comparator.comparingInt(f -> {
return f.getAnnotation(Excel.class).sort();
})).collect(Collectors.toList());


for (int i = 0; i < fieldList.size(); i++) {
Field field = fieldList.get(i);
field.setAccessible(true);
map.put(i, field);
}
return map;
}
}

流式导出excel工具

声明函数式接口

用于传入分页查询函数

import java.util.List;  

@FunctionalInterface
public interface PageQueryFunction<Q, R> {
List<R> query(int pageNum, int pageSize, Q queryParams);
}

导出工具

import com.alibaba.excel.EasyExcel;  
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

/**
* @author Cheems
* @since 2025-02-24 15:37
*/public class StreamingExcelUtil<T> {

private static final Logger log = LoggerFactory.getLogger(StreamingExcelUtil.class);

/**
* Excel sheet最大行数,默认65536
*/ public static final int sheetSize = 59999;

/**
* 工作表名称
*/
private String sheetName;

/**
* 实体对象
*/
public Class<T> clazz;

public StreamingExcelUtil(Class<T> clazz) {
this.clazz = clazz;
}

public void init(String sheetName) {
this.sheetName = sheetName;
}

/**
* 通用流式导出方法
*
* @param response HTTP响应对象
* @param queryParams 查询参数(任意类型)
* @param pageSize 分页大小
* @param queryFunction 查询方法(函数式接口)
* @param clazz Excel映射类
* @param <Q> 查询参数类型
* @param <R> 返回结果类型
*/
public <Q, R> void exportStreaming(
HttpServletResponse response,
String fileName,
Q queryParams,
int pageSize,
PageQueryFunction<Q, R> queryFunction,
Class<R> clazz) throws IOException {
// 初始化响应流
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
this.init(fileName);
ExcelWriter excelWriter = null;
try {
// 构建ExcelWriter(根据是否自定义表头)
// 注册自定义处理器
excelWriter = EasyExcel.write(response.getOutputStream()).build();


// Sheet控制变量
int currentSheetIndex = 1;
int currentSheetRowCount = 0;
WriteSheet currentSheet = createNewSheet(clazz, currentSheetIndex);

// 分页查询处理
int pageNum = 1;
while (true) {
List<R> dataChunk = queryFunction.query(pageNum, pageSize, queryParams);
if (dataChunk == null || dataChunk.isEmpty()) break;

int remainingRows = sheetSize - currentSheetRowCount;

if (remainingRows <= 0) {
// 创建新Sheet
currentSheetIndex++;
currentSheet = createNewSheet(clazz, currentSheetIndex);
currentSheetRowCount = 0;
remainingRows = sheetSize;
}

if (dataChunk.size() <= remainingRows) {
// 整块写入当前Sheet
excelWriter.write(dataChunk, currentSheet);
currentSheetRowCount += dataChunk.size();
} else {
// 分割数据块
List<R> firstPart = dataChunk.subList(0, remainingRows);
List<R> secondPart = dataChunk.subList(remainingRows, dataChunk.size());

excelWriter.write(firstPart, currentSheet);
currentSheetRowCount += remainingRows;

// 创建新Sheet写入剩余数据
currentSheetIndex++;
currentSheet = createNewSheet(clazz, currentSheetIndex);
excelWriter.write(secondPart, currentSheet);
currentSheetRowCount = secondPart.size();
}

dataChunk.clear();
pageNum++;
}
} finally {
if (excelWriter != null) {
excelWriter.finish();
}
}
}


private WriteSheet createNewSheet(Class<?> clazz, int sheetIndex) {
String sheetName;
// 设置工作表的名称.
if (sheetIndex == 0) {
sheetName = this.sheetName;
} else {
sheetName = this.sheetName + sheetIndex;
}
return EasyExcel.writerSheet(sheetName)
.relativeHeadRowIndex(1)
.registerWriteHandler(new CustomColumnWidthStyleStrategy(clazz))
.registerWriteHandler(new CustomDataWriteHandler(clazz))
.registerWriteHandler(new CustomExcelHeaderHandler(clazz))
.build();
}
}

自定义Excel处理器

表头处理器

  
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import org.apache.poi.ss.usermodel.*;

import java.lang.reflect.Field;
import java.util.List;

/**
* @author Cheems
* @since 2025-02-24 15:42
*/public class CustomExcelHeaderHandler implements SheetWriteHandler {
private final Class<?> dataClazz;

public CustomExcelHeaderHandler(Class<?> dataClazz) {
this.dataClazz = dataClazz;
}

@Override
public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {

}

@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
// 获取Sheet对象
Sheet sheet = writeSheetHolder.getSheet();
Workbook workbook = sheet.getWorkbook();

// 创建表头行
Row headerRow = sheet.createRow(0);

// 获取所有字段的Excel注解
List<Field> fields = ExcelFieldFilter.getAnnotatedFields(dataClazz);

// 创建表头样式(对应原ExcelUtil的header样式)
CellStyle headerStyle = createHeaderStyle(workbook);

// 创建表头单元格
int columnIndex = 0;
for (Field field : fields) {
Excel excelAnnotation = field.getAnnotation(Excel.class);
if (excelAnnotation != null) {
Cell cell = headerRow.createCell(columnIndex++);
cell.setCellValue(excelAnnotation.name());
cell.setCellStyle(headerStyle);
}
}
}

private CellStyle createHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
style.setBorderRight(BorderStyle.THIN);
style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBorderLeft(BorderStyle.THIN);
style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBorderTop(BorderStyle.THIN);
style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBorderBottom(BorderStyle.THIN);
style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());

Font font = workbook.createFont();
font.setFontHeightInPoints((short) 10);
font.setBold(true);
font.setColor(IndexedColors.WHITE.getIndex());
style.setFont(font);
return style;
}
}

表数据处理器

import com.alibaba.excel.metadata.CellData;  
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import org.apache.poi.ss.usermodel.*;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* @author Cheems
* @since 2025-02-24 15:47
*/public class CustomDataWriteHandler implements CellWriteHandler {
private final Class<?> dataClazz;
private final Map<String, CellStyle> styleCache = new ConcurrentHashMap<>();

public CustomDataWriteHandler(Class<?> dataClazz) {
this.dataClazz = dataClazz;
}

@Override
public void beforeCellCreate(WriteSheetHolder writeSheetHolder,
WriteTableHolder writeTableHolder,
Row row,
Head head,
Integer columnIndex,
Integer relativeRowIndex,
Boolean isHead) {
// 用于提前准备数据(如果需要)
}

@Override
public void afterCellCreate(WriteSheetHolder writeSheetHolder,
WriteTableHolder writeTableHolder,
Cell cell,
Head head,
Integer relativeRowIndex,
Boolean isHead) {
if (isHead) return; // 跳过表头

Workbook workbook = writeSheetHolder.getSheet().getWorkbook();

// 获取字段元数据
List<Field> fields = ExcelFieldFilter.getAnnotatedFields(dataClazz);
if (cell.getColumnIndex() + 1 > fields.size()) return;
Field field = fields.get(cell.getColumnIndex());
Excel excel = field.getAnnotation(Excel.class);

// 创建或获取样式
String styleKey = buildStyleKey(excel);
CellStyle style = styleCache.computeIfAbsent(styleKey, k ->
createDataStyle(workbook, excel));

// 应用样式
cell.setCellStyle(style);
}

@Override
public void afterCellDataConverted(WriteSheetHolder writeSheetHolder,
WriteTableHolder writeTableHolder,
CellData cellData,
Cell cell,
Head head,
Integer relativeRowIndex,
Boolean isHead) {
if (isHead) return;

// 获取字段和注解
List<Field> fields = ExcelFieldFilter.getAnnotatedFields(dataClazz);
if (cell.getColumnIndex() + 1 > fields.size()) return;
Field field = fields.get(cell.getColumnIndex());
Excel excel = field.getAnnotation(Excel.class);

// 处理特殊数据类型
Object value = cellData.getData();
if (value instanceof Date && StringUtils.isNotEmpty(excel.dateFormat())) {
cell.setCellValue(DateUtils.formatDate((Date) value, excel.dateFormat()));
} else if (value instanceof Number && excel.scale() >= 0) {
BigDecimal bd = new BigDecimal(value.toString())
.setScale(excel.scale(), excel.roundingMode());
cell.setCellValue(bd.toString());
}
}

@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder,
WriteTableHolder writeTableHolder,
List<CellData> cellDataList,
Cell cell,
Head head,
Integer relativeRowIndex,
Boolean isHead) {
// 清理资源(如有需要)
}

private String buildStyleKey(Excel excel) {
return excel.align().name() + "_" + excel.scale();
}

private CellStyle createDataStyle(Workbook workbook, Excel excel) {
CellStyle style = workbook.createCellStyle();

// 对齐方式
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);

// 边框
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());

// 字体
Font font = workbook.createFont();
font.setFontHeightInPoints((short) 10);
style.setFont(font);

// 数字格式
if (excel.scale() > 0) {
String format = "0." + StringUtils.repeat("0", excel.scale());
style.setDataFormat(workbook.createDataFormat().getFormat(format));
}

return style;
}
}

自定义表宽度处理策略

import com.alibaba.excel.enums.CellDataTypeEnum;  
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.util.CollectionUtils;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import org.apache.poi.ss.usermodel.Cell;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CustomColumnWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {
private static final int MAX_COLUMN_WIDTH = 255;
private final Class<?> dataClazz;
private Map<String, Map<Integer, Integer>> cache = new HashMap<>(8);

public CustomColumnWidthStyleStrategy(Class<?> clazz) {
dataClazz = clazz;
}

protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
// 使用该自定义行宽策略时,必须声明@Excel的sort字段,用以定位类属性位置
boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
if (needSetWidth) {
int columnIndex = cell.getColumnIndex();
Map<Integer, Field> fieldMap = ExcelFieldFilter.getFieldMap(dataClazz);
if (fieldMap.containsKey(columnIndex)) {
Map<Integer, Integer> maxColumnWidthMap = (Map) this.cache.get(writeSheetHolder.getSheetName());
if (maxColumnWidthMap == null) {
maxColumnWidthMap = new HashMap<>(16);
this.cache.put(writeSheetHolder.getSheetName(), maxColumnWidthMap);
}
Field field = fieldMap.get(columnIndex);
int columnWidth = 0;
if (field.isAnnotationPresent(Excel.class)) {
Excel annotation = field.getAnnotation(Excel.class);
int width = (int)annotation.width();
if (width != 16) {
// 16为默认值
// 设置了width属性
columnWidth = width;
} else {
columnWidth = this.dataLength(cellDataList, cell, isHead);
}
}else{
columnWidth = this.dataLength(cellDataList, cell, isHead);
}
if (columnWidth >= 0) {
if (columnWidth > 255) {
columnWidth = 255;
}
Integer maxColumnWidth = (Integer) maxColumnWidthMap.get(columnIndex);
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(columnIndex, columnWidth);
writeSheetHolder.getSheet().setColumnWidth(columnIndex, columnWidth * 256);
}

}
}

}
}

private Integer dataLength(List<CellData> cellDataList, Cell cell, Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes().length;
} else {
CellData cellData = (CellData) cellDataList.get(0);
CellDataTypeEnum type = cellData.getType();
if (type == null) {
return -1;
} else {
switch (type) {
case STRING:
return cellData.getStringValue().getBytes().length;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes().length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes().length;
default:
return -1;
}
}
}
}
}

流式导出工具的使用


StreamingExcelUtil<BizWorkLineMachineDTO> util = new StreamingExcelUtil<BizWorkLineMachineDTO>(BizWorkLineMachineDTO.class);
util.exportStreaming(
response,
"作业机械汇总",
bizWorkLineMachine, // 查询参数
5000, // 分页大小
(pageNum, pageSize, q) -> {
// 使用PageHelper分页(或自定义分页逻辑)
Page<?> page = new Page<>(pageNum, pageSize);
page.setMaxLimit(5000L);
return bizWorkLineMachineMapper.selectMachineListSummary(page, q);
},
BizWorkLineMachineDTO.class
);