EasyExcel处理复杂Excel模版导出
- 近期有功能涉及到需要excel单元格合并的模版导出,正巧最近有EasyExcel作者创业的消息,写一篇使用案例支持FastExcel继续更新。
- 官网 easyexcel.opensource.alibaba.com/
- 代码 gitcode.com/gh_mirrors/...
- 建议还是把代码拉下来,可以看到里面有很多作者写的测试类。我们想要实现某些功能的话,可以直接参考官方demo。
模版制作
目标样式如下图: 经过拆分可以把问题分为:
- 基本的数据填充
- 颜色数据的填充
- 列表数据的填充
- 单元格的合并
先根据{param}替换参数的方式一一解决问题
基础数据填充
按照官方的demo,简单的表格导出可以横向填充数据也可以纵向填充数据,但是我们的表格是需要自定义的,所以只能通过模版填充的形式去实现。使用EasyExcel 利用模板填充的方式,就是以一个单元格 为最小单位,把数据全部查出来 ,然后将数据处理成一行一行的形式进行填充,碰到相同的数据,就进行合并单元格。
基础的数据填充使用map做参数对照,对应模版上的{参数名}即可正确填充
javascript
// 表格数据填充
Map<String, String> fileData = getData(assessmentTemplateData);
//单行数据
excelWriter.fill(fileData, writeSheet);
颜色数据填充
使用 CustomRowStyleStrategy 编写某个单元格的颜色背景
java
import com.alibaba.excel.write.handler.AbstractRowWriteHandler;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.IndexedColors;
public class CustomRowStyleStrategy extends AbstractRowWriteHandler {
private final int targetRow; // 目标行号
private final short color; // 背景颜色
public CustomRowStyleStrategy(int targetRow, short color) {
this.targetRow = targetRow;
this.color = color;
}
@Override
protected void afterRowCreate(com.alibaba.excel.metadata.Head head, com.alibaba.excel.write.metadata.holder.WriteSheetHolder writeSheetHolder, Integer relativeRowIndex, Boolean isHead) {
if (relativeRowIndex == targetRow) {
// 获取当前行的所有单元格样式
for (int i = 0; i < 10; i++) { // 假设最多有10列
CellStyle cellStyle = writeSheetHolder.getSheet().getWorkbook().createCellStyle();
cellStyle.setFillForegroundColor(color);
cellStyle.setFillPattern(CellStyle.SOLID_FOREGROUND);
writeSheetHolder.getSheet().setColumnWidth(i, 20 * 256); // 设置列宽
writeSheetHolder.getSheet().getRow(relativeRowIndex).getCell(i).setCellStyle(cellStyle);
}
}
}
}
ini
// 假设你想为第5行(索引从0开始)设置红色背景
int targetRow = 5;
short color = IndexedColors.RED.getIndex();
// 注册自定义行样式策略
excelWriter.registerWriteHandler(new CustomRowStyleStrategy(targetRow, color));
列表数据填充
列表数据可以使用一个对象进行控制,然后对象的属性和模版的{实体类.属性名}对应上,进行循环填充
ini
// 列表数据填充
List<AssessmentTemplate> list = tableDataMap.get(assessmentTemplateData);
//列表循环数据
if (CollectionUtils.isNotEmpty(list)) {
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
excelWriter.fill(new FillWrapper("details", list), fillConfig, writeSheet);
}
单元格的合并
我是重写了单元格合并的策略类,然后注册到writter里进行操作
ExcelFillCellMergeLineUtils
java
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 列合并工具类
*/
public class ExcelFillCellMergeLineUtils implements CellWriteHandler {
private static final String KEY = "%s-%s";
// 所有的合并信息都存在了这个map里面
private Map<String, int[]> mergeInfo = new HashMap<>();
public ExcelFillCellMergeLineUtils() {
}
@Override
public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer integer, Integer integer1, Boolean aBoolean) {
}
@Override
public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer integer, Boolean aBoolean) {
}
@Override
public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, WriteCellData cellData, Cell cell, Head head, Integer integer, Boolean aBoolean) {
}
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> list, Cell cell, Head head, Integer integer, Boolean aBoolean) {
// 当前行
int curRowIndex = cell.getRowIndex();
// 当前列
int curColIndex = cell.getColumnIndex();
int[] mergeColumns = mergeInfo.get(String.format(KEY, curRowIndex, curColIndex));
if (mergeColumns != null) {
// 合并最后一行, 列
mergeWithPrevCol(writeSheetHolder, cell, curRowIndex, curColIndex, mergeColumns);
}
}
public void mergeWithPrevCol(WriteSheetHolder writeSheetHolder, Cell cell, int curRowIndex, int curColIndex, int[] mergeColumns) {
Sheet sheet = writeSheetHolder.getSheet();
for (int i = 0; i < mergeColumns.length; i += 2) {
int startColIndex = mergeColumns[i];
int num = mergeColumns[i + 1];
if (startColIndex + num < startColIndex) {
throw new IllegalArgumentException("合并列范围无效: startColIndex=" + startColIndex + ", num=" + num);
}
CellRangeAddress cellRangeAddress = new CellRangeAddress(curRowIndex, curRowIndex, startColIndex, startColIndex + num);
// 检查是否已经合并
for (int j = 0; j < sheet.getNumMergedRegions(); j++) {
CellRangeAddress existingRegion = sheet.getMergedRegion(j);
if (existingRegion.intersects(cellRangeAddress)) {
throw new IllegalStateException("目标区域已被合并: " + cellRangeAddress);
}
}
// 添加合并区域
sheet.addMergedRegion(cellRangeAddress);
}
}
// 添加需要合并的列信息
// 例如:第5行从第5列开始合并3列,从第8列开始合并3列
// add(4, 4, 2, 7, 2); 表示第5行的第5列到第7列合并,第8列到第10列合并
public void add(int curRowIndex, int startColIndex1, int num1, int startColIndex2, int num2) {
mergeInfo.put(String.format(KEY, curRowIndex, startColIndex1), new int[]{startColIndex1, num1, startColIndex2, num2});
}
public void add(int curRowIndex, int... columns) {
if (columns.length % 2 != 0) {
throw new IllegalArgumentException("列信息必须成对出现(起始列索引和合并数量)");
}
mergeInfo.put(String.format(KEY, curRowIndex, columns[0]), columns);
}
}
ExcelFillCellMergeRowUtils
java
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
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.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import java.util.ArrayList;
import java.util.List;
/**
* 行合并工具类
*/
public class ExcelFillCellMergeRowUtils implements CellWriteHandler {
private static final String KEY = "%s-%s";
// 所有的合并信息都存在了这个map里面
private static class CellMerge {
int colIndex;
int startRow;
int num;
public CellMerge(int colIndex, int startRow, int num) {
this.colIndex = colIndex;
this.startRow = startRow;
this.num = num;
}
}
private List<CellMerge> mergeList = new ArrayList<>();
public ExcelFillCellMergeRowUtils() {
}
@Override
public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer integer, Integer integer1, Boolean aBoolean) {
}
@Override
public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer integer, Boolean aBoolean) {
}
@Override
public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, WriteCellData cellData, Cell cell, Head head, Integer integer, Boolean aBoolean) {
}
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> list, Cell cell, Head head, Integer integer, Boolean aBoolean) {
Sheet sheet = writeSheetHolder.getSheet();
int curRowIndex = cell.getRowIndex();
int curColIndex = cell.getColumnIndex();
for (CellMerge merge : mergeList) {
if (merge.colIndex == curColIndex && merge.startRow == curRowIndex) {
CellRangeAddress region = new CellRangeAddress(
merge.startRow,
merge.startRow + merge.num - 1,
merge.colIndex,
merge.colIndex
);
for (int j = 0; j < sheet.getNumMergedRegions(); j++) {
CellRangeAddress existingRegion = sheet.getMergedRegion(j);
if (existingRegion.intersects(region)) {
throw new IllegalStateException("目标区域已被合并: " + region.formatAsString());
}
}
sheet.addMergedRegion(region);
}
}
}
// 添加需要合并的行信息
// 例如:第5列从第5行开始合并3行,从第8行开始合并3行
// add(4, 4, 2, 7, 2); 表示第5列的第5行到第7行合并,第8行到第10行合并
public void add(int curColIndex, int... rows) {
if (rows.length % 2 != 0) {
throw new IllegalArgumentException("行信息必须成对出现(起始行索引和合并数量)");
}
for (int i = 0; i < rows.length; i += 2) {
int startRow = rows[i];
int num = rows[i + 1];
mergeList.add(new CellMerge(curColIndex, startRow, num));
}
}
}
行合并的业务逻辑
ini
/**
* 行合并
* @param list
* @param mergeRowUtils
*/
private static void mergeRow(List<AssessmentTemplate> list, ExcelFillCellMergeRowUtils mergeRowUtils) {
if (CollectionUtils.isNotEmpty(list)) {
// 从第五行开始(索引为4),合并第二列和第三列
String prevSecondColValue = null;
String prevThirdColValue = null;
int startRowSecondCol = 4; // 第五行的索引
int startRowThirdCol = 4; // 第五行的索引
List<int[]> mergeSecondRanges = new ArrayList<>();
List<int[]> mergeThirdRanges = new ArrayList<>();
// 循环列表行
for (int i = 0; i < list.size(); i++) {
AssessmentTemplate assessmentTemplate = list.get(i);
String secondColValue = assessmentTemplate.getIndicatorType(); // 第二列是 考核维度类型
String thirdColValue = assessmentTemplate.getIndicator(); // 第三列是 考核维度
// 处理第二列合并
if (i == 0 || !secondColValue.equals(prevSecondColValue)) {
// 上一组结束,添加合并范围
if (i > 0 && (i - startRowSecondCol + 4) > 1) {
mergeSecondRanges.add(new int[]{1, startRowSecondCol, i - startRowSecondCol + 4}); // 第二列合并
}
// 新的一组开始
startRowSecondCol = 4 + i;
}
// 处理第三列合并
if (i == 0 || !thirdColValue.equals(prevThirdColValue)) {
if (i > 0 && (i - startRowThirdCol + 4) > 1) {
mergeThirdRanges.add(new int[]{2, startRowThirdCol, i - startRowThirdCol + 4}); // 第三列合并
}
startRowThirdCol = 4 + i;
}
prevSecondColValue = secondColValue;
prevThirdColValue = thirdColValue;
}
// 添加最后一组的合并
if (!list.isEmpty()) {
if((list.size() - startRowSecondCol + 4) > 1){
mergeSecondRanges.add(new int[]{1, startRowSecondCol, list.size() - startRowSecondCol + 4});
}
if((list.size() - startRowThirdCol + 4) > 1){
mergeThirdRanges.add(new int[]{2, startRowThirdCol, list.size() - startRowThirdCol + 4});
}
}
// 执行所有合并操作
mergeThirdRanges.sort((a, b) -> Integer.compare(a[1], b[1])); // 按起始行排序
mergeSecondRanges.sort((a, b) -> Integer.compare(a[1], b[1])); // 按起始行排序
for (int[] range : mergeSecondRanges) {
mergeRowUtils.add(range[0], range[1], range[2]);
}
for (int[] range : mergeThirdRanges) {
mergeRowUtils.add(range[0], range[1], range[2]);
}
}
}
可以根据业务进行 mergeRowUtils操作需要合并的单元格位置,需要注意的是工具类对单元格冲突的问题我采取了直接报错的方式,如果对单元格合并冲突有其他业务要求可以改为其他方式处理。
完整处理代码
controller层方法 可以单个导出也可以批量导出,批量导出可以转为压缩包
ini
/**
* 批量打包导出
*
* @param response
* @param ids
*/
@GetMapping("/exportAssessmentInfo")
public void exportAssessmentInfoForZip(HttpServletResponse response, @RequestParam Long[] ids) {
if (ids == null || ids.length == 0) {
throw new ServiceException("参数错误");
}
try {
// 统一获取数据
Map<String, byte[]> excelBytesMap = assessmentDetailService.exportSingleAssessmentInfo(ids);
if (excelBytesMap.isEmpty()) {
throw new ServiceException("未找到可导出的数据");
}
if (ids.length == 1) {
//导出单个excel
String fileName = excelBytesMap.keySet().iterator().next();
byte[] bytes = excelBytesMap.get(fileName);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Disposition", "attachment;filename=" +
URLEncoder.encode(fileName, StandardCharsets.UTF_8));
try (ServletOutputStream outputStream = response.getOutputStream()) {
outputStream.write(bytes);
outputStream.flush();
} catch (Exception e) {
log.error("导出Excel失败:{}", e.getMessage(), e);
}
} else {
// 导出多个文件打包为ZIP
String zipFileName = "考核信息-" + System.currentTimeMillis() + ".zip";
response.setContentType("application/zip");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Disposition", "attachment;filename=" +
URLEncoder.encode(zipFileName, StandardCharsets.UTF_8));
try (ServletOutputStream outputStream = response.getOutputStream();
ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream(outputStream)) {
for (Map.Entry<String, byte[]> entry : excelBytesMap.entrySet()) {
String fileName = entry.getKey();
byte[] bytes = entry.getValue();
try {
zipOutputStream.putArchiveEntry(new ZipArchiveEntry(fileName));
zipOutputStream.write(bytes);
zipOutputStream.closeArchiveEntry();
} catch (Exception e) {
log.error("添加文件[{}]到压缩包失败", fileName, e);
}
}
zipOutputStream.finish();
outputStream.flush();
} catch (IOException e) {
log.error("导出ZIP失败:{}", e.getMessage(), e);
throw new ServiceException("导出失败:" + e.getMessage());
}
}
} catch (Exception e) {
log.error("批量导出失败:{}", e.getMessage(), e);
throw new ServiceException("系统异常,请联系管理员");
}
}
数据获取后的表格处理,转换为内存流
scss
/**
* 导出
*/
public Map<String,byte[]> exportSingleAssessmentInfo(Long[] assessmentDetailIds) throws Exception {
Map<String,byte[]> result = new HashMap<>();
// 1. 数据获取
Map<AssessmentTemplateData, List<AssessmentTemplate>> tableDataMap = getTableData(assessmentDetailIds);
// 2. 表格处理
for (AssessmentTemplateData assessmentTemplateData : tableDataMap.keySet()) {
String excelName = assessmentTemplateData.getExcelName();
// 表格数据填充
Map<String, String> fileData = getData(assessmentTemplateData);
// 列表数据填充
List<AssessmentTemplate> list = tableDataMap.get(assessmentTemplateData);
// 列合并(合并考核目标 以及考核依据)
ExcelFillCellMergeLineUtils mergeLineUtils = new ExcelFillCellMergeLineUtils();
//首行不需要合并,新增的行都需要合并
for (int i = 0; i < list.size(); i++) {
int rowIndex = 4 + i;
mergeLineUtils.add(rowIndex, 5, 2, 8, 2);
}
// 行合并
ExcelFillCellMergeRowUtils mergeRowUtils = new ExcelFillCellMergeRowUtils();
// 行合并逻辑
mergeRow(list, mergeRowUtils);
// 3. 构建流返回
try (InputStream template = getTemplateInputStream()) {
if (ObjectUtils.isEmpty(template)) {
throw new ServiceException("模板文件不存在");
}
// 写入内存流
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//写入到内存流
try (ExcelWriter excelWriter = EasyExcel.write(byteArrayOutputStream).withTemplate(template)
.registerWriteHandler(new CustomStyleStrategy())
.registerWriteHandler(mergeLineUtils)
.registerWriteHandler(mergeRowUtils)
.build()) {
//构建excel的sheet
WriteSheet writeSheet = EasyExcel.writerSheet().build();
//列表循环数据
if (CollectionUtils.isNotEmpty(list)) {
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
excelWriter.fill(new FillWrapper("details", list), fillConfig, writeSheet);
}
//单行数据
excelWriter.fill(fileData, writeSheet);
excelWriter.finish();
}
result.put(excelName, byteArrayOutputStream.toByteArray());
} catch (Exception e) {
log.error("导出考核信息失败: {}", e.getMessage(), e);
throw new ServiceException("导出失败:" + e.getMessage());
}
}
return result;
}
操作方法
java
/**
* 行合并
* @param list
* @param mergeRowUtils
*/
private static void mergeRow(List<AssessmentTemplate> list, ExcelFillCellMergeRowUtils mergeRowUtils) {
if (CollectionUtils.isNotEmpty(list)) {
// 从第五行开始(索引为4),合并第二列和第三列
String prevSecondColValue = null;
String prevThirdColValue = null;
int startRowSecondCol = 4; // 第五行的索引
int startRowThirdCol = 4; // 第五行的索引
List<int[]> mergeSecondRanges = new ArrayList<>();
List<int[]> mergeThirdRanges = new ArrayList<>();
// 循环列表行
for (int i = 0; i < list.size(); i++) {
AssessmentTemplate assessmentTemplate = list.get(i);
String secondColValue = assessmentTemplate.getIndicatorType(); // 第二列是 考核维度类型
String thirdColValue = assessmentTemplate.getIndicator(); // 第三列是 考核维度
// 处理第二列合并
if (i == 0 || !secondColValue.equals(prevSecondColValue)) {
// 上一组结束,添加合并范围
if (i > 0 && (i - startRowSecondCol + 4) > 1) {
mergeSecondRanges.add(new int[]{1, startRowSecondCol, i - startRowSecondCol + 4}); // 第二列合并
}
// 新的一组开始
startRowSecondCol = 4 + i;
}
// 处理第三列合并
if (i == 0 || !thirdColValue.equals(prevThirdColValue)) {
if (i > 0 && (i - startRowThirdCol + 4) > 1) {
mergeThirdRanges.add(new int[]{2, startRowThirdCol, i - startRowThirdCol + 4}); // 第三列合并
}
startRowThirdCol = 4 + i;
}
prevSecondColValue = secondColValue;
prevThirdColValue = thirdColValue;
}
// 添加最后一组的合并
if (!list.isEmpty()) {
if((list.size() - startRowSecondCol + 4) > 1){
mergeSecondRanges.add(new int[]{1, startRowSecondCol, list.size() - startRowSecondCol + 4});
}
if((list.size() - startRowThirdCol + 4) > 1){
mergeThirdRanges.add(new int[]{2, startRowThirdCol, list.size() - startRowThirdCol + 4});
}
}
// 执行所有合并操作
mergeThirdRanges.sort((a, b) -> Integer.compare(a[1], b[1])); // 按起始行排序
mergeSecondRanges.sort((a, b) -> Integer.compare(a[1], b[1])); // 按起始行排序
for (int[] range : mergeSecondRanges) {
mergeRowUtils.add(range[0], range[1], range[2]);
}
for (int[] range : mergeThirdRanges) {
mergeRowUtils.add(range[0], range[1], range[2]);
}
}
}
/**
* 获取模版输入流
*
* @return
* @throws IOException
*/
private InputStream getTemplateInputStream() throws IOException {
return new PathMatchingResourcePatternResolver()
.getResource("templates/AssessmentTemplate.xlsx").getInputStream();
}
/**
* 设置模版名称字体样式
*
* @param cellStyleMap
*/
private void templateSetRed(Map<String, CustomCellStyleHandler.CellStyleInfo> cellStyleMap, String templateName) {
// 示例:设置第六行第三列的样式
String text2 = "绩效考核评估表(" + templateName + ")";
List<int[]> redIndexRanges2 = new ArrayList<>();
redIndexRanges2.add(new int[]{8, 12}); // 红色部分
cellStyleMap.put("0_1", new CustomCellStyleHandler.CellStyleInfo(text2, redIndexRanges2));
}
/**
* 主体内容
*
* @param assessmentTemplateData
* @return
*/
private Map<String, String> getData(AssessmentTemplateData assessmentTemplateData) {
Map<String, String> fileData = new HashMap<>();
// 模板名称
fileData.put("template_name", assessmentTemplateData.getTemplateName() == null ? "" : assessmentTemplateData.getTemplateName());
// 所属部门
fileData.put("department", assessmentTemplateData.getDepartment() == null ? "" : assessmentTemplateData.getDepartment());
// 岗位
fileData.put("post", assessmentTemplateData.getPost() == null ? "" : assessmentTemplateData.getPost());
// 被考核人
fileData.put("name", assessmentTemplateData.getName() == null ? "" : assessmentTemplateData.getName());
// 评分人
fileData.put("evaluator", assessmentTemplateData.getEvaluator() == null ? "" : assessmentTemplateData.getEvaluator());
// 考核周期
fileData.put("period", assessmentTemplateData.getPeriod() == null ? "" : assessmentTemplateData.getPeriod());
// 目标确认被考核对象
fileData.put("target_assessed_person_name", assessmentTemplateData.getTargetAssessedPersonName() == null ? "" : assessmentTemplateData.getTargetAssessedPersonName());
// 目标确认考核人
fileData.put("target_assess_person_name", assessmentTemplateData.getTargetAssessPersonName() == null ? "" : assessmentTemplateData.getTargetAssessPersonName());
// 目标确认时间
fileData.put("target_assessed_date", assessmentTemplateData.getTargetAssessedDate() == null ? "" : assessmentTemplateData.getTargetAssessedDate());
// 结果确认被考核对象
fileData.put("result_assessed_person_name", assessmentTemplateData.getResultAssessedPersonName() == null ? "" : assessmentTemplateData.getResultAssessedPersonName());
// 结果确认考核人
fileData.put("result_assess_person_name", assessmentTemplateData.getResultAssessPersonName() == null ? "" : assessmentTemplateData.getResultAssessPersonName());
// 结果确认时间
fileData.put("result_assessed_date", assessmentTemplateData.getResultAssessedDate() == null ? "" : assessmentTemplateData.getResultAssessedDate());
// 复盘备注 或者 其他内容
fileData.put("content", assessmentTemplateData.getContent() == null ? "" : assessmentTemplateData.getContent());
return fileData;
}
大致就是以上内容,希望可以帮到你!