本文以一个实战案例,详解关于该案例中Excel导入方法。本篇会用更加通俗的讲法让读到这篇博客的伙伴能够更轻松的理解导入方法的整体流程,以及涉及到的一些类,例如consumer
类的用法,更加偏向于理解而不是底层原理
I. 项目结构概述
本篇以导入历史问题为背景,进行项目结构的构造
- module层:服务实现层,调用dao层中实现的各种数据库操作方法实现服务功能
- controller层:定义控制器结构
HistoricalIssuesController
- service层:实现各种服务功能,定义
HistoricalIssuesService
和HistoricalIssuesServiceImpl
- controller层:定义控制器结构
- dao层:数据库操作实现层,定义了
TbIssuesDaoService
和ReviewProcessDaoService
来实现对应的数据库的CRUD
- dto层:数据传输对象层,定义
IssuesModel
作为接收excel内容的对象 - enums层:枚举类,定义
IssueTypeEnum
II. Excel导入具体实现
1. controller层
在HistoricalIssuesController
中现将接口定义出来
@RestController
@RequestMapping("/file/operation")
@Api(tags = "问题导入")
public class HistoricalIssuesController extends BaseController {
@Autowired
private HistoricalIssuesService historicalIssuesService;
@ApiOperation("导入历史问题清单")
@PostMapping("/import-historicalIssues")
public JsonResult<Void> importHistoricalIssues(@RequestParam("files")List<MultipartFile> files, @RequestParam("serialNo") String serialNo throws IOException {
historicalIssuesService.importHistoricalIssues(files, serialNo);
return okReturn(ReturnCodeEnum.S0002);
}
}
其中,MultipartfFile
类用来接收excel文件,文件可用postman
在接口中上传。其中较为常用的方法是MultipartFile.getInputStream()
,用来获取输入文件的流,流可以理解为文件进入程序之后的一种存在形式,可以调用InputStream
中的read()
方法读出流中的内容
2. dao层
dao层中实现数据库操作的逻辑,在本例中,TbIssuesDaoService
用来操作存储问题的数据库表,Issues
对象包含一个reviewProcessId
字段,需要用ReviewProcessDaoService
来获取。而每一个reviewProcessId
对应一个serialNo
,于是有getBySerialNo()
方法
@Service
public class ReviewProcessDaoServiceImpl extends ServiceImpl<ReviewProcessMapper, ReviewProcessPo> implements ReviewProcessDaoService {
@Override
public ReviewProcessPo getBySerialNo(String serialNo) {
if (StringUtils.isEmpty(serialNo)) {
return null;
}
LambdQueryWrapper<ReviewProcessPo> wrapper = new LambdQueryWrapper<>();
wrapper.eq(ReviewProcessPo::getSerialNo, serialNo);
return this.getOne(wrapper) == null ? null : this.getOne(wrapper);
3. dto层
数据传输对象,用来存储excel导入的数据
@Data
public class IssuesModel {
@ExcelProperty(index = 0)
private String problemDescription;
@ExcelProperty(index = 1)
private String historicalIssueType;
@ExcelProperty(index = 2)
private String deadline;
@ExcelProperty(index = 3)
private String solution;
@ExcelProperty(index = 4)
private String principal;
@ExcelProperty(index = 5)
private StringprincipalName;
@ExcelProperty(index = 6)
private String serilaNo;
}
4. enum层以及converter方法
IssueTypeEnum
定义了问题类型的枚举类,converter方法写在HistoricalIssuesServiceImpl
中
@Getter
public enum IssueTypeEnum {
A(1, "A"),
B(2, "B"),
C(3, "C"),
D(4, "D"),
;
@EnumValue
private Integer value;
private String message;
IssueTypeEnum(Integer value, String message) {
this.value = value;
this.message = message;
}
}
// converter写在HistoricalIssueaServiceImpl中
private IssueTypeEnum converter(String historicalIssuesType) {
if (StringUtils.equals(historicalIssueType, "a类")) {
return IssueTypeEnum.A;
} else if (StringUtils.equals(historicalIssueType, "b类")) {
return IssueTypeEnum.B;
} else if (StringUtils.equals(historicalIssueType, "c类")) {
return IssueTypeEnum.C;
} else {
return IssueTypeEnum.D;
}
}
5. service层
service层实现HistoricalIssuesServiceImpl
,首先是方法函数dataProcess
:
public Consumer<List<IssuesModel>> dataProcess(String serialNo)
Consumer<List<IssuesModel>> consumer = models -> {
// 根据业务逻辑进行代码书写
if (CollectionUtils.isEmpty(models)) {
return;
}
ReviewProcessPo reviewProcess = reviewProcessDaoService.getBySerialNo(serialNo);
String reviewProcessId = reviewProcess.getId().toString();
List<TbIssue> issues = models.stream().map(model -> {
TbIssue issue = new TbIssue();
......
}).collect(Collectors.toList());
};
return consumer;
}
这部分说明一下Consumer
类的用法。他可以理解为一个消费者,在本例中,Consumer<List<IssuesModel>>
的消费对象即为List<IssuesModel>
,意思就是需要投入一个List<IssuesModel>
供这个consumer使用。从方法的第一行开始,models
就代表了投入的List
对象,接下来的代码就是对投入对象进行的处理,例如本例中,首先判断了数组是否为空,为空则直接返回。
接下来是代码的完整部分:
@Service
@Transactional(rollbackFor = Exception.class)
public HistoricalIssuesServiceImpl implements HistoricalIssuesService {
@Autowired
private TbIssueDaoService tbIssueDaoService;
@Autowired
private ReviewProcessDaoService reviewProcessDaoService;
@Override
public void importHistoricalIssues(List<MultipartFile> files, String serialNo) throws OPException {
InputStream inputStream = files.get(0).getInputStream();
Consumer<List<IssuesModel>> consumer = dataProcess(serialNo);
MyAnaLysisEventListener<IssuesModel> readListener = ExcelReaderUtils.getReadListener(consumer);
ExcelReader reader = EasyExcel.read(inputStream, IssueModel.class, readListener).build();
ReadSheet sheet = EasyExcel.readSheet(0).build();
reader.read(sheet);
reader.finish();
}
public Consumer<List<IssuesModel>> dataProcess(String serialNo)
Consumer<List<IssuesModel>> consumer = models -> {
// 根据业务逻辑进行代码书写
if (CollectionUtils.isEmpty(models)) {
return;
}
ReviewProcessPo reviewProcess = reviewProcessDaoService.getBySerialNo(serialNo);
String reviewProcessId = reviewProcess.getId().toString();
List<TbIssue> issues = models.stream().map(model -> {
TbIssue issue = new TbIssue();
......
}).collect(Collectors.toList());
};
return consumer;
}
private IssueTypeEnum converter(String historicalIssuesType) {
if (StringUtils.equals(historicalIssueType, "a类")) {
return IssueTypeEnum.A;
} else if (StringUtils.equals(historicalIssueType, "b类")) {
return IssueTypeEnum.B;
} else if (StringUtils.equals(historicalIssueType, "c类")) {
return IssueTypeEnum.C;
} else {
return IssueTypeEnum.D;
}
}
}
其中MyAnalysisEventListener
较为复杂,后面细说。较为容易理解的是EasyExcel
中的方法,EasyExcel为Excel
处理接口类,EasyExcel.read()
方法的三个参数分别为输入数据的流、承接流的类型类和回调监听器。其中回调监听器MyAnaLysisEventListener
继承了AnaLysisEventListener
对输入数据进行了处理,实现如下:
public calss ExcellReaderUtils {
public static<T> MyAnalysisEventListener<T> getReadListener(Consumer<List<T>> consumer, int threshold) {
return new MyAnalysisEventListener<T>(consumer, threshold):
}
public static<T> MyAnalysisEventListener<T> getReadListener(Consumer<List<T>> consumer) {
return getReadListener(consumer, 2000);
@Getter
public static class MyAnalysisEventListener<T> extends AnalysisEventListener<T> {
private final List<T> dataList = new LinkedList<>();
private AtomicInteger count = new AtomicInteger(0);
private Consumer<List<T>> consumer;
private int threshole;
public MyAnalysisEventListener(Consumer<List<T>> consumer, int shreshold) {
this.consumer = consumer;
this.threshold = threshole;
}
@Override
public void invoke(T data, AnalysisContext analysisContext) {
dataList.add(data);
// 到达阈值就处理一次存储的数据
if (dataList.size() >= threshold) {
consumer.accept(dataList);
dataList.clear();
}
count.incrementAndGet();
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
if (dataList.size() > 0) {
consumer.accept(dataList);
}
}
}
}
其中重写了两个方法,invoke()
方法定义了数据处理的方式,consumer
的accept()
方法表示向其中投入一个数据;doAfterAllAnalysed()
方法表示在处理完所有数据之后再进行的操作。至此,这就是本篇所有代码的讲解。
以上就是本文的全部内容,如果有疑问或者纰漏欢迎大家评论以及交流!