最近有个项目里面中有大量的Excel文档导入导出需求,数据量最多的文档有上百万条数据,之前的导入导出都是用apache的POI,于是这次也决定使用POI,结果导入一个四十多万的文档就GG了,内存溢出... 于是找到EasyExcel的文档,学习了一番,解决了大数据量导入导出的痛点。
由于项目中很多接口都需要用到导入导出,部分文档都是根据日期区分,部分文档是需要全表覆盖,于是抽出一个工具类,简化下重复代码,在此把实现过程记录一下。
数据量100W

测试了几次,读取完加保存到数据库总耗时都是在140秒左右
由于在业务中不涉及到大数据量的导出,最多只有10W+数据的导出,所以用的是最简单的写,测试二十万的数据量五十秒左右
官方文档:EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel
| 1 2 3 4 5 |
|
@ExcelProperty注解对应Excel文档中的表头,其中默认属性是value,对应文字,也有index属性,可以对应下标。converter属性是指定一个转换器,这个转换器中实现了把Excel内容转换成java对象(导入使用),Java对象转Excel内容(导出使用),我这里实现的是LocalDateTime和文本的转换。
@ExcelIgnoreUnannotated注解的意思就是在导入导出的时候忽略掉未加@ExcelProperty注解的字段
1 @Data2 @TableName("t_test_user")3 @ApiModel(value = "TestUserEntity对象", description = "测试表")4 @ExcelIgnoreUnannotated5 public class TestUserEntity implements Serializable {6 7 private static final long serialVersionUID = 1L;8 9 @TableId(value = "id", type = IdType.AUTO)
10 private Long id;
11
12 @ExcelProperty("用户名")
13 @ApiModelProperty("用户名")
14 @TableField("user_name")
15 private String userName;
16
17 @ExcelProperty("账号")
18 @ApiModelProperty("账号")
19 @TableField("account")
20 private String account;
21
22 @ExcelProperty("性别")
23 @ApiModelProperty("性别")
24 @TableField("sex")
25 private String sex;
26
27 @ExcelProperty(value = "注册时间", converter = StringToLocalDateTimeConverter.class)
28 @ApiModelProperty("注册时间")
29 @TableField("registered_time")
30 private LocalDateTime registeredTime;
31
32 @ApiModelProperty("数据日期")
33 @TableField("data_date")
34 private Integer dataDate;
35
36 @ApiModelProperty("创建人")
37 @TableField("create_user")
38 private String createUser;
39
40 @ApiModelProperty("创建时间")
41 @TableField("create_time")
42 private LocalDateTime createTime;
43 }
在这里实现导入导出的数据格式转换
1 /**2 * @author Tang3 * @describe easyExcel格式转换器4 * @date 2022年08月29日 09:41:035 */6 public class StringToLocalDateTimeConverter implements Converter{7 /**8 * 这里读的时候会调用9 */ 10 @Override 11 public LocalDateTime convertToJavaData(ReadConverterContext> context) { 12 String stringValue = context.getReadCellData().getStringValue(); 13 return StringUtils.isBlank(stringValue) ? null : DateUtil.stringToLocalDatetime(stringValue); 14 } 15 16 /** 17 * @describe 写的时候调用 18 * @Param context 19 * @return com.alibaba.excel.metadata.data.WriteCellData> 20 * @date 2022年11月17日 16:03:39 21 * @author Tang 22 */ 23 @Override 24 public WriteCellData> convertToExcelData(WriteConverterContext context) { 25 return new WriteCellData<>(DateUtil.localDateToDayString(context.getValue())); 26 } 27 28 }
由于项目中很多接口都有使用到导入导出,且持久层框架是Mybatis Plus,在此封装成通用的方法。
如果数据量不大,那么一行代码就可以解决了,直接用Mybatis Plus的批量插入:
EasyExcel.read(file.getInputStream(), TestUserEntity.class, new PageReadListener
PageReadListener是默认的监听器,在此监听器中传入一个Consumer接口的实现,由此来保存数据。它具体实现原理是从文件中分批次读取,然后在此监听器中实现保存到数据库,当然也可以重写监听器,定义自己想要实现的业务,如数据校验等。BATCH_COUNT参数是每次读取的数据条数,3.1.2的版本默认是100条,建议修改为3000。

导出也是一行代码:EasyExcel.write(response.getOutputStream(), clazz).sheet().doWrite(() -> testUserService.list());
数据量大的话用Mybatis Plus的批量插入还是会很慢,因为这个批量插入实际上还是一条条数据插入的,需要把所有数据拼接成insert into table(field1,field2) values(value1,value2),(value1,value2),(value,value2)...,配合数据库的rewriteBatchedStatements=true参数配置,可以实现快速批量插入,在下文中的114行调用实现。
1 /**2 * @author Tang3 * @describe EasyExcel工具类4 * @date 2022年11月02日 17:56:455 */6 public class EasyExcelUtil {7 8 /**9 * @describe 封装成批量插入的参数对象10 * @Param clazz11 * @Param dataList12 * @date 2022年11月17日 18:00:3113 * @author Tang14 */15 public static DynamicSqlDTO dynamicSql(Class> clazz, List> dataList) {16 //字段集合 key=数据库列名 value=实体类get方法17 Map getMethodMap = new LinkedHashMap<>();18 //获取所有字段19 Field[] declaredFields = clazz.getDeclaredFields();20 for (Field field : declaredFields) {21 field.setAccessible(true);22 //获取注解为TableField的字段23 TableField annotation = field.getAnnotation(TableField.class);24 if (annotation != null && annotation.exist()) {25 String column = annotation.value();26 Method getMethod = getGetMethod(clazz, field.getName());27 getMethodMap.put(column, getMethod);28 }29 }30 31 //value集合32 List> valueList = dataList.stream().map(v -> {33 List
1 /**2 * @author Tang3 * @describe 生成批量插入sqlDTO4 * @date 2022年11月02日 17:53:335 */6 @Data7 @Builder8 @AllArgsConstructor9 @NoArgsConstructor
10 public class DynamicSqlDTO {
11
12 //表名
13 private String tableName;
14
15 //列名集合
16 private List columnList;
17
18 //value集合
19 private List> valueList;
20 }
根据业务实现了两个方法,一个是批量插入,一个是全表覆盖删除
1 @Mapper2 public interface CustomSqlMapper {3 4 /**5 * @describe 执行动态批量插入语句6 * @Param dynamicSql7 * @date 2022年11月03日 09:59:228 * @author Tang9 */
10 void executeCustomSql(@Param("dto") DynamicSqlDTO dto);
11
12 /**
13 * @describe 快速清空表
14 * @Param tableName
15 * @date 2022年11月08日 17:47:45
16 * @author Tang
17 */
18 void truncateTable(@Param("tableName") String tableName);
19 }
1 2 34 5 insert into ${dto.tableName}6 18 19 207 `${item}`8 9 values 1011 ( 12 1713 #{value} 14 15 ) 1621 truncate table ${tableName} 22 23 24
1 @RestController2 @Api(value = "测试-测试", tags = "测试-测试")3 @RequestMapping("/test")4 public class TestUserController {5 6 @Resource7 private TestUserMapper testUserMapper;8 9 @PostMapping(value = "/import", produces = BaseConstant.REQUEST_HEADERS_CONTENT_TYPE)
10 @ApiOperation(value = "测试-导入(全表覆盖)", notes = "测试-导入(全表覆盖)")
11 public RR testImport(@RequestParam(value = "file") @ApiParam("上传文件") MultipartFile file) {
12 return RR.success(
13 EasyExcelUtil.importExcel(
14 file,
15 TestUserEntity.class
16 )
17 );
18 }
19
20 @PostMapping(value = "/import", produces = BaseConstant.REQUEST_HEADERS_CONTENT_TYPE)
21 @ApiOperation(value = "测试-导入(按日期覆盖)", notes = "测试-导入(按日期覆盖)")
22 public RR testImport(@RequestParam(value = "file") @ApiParam("上传文件") MultipartFile file, @ApiParam("日期 20110101") @RequestParam(value = "date") Integer date) {
23 return RR.success(
24 EasyExcelUtil.importExcel(
25 file,
26 date,
27 TestUserEntity::getDataDate,
28 TestUserEntity::setDataDate,
29 testUserMapper,
30 TestUserEntity.class
31 )
32 );
33 }
34
35 @PostMapping(value = "/export", produces = BaseConstant.REQUEST_HEADERS_CONTENT_TYPE)
36 @ApiOperation(value = "测试-导出", notes = "测试-导出")
37 public void testExport() {
38 EasyExcelUtil.exportExcel(
39 TestUserEntity.class,
40 testUserMapper.selectList(null)
41 );
42 }
43 }