谈到新技术,每个人都会有点恐惧,怕处理不好。确实,第一次使用新技术会遇到很多坑,这次使用 EasyExcel 这个新技术去做 excel 导出,还要给表格加样式,遇到不同的版本问题,遇到颜色加错了地方,反正各种效果都打不到自己想要的那种,幸好最终看文档解决了,特此写下这篇博客。
EasyExcel导出自定义表格
- 一、导入依赖
- 二、原理分析
- 三、上代码
一、导入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
二、原理分析
-
对于 read 函数主要通过流操作获取
对于 EasyExcel.read 方法中常用的一个 read 函数:java">EasyExcel.read(fileName, head, readListener).sheet().doRead();
三个参数如下:
- fileName:Excel 文件的路径或输入流。
- head:Excel 表头对应的实体类,定义了 Excel 表的结构。
- readListener:数据读取的监听器,定义了读取数据的逻辑。
-
Excel 表头的实体类
在读取 Excel 文件时,需要定义一个实体类来映射 Excel 表头,每个字段对应一个表头列。这个实体类用于指定数据在 Java 对象中的存储结构。
java">public class ExcelData { private String name; private Integer age; // 其他字段... // 省略 getter 和 setter 方法 }
-
数据读取监听器
EasyExcel 提供了 AnalysisEventListener 类来处理 Excel 数据的读取。
需要集成该类,并实现 invoke 方法来处理每一行数据的读取逻辑,以及 doAfterAllAnalysed 方法来处理所有数据解析完成后的逻辑。
public class ExcelDataListener extends AnalysisEventListener<ExcelData> { @Override public void invoke(ExcelData data, AnalysisContext context) { // 处理每一行数据的逻辑 System.out.println("Read data: " + data); } @Override public void doAfterAllAnalysed(AnalysisContext context) { // 所有数据解析完成后的逻辑 } }
-
Excel 写入
EasyExcel 也提供了写入 Excel 文件的功能。可以使用 EasyExcel.write 方法来配置写入参数,然后调用 sheet 方法指定写入的 sheet,最后调用 doWrite 方法执行写入操作。
EasyExcel.write(fileName, head).sheet("Sheet1").doWrite(dataList);
三个参数如下:
- fileName:写入的 Excel 文件路径。
- head:Excel 表头对应的实体类。
- dataList:要写入的数据列表。dataList 是一个 List 集合,其中的元素是实体类的对象。
-
Excel 写入监听器
写入 Excel 文件时进行一些额外的处理,可以使用写入的监听器 WritHandler。
public class ExcelWriteHandler implements WriteHandler { @Override public void sheet(int sheetNo, Sheet sheet) { // 对每个 sheet 进行处理的逻辑 } @Override public void row(int rowNum, Row row) { // 对每一行进行处理的逻辑 } @Override public void cell(int cellNum, Cell cell) { // 对每个单元格进行处理的逻辑 } }
在写入 Excel 文件时,通过 excelWriter.registerWriterHandler( new ExcelWriterHandler() ) 注册写入监听器即可。
三、上代码
先看要求
其实这里的大部分样式,都可以参考 EasyExcel API 文档
导出
@Override
public void importUserSign(ImportUserSignReq req, HttpServletResponse response) {
String projectName = req.getProjectName();
String time = req.getTime();
// 查询第一页数据
List<SignTemplate1> data1 = new ArrayList<>();
data1.add(new SignTemplate1().setE1("序号").setE2("成员姓名").setE3("签到次数").setE4("补签次数").setE5("签到总工时").setE6("最后签到时间"));
data1.addAll(getData1(req));
// 查询第二页数据
List<SignTemplate2> data2 = getData2(req, getData1(req));
Integer maxRow = data2.stream().map(SignTemplate2::getE2).max(Integer::compare).orElse(0); //获取最大行
try {
// 指定文件名
// response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
// response.setCharacterEncoding("utf-8");
// String fileName = URLEncoder.encode("签到模板导出.xlsx", "UTF-8");
// response.setHeader("Content-disposition", "attachment;filename*=" + fileName);
String fileName = "E:\\excel\\" + "签到模板导出" + System.currentTimeMillis() + ".xlsx";
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
// 第一页
// 自定义头部样式
WriteCellStyle headWriteCellStyle1 = new WriteCellStyle();
headWriteCellStyle1.setFillForegroundColor(IndexedColors.GOLD.getIndex()); //背景颜色-黄色
headWriteCellStyle1.setHorizontalAlignment(HorizontalAlignment.LEFT); //左对齐
// 自定义内容样式
WriteCellStyle contentWriteCellStyle1 = new WriteCellStyle();
// 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
HorizontalCellStyleStrategy style1 = new HorizontalCellStyleStrategy(headWriteCellStyle1, contentWriteCellStyle1);
// sheet命名
WriteSheet writeSheet1 = EasyExcel.writerSheet(1, "项目名称")
.registerWriteHandler(style1) //自定义策略
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) //自动列宽
.head(getHead("【"+projectName+"】", time, 6)) //动态表头
.head(SignTemplate1.class)
.build();
// 写入第一页
excelWriter.write(data1, writeSheet1);
// 第二页
// 自定义头部样式
WriteCellStyle headWriteCellStyle2 = new WriteCellStyle();
headWriteCellStyle2.setFillForegroundColor(IndexedColors.GOLD.getIndex()); //背景颜色-黄色
headWriteCellStyle2.setHorizontalAlignment(HorizontalAlignment.LEFT); //左对齐
// 自定义内容样式
WriteCellStyle contentWriteCellStyle2 = new WriteCellStyle();
contentWriteCellStyle2.setHorizontalAlignment(HorizontalAlignment.RIGHT); //右对齐
// 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
HorizontalCellStyleStrategy style2 = new HorizontalCellStyleStrategy(headWriteCellStyle2, contentWriteCellStyle2);
// sheet命名
WriteSheet writeSheet2 = EasyExcel.writerSheet(2, "签到明细")
.registerWriteHandler(style2) //自定义策略
.registerWriteHandler(new CustomCellWriteHandler(maxRow, (data2.size()+1))) //自定义动态行/列背景颜色
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) //自动列宽
.head(getHead("【"+projectName+"】", time, (data2.size()+1))) //动态表头
.build();
// 写入第二页
excelWriter.write(dataList(data2, maxRow), writeSheet2).close();
}catch (Exception e){
e.printStackTrace();
throw new CustomException("导出失败");
}
}
动态标题头
private List<List<String>> getHead(String projectName, String time, Integer num) {
List<List<String>> list = new ArrayList<List<String>>();
for (int i = 0; i < num; i++) {
list.add(Arrays.asList(projectName, time));
}
return list;
}
动态填充数据
private List<List<Object>> dataList(List<SignTemplate2> data2, Integer maxRow) {
List<List<Object>> list = new ArrayList<>();
List<Object> row1 = ListUtils.newArrayList(); //第一行
List<Object> row2 = ListUtils.newArrayList(); //第二行
row1.add("成员名称");
row2.add("签到次数");
for (int i = 0; i <data2.size(); i++) { //行内每一列数据
row1.add(data2.get(i).getE1());
row2.add(data2.get(i).getE2());
}
list.add(row1);
list.add(row2);
for (int i = 0; i < maxRow; i++) {
List<Object> row3 = ListUtils.newArrayList(); //第三行-多条
List<Object> row4 = ListUtils.newArrayList(); //第四行-多条
row3.add(null);
row4.add(null);
for (int j = 0; j <data2.size(); j++) { //行内每一列数据
List<SignTemplate3> eList = data2.get(j).getEList();//当前列的签到集合
if (i < eList.size()) {
row3.add(eList.get(i).getE2()+" "+eList.get(i).getE1());
row4.add(ObjectUtil.isNotNull(eList.get(i).getE3())?eList.get(i).getE3():"暂无");
}else {
row3.add(null);
row4.add(null);
}
}
list.add(row3);
list.add(row4);
}
return list;
}
自定义动态行/列背景颜色
package com.glbTech.business.dto.req.stat;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.AbstractCellWriteHandler;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import lombok.Data;
import org.apache.commons.lang.BooleanUtils;
import org.apache.poi.hssf.usermodel.HSSFPalette;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Data
public class CustomCellWriteHandler extends AbstractCellWriteHandler {
private Integer maxRow;
private Integer maxCol;
private final short colorL = IndexedColors.LIME.getIndex(); //绿色
private final short colorH = IndexedColors.GREY_25_PERCENT.getIndex(); //灰色
public CustomCellWriteHandler(Integer maxRow, Integer maxCol) {
this.maxRow = maxRow;
this.maxCol = maxCol;
}
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
// 自定义样式处理
// 当前事件会在 数据设置到poi的cell里面才会回调
int x = 1;
for (int i = 4; i < (maxRow+2)*2; i=(x*2)) {
Cell cell = context.getCell();
int rowIndex = cell.getRowIndex(); //行
int cellIndex = cell.getColumnIndex(); //行的列
// 判断不是头的情况 如果是fill 的情况 这里会==null 所以用not true
if (BooleanUtils.isNotTrue(context.getHead())) {
if (cellIndex > 0 && (rowIndex==i || rowIndex==i+1)) {
// 拿到poi的workbook
Workbook workbook = context.getWriteWorkbookHolder().getWorkbook();
// 这里千万记住 想办法能复用的地方把他缓存起来 一个表格最多创建6W个样式
// 不同单元格尽量传同一个 cellStyle
CellStyle cellStyle = workbook.createCellStyle();
//设置颜色
if (x%2==0) {
cellStyle.setFillForegroundColor(colorL); //绿色
}else {
cellStyle.setFillForegroundColor(colorH); //灰色
}
cellStyle.setAlignment(HorizontalAlignment.RIGHT); //右对齐
// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
cell.setCellStyle(cellStyle);
// 由于这里没有指定dataformat 最后展示的数据 格式可能会不太正确
// 这里要把 WriteCellData的样式清空, 不然后面还有一个拦截器 FillStyleCellWriteHandler 默认会将 WriteCellStyle 设置到
// cell里面去 会导致自己设置的不一样(很关键)
context.getFirstCellData().setWriteCellStyle(null);
}
}
x++;
}
}
}
好事定律:每件事最后都会是好事,如果不是好事,说明还没到最后。