描述

使用Java代码封装的一个Excel批量导入框架,使用EasyExcel完成excel文件方面的交互,重在整合功能,仅需少量代码就可以在自己的项目中快速引入该功能。可以流式分批读取Excel中的数据,避免OOM,使用多线程提高处理速度,异步线程处理便于前端查看导入进度与处理结果。

框架代码采取了设计模式中的策略模式,只需要实现其中的策略类(针对自己的业务来处理每行数据),就可以快速使用导入功能,并且给出了前端的配套代码示例(vue)。

demo项目地址:https://github.com/destinyol/excel-multi-import-progress

(demo项目不包括数据库交互部分,所以无法运行,只是演示代码如何使用)

框架项目文件结构:

image-20250209232807807

ps:修改ExcelImportMainTool 中的 BATCH_COUNT 可自定义每次从excel中读取多少行数据,默认1000行

效果展示

测试上传效果

可调参数

ExcelImportMainTool类中有一些可以根据实际情况自行调整的参数

1
2
3
4
5
public static final int BATCH_COUNT = 1000; // 批量处理,每批从excel中读取并处理的数据行数,可根据情况自行修改

public int BATCH_INSERT_COUNT = 200; // 使用批量插入时的每批次处理数量(传入handleMultiDataAndSave函数的数组大小),可根据情况自行修改,可动态设置

public static final boolean PROCESS_SAVE_RESULT_LIST = true; // redis保存进度条时是否保存报错结果数组ExcelResDto,true保存,false不保存

使用方法

1、继承EasyExcelReadDataAbstract抽象类并实现其中EasyExcelReadData接口的方法,该子类是对应EasyExcel导入的实体类,用来存储单行数据,需要实现其中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface EasyExcelReadData {

/**
* 去掉每个字段首尾空格(若不需要则方法体为空即可)
* (处理过程中每行会调用一次)
*/
public void trimAllFields();

/**
* 为了排除表格空行(建议实现该方法,空行比较容易出现)
* 检查该行中的每个属性是否都为null或空字符串,如果是,则返回true;否则返回false
* (处理过程中每行会调用一次)
*/
public boolean dataIsAllEmpty();

}

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Data
public class CustomerImportDto extends EasyExcelReadDataAbstract {

@ExcelProperty(value = "客户名称")
private String name;
@ExcelProperty(value = "手机号")
private String phone;
// ***********************
// 省略各种字段


@Override
public boolean dataIsAllEmpty(){
return (name == null || name.isEmpty()) &&
(phone == null || phone.isEmpty());
}

@Override
public void trimAllFields() {
if (name != null) name = name.trim();
if (phone != null) phone = phone.trim();
}
}

2、实现DataDealHandler接口及对应方法,该实现类是用于自定义的数据处理与保存,便于衔接不同的业务场景

(要用批量插入就实现handleMultiDataAndSave方法,要用逐条插入就实现handleOneDataAndSave

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface DataDealHandler {

/**
* 返回模板表头行数(根据对应模板配置实现该方法,例如表头三行:return 3;)
* @return
*/
public Integer getHeadRows();

/**
* 处理单条数据(比如去重,特殊判断,或者其他自定义业务)
* 并自行保存该条数据到数据库
* @param importData EasyExcelReadDataAbstract的子类
* @return 返回结果内容(显示给前端,表格行号+错误内容)。若返回类为null,则该条数据导入成功,不用显示;若不为null,则将其放入错误结果集中
*/
@Transactional(rollbackFor = Exception.class)
default public ExcelResDto handleOneDataAndSave(EasyExcelReadData importData){
throw new RuntimeException("未实现逐条插入数据处理函数handleOneDataAndSave");
}

/**
* 批量处理数据(比如去重,特殊判断,或者其他自定义业务)
* 并自行保存数据到数据库,若抛出错误则这一批全部回滚
* @param importDataList EasyExcelReadDataAbstract的子类list
* @return 返回结果内容数组(显示给前端,表格行号+错误内容)。若数组长度为0,则全部成功;如果长度大于0,则将其放入错误结果集中
*/
@Transactional(rollbackFor = Exception.class)
default public List<ExcelResDto> handleMultiDataAndSave(List<EasyExcelReadData> importDataList){
throw new RuntimeException("未实现批量插入数据处理函数handleMultiDataAndSave");
}

}

3、直接调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// --------------------------------------导入任务启动的地方---------------------------------
// 构造DataDealHandler的实现类的实例对象
DataDealHandlerImpl handler = new DataDealHandlerImpl(xxxxx,xxxxx自定义构造);
// 启动导入主函数并异步执行,返回redis中保存的该进度的processKey,可以直接返回给前端用于查询进度
// (逐条插入,需实现对应方法)
String processKey = ExcelImportMainTool.buildImport(
EasyExcelReadDataAbstract的子类.class,
handler,
redisTemplate // 进度保存在redis中,所以需要自行注入redisTemplate
).runAsyncSingle(multipartFile);

// (批量插入,需实现对应方法)
String processKey = ExcelImportMainTool.buildImport(
EasyExcelReadDataAbstract的子类.class,
handler,
redisTemplate // 进度保存在redis中,所以需要自行注入redisTemplate
).setBatchInsertCount(250) // 设置批量插入每批传入`DataDealHandler`中的数据量,不设置则默认200行
.runAsyncMulti(multipartFile);
1
2
3
// ---------------------------------------轮询或其他手段,获取任务进度-----------------------
// 获取进度(静态方法,需传入redisTemplate)
ImportProgress progress = ExcelImportMainTool.getProgress(redisTemplate, processKey);

至此整个功能就跑起来了

使用示例

数据实体类实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* 示例:easyExcel导入的数据实体类
* ( EasyExcelReadDataAbstract的子类 )
*/
@Data
public class CustomerImportDto extends EasyExcelReadDataAbstract {

@ExcelProperty(value = "销售")
private String saleUserName;

@ExcelProperty(value = "客户名称")
private String name;

@ExcelProperty(value = "联系人")
private String contacts;

@ExcelProperty(value = "联系电话")
private String phone;

@ExcelProperty(value = "统一社会信用代码")
private String sucCode;

@ExcelProperty(value = "法人")
private String legalPerson;

@ExcelProperty(value = "注册资本(单位万元)")
private String registerMoney;

@ExcelProperty(value = "客户地址")
private String address;

/**
* 检查类中的每个属性是否为null或空字符串,如果是,则返回true;否则返回false
* @return
*/
@Override
public boolean dataIsAllEmpty(){
return (saleUserName == null || saleUserName.isEmpty()) &&
(name == null || name.isEmpty()) &&
(contacts == null || contacts.isEmpty()) &&
(phone == null || phone.isEmpty()) &&
(sucCode == null || sucCode.isEmpty()) &&
(legalPerson == null || legalPerson.isEmpty()) &&
(registerMoney == null || registerMoney.isEmpty()) &&
(address == null || address.isEmpty());
}

@Override
public void trimAllFields() {
if (saleUserName != null){
saleUserName = saleUserName.replaceAll("\\s+", "");
}
if (name != null) name = name.trim();
if (contacts != null) contacts = contacts.trim();
if (phone != null) phone = phone.trim();
if (sucCode != null) sucCode = sucCode.trim();
if (legalPerson != null) legalPerson = legalPerson.trim();
if (registerMoney != null) registerMoney = registerMoney.trim();
if (address != null) address = address.trim();
}
}

策略类实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
* 示例:客户批量导入handler
* ( DataDealHandler实现类 )
*/
public class CustomerImportDataHandler implements DataDealHandler {

private UserMapper userMapper;
private TdCustomerMapper tdCustomerMapper;

public CustomerImportDataHandler(UserMapper userMapper, TdCustomerMapper tdCustomerMapper) {
this.userMapper = userMapper;
this.tdCustomerMapper = tdCustomerMapper;
}

@Override
public Integer getHeadRows() {
return 2;
}

@Override
@Transactional(rollbackFor = Exception.class)
public ExcelResDto handleOneDataAndSave(EasyExcelReadData importData) {
CustomerImportDto customerImportDto = (CustomerImportDto) importData;
ExcelResDto resDto = null;
TdUser user = userMapper.queryOneByUserName(customerImportDto.getSaleUserName());
if (user == null){
resDto = ExcelResDto.build(customerImportDto,"该姓名用户不存在:("+customerImportDto.getSaleUserName()+") ,对应客户名:"+customerImportDto.getName());
return resDto;
}

TdCustomer step = new TdCustomer();
step.setName(customerImportDto.getName());
step.setContacts(customerImportDto.getContacts());
step.setAddress(customerImportDto.getAddress());
step.setPhone(customerImportDto.getPhone());
step.setCreateBy(user.getUserId());
step.setLegalPerson(customerImportDto.getLegalPerson());
step.setSucCode(customerImportDto.getSucCode());
step.setRegisterMoney(customerImportDto.getRegisterMoney());

if (isBlank(step.getName())){
resDto = ExcelResDto.build(customerImportDto,"客户名不能为空");
return resDto;
}

TdCustomer customerByName = tdCustomerMapper.getCustomerByName(customerImportDto.getName());
if (customerByName != null) {
resDto = ExcelResDto.build(customerImportDto,"客户名重复:"+customerImportDto.getName());
return resDto;
}

if (isBlank(step.getContacts())){
resDto = ExcelResDto.build(customerImportDto,"客户联系人不能为空,客户名:"+customerImportDto.getName());
return resDto;
}
if (isBlank(step.getPhone())){
resDto = ExcelResDto.build(customerImportDto,"客户联系方式不能为空,客户名:"+customerImportDto.getName());
return resDto;
}

boolean save = tdCustomerMapper.insert(step) != 0;
if (!save){
resDto = ExcelResDto.build(customerImportDto,"客户添加失败,客户名:"+customerImportDto.getName());
}

return resDto;
}

@Override
@Transactional(rollbackFor = Exception.class)
public List<ExcelResDto> handleMultiDataAndSave(List<EasyExcelReadData> importDataList) {
// 结果数组
List<ExcelResDto> res = new ArrayList<>();

List<CustomerImportDto> customerImportDtoList = new ArrayList<>();
for (EasyExcelReadData easyExcelReadData : importDataList) {
customerImportDtoList.add((CustomerImportDto)easyExcelReadData);
}

List<TdCustomer> tdCustomerList = new ArrayList<>();
for (CustomerImportDto customerImportDto : customerImportDtoList) {
TdCustomer step = new TdCustomer();
step.setName(customerImportDto.getName());
step.setContacts(customerImportDto.getContacts());
step.setAddress(customerImportDto.getAddress());
step.setPhone(customerImportDto.getPhone());
step.setLegalPerson(customerImportDto.getLegalPerson());
step.setSucCode(customerImportDto.getSucCode());
step.setRegisterMoney(customerImportDto.getRegisterMoney());
}

//==========================================================
// 省略各种业务检查,判断重复之类的
//==========================================================

Boolean insetCheck = tdCustomerMapper.saveBatch(tdCustomerList);
if (!insetCheck){
for (CustomerImportDto customerImportDto : customerImportDtoList) {
res.add(ExcelResDto.build(customerImportDto,customerImportDto.getName()+"添加失败"));
}
}

return res;
}
}

接口示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* excel 导入客户
* @return
*/
@PostMapping("/importExcel")
public Response importExcel(MultipartFile file){
Response response = new Response();
response.ret(0,"成功了");
try {
CustomerImportDataHandler handler = new CustomerImportDataHandler(userMapper,tdCustomerMapper);
// 逐条插入
String processKey = ExcelImportMainTool.buildImport(CustomerImportDto.class, handler, redisTemplate).runAsyncSingle(file);
// 批量插入
// String processKey = ExcelImportMainTool.buildImport(CustomerImportDto.class, handler, redisTemplate).runAsyncMulti(file);
response.setData(processKey);
} catch (Exception e) {
e.printStackTrace();
response.ret(111000,"出错了");
}
return response;
}

/**
* 获取进度
* @return
*/
@GetMapping("/getProgress")
public Response getProgress(String key){
Response response = new Response();
response.ret(0,"成功了");
try {
ImportProgress progress = ExcelImportMainTool.getProgress(redisTemplate, key);
response.setData(progress);
} catch (Exception e) {
e.printStackTrace();
response.ret(111001,"出错了");
}
return response;
}