前言
记录工作过程中用到excel导出的学习过程。
一、引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
二、使用步骤
首先easyExcel中有两种方式将数据写入excel文件。分别是写excel和填充excel。
- 写excel主要是实现数据列表的循环写入,不支持太过复杂的excel表格格式。
- 填充excel则是弥补了这方面的短板,通过读取你自己指定的模板来填充数据。
用大白话来讲就是,如果你只是需要每列有一个简单的表头,然后将列表数据填入excel,最后导出。可以使用
写excel方式。如果你需要有复杂的表头信息,则选择
填充excel方式。
1.写excel方式
- 创建对应的实体类(要导出的列表中的元素类型)。下面举个简单的例子
@Data
@EqualsAndHashCode
public class StorageIndicationVO {
@ExcelProperty(value = "钢种", index = 1)
String steelType;
@ExcelProperty(value = "规格", index = 2)
String cargoSpecification;
@ExcelProperty(value = "数量", index = 3)
Integer amount;
@ExcelProperty(value = "重量", index = 4)
BigDecimal weight;
@ExcelIgnore
String consignor;
@ExcelIgnore
String variety;
@ExcelProperty(value = "序号", index = 0)
Integer index;
}
其中的注解如下:
- @ExcelProperty表示该属性是excel表格中的一列,value值代表该列的表头名,index值表示该列是excel表格中的第几列(列从0开始)
- @ExcelIgnore表示该属性不属于excel表格中的一列。即导出数据后,该属性的值不会导出到excel文件中。
对应excel格式为:
注:如果不需要表头的话,可以不需要设定对应实体类模板,直接导出即可。
-
写excel的两种方式:
-
一次性写入:(要输入的数据格式是相同的)
public void exportStorageInfoGroupByConsignor(List<StorageIndicationVO> storageInfoGroupByConsignor, HttpServletResponse response){ // 设置响应头信息 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"); // 新建一个excel,写入相应的数据,最后向请求方响应 EasyExcel.write(response.getOutputStream()) // 指定输出的位置,即前端请求的浏览器 .head(StorageIndicationVO.class) // 指定表头模板 .sheet("表单名") // 指定工作表名称(一个excel有多个工作表,这里默认第一个) .doWrite(storageInfoGroupByConsignor); // 进行写入操作,storageInfoGroupByConsignor是列表数据 }
对应的结果:
-
分多次写入:(要输入的数据格式有不相同的,比如说在最后一行需要填入总计数量)
public void exportStorageInfoGroupByConsignor(List<StorageIndicationVO> storageInfoGroupByConsignor, HttpServletResponse response){ // 统计总数量 Integer totalAmount = storageInfoGroupByConsignor.stream().map(StorageIndicationVO::getAmount).filter(Objects::nonNull).reduce(0, Integer::sum); // 设置响应头信息 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"); // 新建一个excel,指定响应的对象(即输出位置) ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build(); // 新建工作表sheet WriteSheet writeSheet = EasyExcel.writerSheet().head(StorageIndicationVO.class).sheetName("库存表").build(); List<List<String>> head = new ArrayList<>(); List<String> head1 = new ArrayList<>(); head1.add(null); head1.add(null); head1.add("总数量:"); head1.add(totalAmount); head.add(head1); // 第一次插入数据 excelWriter.write(head, writeSheet); // head是第一次要插入的数据,writeSheet是指定的工作表 AtomicReference<Integer> i = new AtomicReference<>(1); storageInfoGroupByConsignor = storageInfoGroupByConsignor.stream().peek(c -> c.setIndex(i.getAndSet(i.get() + 1))).collect(Collectors.toList()); // 第二次插入数据 excelWriter.write(storageInfoGroupByConsignor, writeSheet); // storageInfoGroupByConsignor是第二次要插入的数据,writeSheet是同一个工作表 // 关闭excelWriter。这里必须关闭,不然导出时文件为空,显示台报excelWriter不能关闭警告 excelWriter.finish(); }
对应的结果:
-
-
常见的设置:
-
复杂头的写入:
@Getter @Setter @EqualsAndHashCode public class ComplexHeadData { @ExcelProperty({"主标题", "字符串标题"}) private String string; @ExcelProperty({"主标题", "日期标题"}) private Date date; @ExcelProperty({"主标题", "数字标题"}) private Double doubleData; }
相应的excel:
-
写入多个Sheet
try { // 设置响应头信息 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"); // 新建一个excel,指定响应的对象(即输出位置) ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build(); // 新建工作表sheet WriteSheet writeSheet = EasyExcel.writerSheet().head(StorageIndicationVO.class).build(); // 填充序号 AtomicReference<Integer> i = new AtomicReference<>(1); storageInfoGroupByConsignor = storageInfoGroupByConsignor.stream().peek(c -> c.setIndex(i.getAndSet(i.get() + 1))).collect(Collectors.toList()); // 向不同的Sheet中插入数据,这里为了方便,没有修改插入的数据,事实上这里的数据列表是可以不一样的 for (int num = 0; num < 2; num++) { writeSheet.setSheetName("Sheet" + num); // 为每一个工作表取不同的名字,多个工作表的名字必须不同! excelWriter.write(storageInfoGroupByConsignor, writeSheet); // 向不同的工作表中写入相同的数据 } // 向最后一个工作表中的末尾新增一行数据,用来与前面的工作表做区分 List<List<String>> addition = ListUtils.newArrayList(); List<String> addition1 = ListUtils.newArrayList("Sheet2,新增一行用来与Sheet1区分"); addition.add(addition1); excelWriter.write(addition, writeSheet); // 关闭excelWriter。这里必须关闭,不然导出时文件为空,显示台报excelWriter不能关闭警告 excelWriter.finish(); } catch (IOException e) { throw new RuntimeException(e); }
对应的excel:
- 合并单元格
@Getter @Setter @EqualsAndHashCode // 将第6-7行的2-3列合并成一个单元格 // @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2) public class DemoMergeData { // 这一列 每隔2行 合并单元格 @ContentLoopMerge(eachRow = 2) @ExcelProperty("字符串标题") private String string; @ExcelProperty("日期标题") private Date date; @ExcelProperty("数字标题") private Double doubleData; }
对应excel:
- 图片导出
-
注意:写excel的方式有个最大的缺陷就是无法在表头的上边添加数据,即表头的上面永远是不变的数据。例子如下:
上述图片中红框部分是个变量,我需要根据前端传来的值来填入。但用写excel的方式是做不到的。写excel方式默认表头部分放在最上面。也就是说,
厦门同金(江苏优特钢)
这几个字顶多排在列表数据中的第一个,而不可能排在表头的上面。
2.填充excel方式(推荐方式
)
好处:使用这种方式基本不需要使用代码去调试表格的复杂样式。所有的样式均可以直接在模板中设定,然后仅将对应的值填充到相应的位置。
-
创建xlsx模板
填充excel分单个元素(一个单元格)、 单列表对象(一行或多行数据)、多列表对象(一行或多行数据) -
填充方式:
-
对于单个元素(一个单元格):新建一个Map<String, Object>对象,key为模板中对应的变量,如:title。value为你想要插入到相应位置的真正的值。
// HelloWorld是当前类,这里是获取exportTemplate3.xlsx文件所在的路径 String filePath = Objects.requireNonNull(HelloWorld.class.getResource("/exportTemplate3.xlsx")).getPath(); // 新建excel,设置响应对象(请求的浏览器)和对应的excel模板 ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).withTemplate(filePath).build(); // 新建工作表 WriteSheet writeSheet = EasyExcel.writerSheet().build(); // 设置表头和表尾信息 Map<String, Object> map = new HashMap<>(); map.put("title", "测试案例"); map.put("totalAmount", totalAmount); map.put("totalWeight", totalWeight); // 填充表头和结尾 excelWriter.fill(map, writeSheet);
-
对于单个列表对象(一行或多行数据):可以直接填充到xlsx文件中
// HelloWorld是当前类,这里是获取exportTemplate3.xlsx文件所在的路径 String filePath = Objects.requireNonNull(HelloWorld.class.getResource("/exportTemplate3.xlsx")).getPath(); // 新建excel,设置响应对象(请求的浏览器)和对应的excel模板 ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).withTemplate(filePath).build(); // 新建工作表 WriteSheet writeSheet = EasyExcel.writerSheet().build(); // 填充列表,这里的inStorageInfoVOS是一个对象列表List<相应的类名> excelWriter.fill(inStorageInfoVOS, writeSheet);
用这种方式填充时,因为每一行数据对应的key都是一样的,所以需要在前面加一个“.”来表示模板中这一部分是用来填充列表的。如下图:
-
对于多列表对象(有时单个列表对象中缺少了某个属性时,就可以使用这种):需要为每个列表对象取别名,用来在文件模板中区分不同列表的key。毕竟不同列表中的对象属性名是有可能重复的。
// 新建excel,设置响应对象(请求的浏览器)和对应的excel模板 String filePath = Objects.requireNonNull(HelloWorld.class.getResource("/exportTemplate3.xlsx")).getPath(); ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).withTemplate(filePath).build(); // 新建工作表 WriteSheet writeSheet = EasyExcel.writerSheet().build(); // 设置列表中每写入一行,都会通过新增一行来写入。不管当前行是否有预留的空间 FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build(); // 填充列表 // 此处为列表对象inStorageInfoVOS取了一个别名data1 excelWriter.fill(new FillWrapper("data1", inStorageInfoVOS), fillConfig, writeSheet); // 通过 List<Map<String, Object>> 设置自定义列表数据,而不通过持久层对象 List<Map<String, Object>> tempLists = ListUtils.newArrayList(); Map<String, Object> temp; for (int index = 1; index <= inStorageInfoVOS.size(); index++) { temp = new HashMap<>(); // 为自定义列表数据中添加一个key为index的属性 temp.put("index", String.valueOf(index)); tempLists.add(temp); } // 此处为列表对象tempLists取了一个别名data2 excelWriter.fill(new FillWrapper("data2", tempLists), writeSheet); // 关闭excelWriter。不关闭的话,缓存中的数据不会写入文件,导致文件内容为空 excelWriter.finish();
使用这个方式来填充时,因为有多个列表,所以需要在原有的基础上添加上各自的别名。其中,橙框部分是data1列表对象填充的位置,红框部分是data2列表填充的部分。
-
最后附上我这部分案例的全部代码,让读者能够理解的更清楚。
@PostMapping("/exportInStorageInfoGroupByConsignor")
public void exportInStorageInfoGroupByConsignor(@RequestBody List<InStorageInfoVO> inStorageInfoVOS, HttpServletResponse response) {
Integer totalAmount = inStorageInfoVOS.stream().map(InStorageInfoVO::getAmount).filter(Objects::nonNull).reduce(0, Integer::sum);
BigDecimal totalWeight = inStorageInfoVOS.stream().map(InStorageInfoVO::getWeight).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
try {
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");
// 新建excel,设置响应对象(请求的浏览器)和对应的excel模板
String filePath = Objects.requireNonNull(HelloWorld.class.getResource("/exportTemplate3.xlsx")).getPath();
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).withTemplate(filePath).build();
// 新建工作表
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 设置表头和表尾信息
Map<String, Object> map = new HashMap<>();
map.put("title", "测试案例");
map.put("totalAmount", totalAmount);
map.put("totalWeight", totalWeight);
// 填充表头和结尾
excelWriter.fill(map, writeSheet);
// 设置列表中每写入一行,都会通过新增一行来写入。不管当前行是否有预留的空间
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
// 填充列表
excelWriter.fill(new FillWrapper("data1", inStorageInfoVOS), fillConfig, writeSheet);
List<Map<String, Object>> tempLists = ListUtils.newArrayList();
Map<String, Object> temp;
for (int index = 1; index <= inStorageInfoVOS.size(); index++) {
temp = new HashMap<>();
temp.put("index", String.valueOf(index));
tempLists.add(temp);
}
excelWriter.fill(new FillWrapper("data2", tempLists), writeSheet);
// 关闭excelWriter。不关闭的话,缓存中的数据不会写入文件,导致文件内容为空
excelWriter.finish();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
结果显示:
总结
以上就是今天要记录的内容,本文仅仅简单介绍了easyExcel的使用,如果想了解更多可以前往easyExcel官网查看.