文章目录
- 前言
- 一、easyExcel的优势
- 二、开始修改
- 1.访问方式改为post,接收页面参数
- 2.模仿官方实例,实现下载
- 3.自定义样式
- 三、样式修改
- 源码
- 1.自定义样式,进行表头和内容的样式设置
- 2.修改自定义拦截器,进行更灵活的内容设置
- 3、列宽固定设置
- 4.异常处理,不下载表格
- 遗留问题
- 请求参数
前言
之前使用poi实现了简单的数据填充及表格下载,由于时间花费太长,所以改为easyExcel进行处理。
并且本次修改的要求是,页面上选择好的数据,直接下载,去掉了跟数据库交互的代码。
链接: easyExcel官方文档地址.
easyExcel 实测 3000条数据,花费 2.91秒,已经满足我的需要。
一、easyExcel的优势
相对来说,比poi更加简单入手,使用简单的几行代码就可以实现表格下载功能
例如(代码来源于官方文档)
/**
* 文件下载(失败了会返回一个有部分数据的Excel)
* <p>
* 1. 创建excel对应的实体对象 参照{@link DownloadData}
* <p>
* 2. 设置返回的 参数
* <p>
* 3. 直接写,这里注意,finish的时候会自动关闭OutputStream,当然你外面再关闭流问题不大
*/
@GetMapping("download")
public void download(HttpServletResponse response) throws IOException {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), DownloadData.class).sheet("模板").doWrite(data());
}
可以看到,跟poi相比省去了很多代码,其中的data()方法,就是需要我们自己补充的数据查询的业务逻辑。
二、开始修改
1.访问方式改为post,接收页面参数
@PostMapping("download")
public void download(HttpServletResponse response, @RequestBody List<Customer> customerList)
其中的Customer,就是页面上的数据对应的实体类对象
2.模仿官方实例,实现下载
总体的结果很明显,从之前的6秒,减少到600毫秒,但是问题也出现了。
3.自定义样式
根据官方的例子,下载后的表格样式几乎没有,所以如果有样式的需求,则需要自己动手修改样式。
样式要求如下:
表头:背景颜色:天蓝色,字体:宋体、居中、大小14、加粗,
内容:无背景色,字体:宋体,居中,大小12
表格宽度自适应,并且如果某一列数据量太大,需要固定该列的宽,并自动换行
三、样式修改
源码
1.自定义样式,进行表头和内容的样式设置
- 自定义拦截器,继承 HorizontalCellStyleStrategy,重新构造方法。
public class CustomHandler extends HorizontalCellStyleStrategy {
private static final String RECORD = "Record";
public CustomHandler(WriteCellStyle headWriteCellStyle, WriteCellStyle contentWriteCellStyle) {
super(headWriteCellStyle, contentWriteCellStyle);
}
}
- 自定义表头和内容的样式
public static WriteCellStyle getHeadStyle() {
// 头的样式
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
// 下边是具体实现
}
public static WriteCellStyle getContentStyle() {
// 内容的样式
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
// 下边是具体实现
}
- 使用自定义的样式进行构建拦截器,最后输出表格的时候,使用registerWriteHandler,将自定义的拦截器注册上去。
CustomHandler customHandler = new CustomHandler(headWriteCellStyle, contentWriteCellStyle);
write(response.getOutputStream(), Customer.class)
.registerWriteHandler(customHandler)
.sheet("信息").doWrite(customerList);
- 使用LongestMatchColumnWidthStyleStrategy 进行列宽自适应设置,同样进行注册
write(response.getOutputStream(), Customer.class)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrate
.registerWriteHandler(customHandler)
.sheet("信息").doWrite(customerList);
效果展示
2.修改自定义拦截器,进行更灵活的内容设置
最后发现,无法针对某一列进行其他样式的设置,例如之前说的“表格宽度自适应,并且如果某一列数据量太大,需要固定该列的宽,并自动换行”。
经过翻阅资料,发现拦截器父类中的有一个方法很特殊setContentCellStyle 。
该方法的入参有个: Head head,如果能从head中拿到列名,那么就可以针对某一列的内容进行其他的样式设置。
于是重写setContentCellStyle,其中head.getFieldName()可以拿到列名。
@Override
protected void setContentCellStyle(Cell cell, Head head, Integer relativeRowIndex) {
if (head.getFieldName().endsWith(RECORD)) {
// 如果该列的名字以 Record 结尾,则进行单独的样式设置
Workbook wb = cell.getSheet().getWorkbook();
CellStyle cellStyle = wb.createCellStyle();
// 左对齐,垂直居中对齐
cellStyle.setAlignment(HorizontalAlignment.LEFT);
cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 边框线
cellStyle.setBorderLeft(BorderStyle.THIN);
cellStyle.setBorderRight(BorderStyle.THIN);
cellStyle.setBorderBottom(BorderStyle.THIN);
cellStyle.setBorderTop(BorderStyle.THIN);
// 自动换行
cellStyle.setWrapText(true);
cell.setCellStyle(cellStyle);
} else {
super.setContentCellStyle(cell, head, relativeRowIndex);
}
}
3、列宽固定设置
由于列宽的设置属于 sheet 层次,之前设置的都是 cell 单元格层次。因此继续查找父类拦截器的方法。
发现 beforeCellCreate,该方法的入参有:writeSheetHolder。所以如果能获得 sheet对象,那么就能对列宽设置。
重写 beforeCellCreate
@Override
public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {
Sheet sheet = writeSheetHolder.getSheet();
// 设置固定列宽,在第 11+1=12 列(从0开始)设置宽度固定
sheet.setColumnWidth(11, 40 * 256);
super.beforeCellCreate(writeSheetHolder, writeTableHolder, row, head, columnIndex, relativeRowIndex, isHead);
}
最终效果展示
4.异常处理,不下载表格
如果发生异常,接口仍然返回200,表格也会下载成功,表格里边的数据就是异常信息的打印,这显然不符合需求。
所以进行异常捕捉,如果发生异常,响应流重置,自定义返回状态和信息。
然后前端就可以进行自定义的页面展示。
代码示例:
@PostMapping("download")
public void download(HttpServletResponse response, @RequestBody List<Customer> customerList) {
try {
// 表格下载代码逻辑
} catch (Exception e) {
response.reset();
response.setStatus(500);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
Map<String, String> map = new HashMap<String, String>();
map.put("status", "failure");
map.put("message", "下载文件失败" + e.getMessage());
response.getWriter().println(JSON.toJSONString(map));
}
}
效果展示
遗留问题
- easyExcel的列宽自适应,针对于中文效果并不好。
官方解释
/**
* 自动列宽(不太精确)
* <p>
* 这个目前不是很好用,比如有数字就会导致换行。而且长度也不是刚好和实际长度一致。 所以需要精确到刚好列宽的慎用。 当然也可以自己参照
* {@link LongestMatchColumnWidthStyleStrategy}重新实现.
* <p>
* poi 自带{@link SXSSFSheet#autoSizeColumn(int)} 对中文支持也不太好。目前没找到很好的算法。 有的话可以推荐下。
*
* <p>
* 1. 创建excel对应的实体对象 参照{@link LongestMatchColumnWidthData}
* <p>
* 2. 注册策略{@link LongestMatchColumnWidthStyleStrategy}
* <p>
* 3. 直接写即可
*/
@Test
public void longestMatchColumnWidthWrite() {
String fileName =
TestFileUtil.getPath() + "longestMatchColumnWidthWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(fileName, LongestMatchColumnWidthData.class)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).sheet("模板").doWrite(dataLong());
}
- 有些特殊字符会被换行显示
请求参数
[
{
"number": "1",
"customerNo": "okvy5sy1qcdknCkby3WP9e-mwRHk",
"customerName": "客户姓名",
"customerDescription": "客户概述",
"customerPhone": "12345678910",
"consultantName": "咨询人姓名",
"consultantPhone": "12345678910",
"customerLevel": "客户等级",
"agentName": "销售人员名字",
"noTrace": "3",
"lastTraceDate": "20169-06-25",
"communicationRecord": "沟通记录沟通\n记录沟通记录沟通记录沟通记录沟通记录沟通记录沟通记录沟通记录\n沟通记录沟通记录沟通记录沟通记录沟通记录沟通记录",
"status": "未联系"
},
{
"number": "1",
"customerNo": "okvy5sy1qcdknCkby3WP9e-mwRHk",
"customerName": "客户姓名",
"customerDescription": "客户概述",
"customerPhone": "12345678910",
"consultantName": "咨询人姓名",
"consultantPhone": "12345678910",
"customerLevel": "客户等级",
"agentName": "销售人员名字",
"noTrace": "3",
"lastTraceDate": "20169-06-25",
"communicationRecord": "沟通记录沟通记录沟通记录沟通记录沟通记录沟通记录沟通记录沟通记录沟通记录沟通记录沟通记录沟通记录沟通记录沟通记录沟通记录",
"status": "未联系"
}
]