一、 前言
本篇是EasyExcel快速入门知识,讲解如何读取Excel文件,对Excel中错误信息如空字符、必填项为空、表格格式校验做到处理,并给出了实际项目中示例代码;为什么要使用easyexcel;原因是相比于poi,easyexcel更加轻量级,读取写入API方便,并且在工作中占用内存较小;
官方描述如下:
Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,能够原本一个3M的excel用POI sax依然需要100M左右内存降低到几M,并且再大的excel不会出现内存溢出,03版依赖POI的sax模式。在上层做了模型转换的封装,让使用者更加简单方便。
二、前端上传Excel文件
关于前端上传Excel文件分为两种情况:
(1)前后端分离情况下(前端使用Vue)实现文件上传。
该情况下直接借助elementUI提供的上传组件可以很好的实现上传文件,包括对上传文件前后均有回调函数,可以对各个时间节点进行操作,对文件处理非常便捷,我这里就不多赘述。
(2)前后端未分离情况下(springboot模板)实现文件上传。
该情况下可以使用input标签的type=“file”来实现文件选取,但是使用它主要有两点问题,相信大家在做项目时也会遇到:1、默认样式很丑,修改后点击无法触发文件选择的弹窗。2、如何取上传的file?直接获取input.value是fakePath路径(浏览器处于安全考虑修改为虚拟路径),因此不能通过此路径访问到文件,解析路径也比较麻烦。
(3)模板下载。
模板存在D盘img->download文件夹下,该路径通过nginx配置(8091为我的nginx地址),访问该路径实现文件下载,通过_blank实现不跳转页面。
下面代码为解决上述(2)(3)两个问题:
导入模板下载学生数据导入function downloadFile(){const url = "http://192.168.3.96:8091/download/学生数据导入模板.xls";// 使用 window.open() 方法发起 GET 请求,跳转到下载页面window.open(url, '_blank')}function showInput(){deleteTemp();//删除临时表$("#articleImageFile").click();}function showFileName(){var file = $("#articleImageFile")[0].files[0];if(file == undefined || file == null){$("#showFileName").text("")return;}$("#showFileName").text(name.name)showTips("文件上传成功,请查看导入数据后确认导入!");}
三、后端使用easyExcel解析文件
(1)准备与Excel文件数据相同属性的dto,需要注意的是每一列对应的值。从0开始,注意使用
注解。
public class ExcelWgsUserInfoFileDto extends BaseDto {@ExcelProperty(index = 0)private String index;@ExcelProperty(index = 1)private String id;@ExcelProperty(index = 2)private String name;//get、set方法}
(2)controller层接收文件(可以接收多个)
/** * 导入Excel信息 */@ApiOperation("导入Excel表")@RequestMapping(value = "/toexcel",method = RequestMethod.POST)public APIResponse toexcel(@RequestParam("file") MultipartFile file) throws Exception{return wgsUserInfoService.loadScoreInfo(file);}
(3)service层(最关键)
定义的变量解释:
BATCH_COUNT:集合最大缓存量,当到达最大缓存调用一次存方法。
temp:对调用了几次存函数进行计数,在判断表头、计算错误行数时使用。
isError:由于存方法中return只会跳出该方法,不会跳出整个read方法,使用它实现当出现错误后下一次1000行或者读取完成不再调用存方法。
该代码较长,我稍微做一下解释:其实使用easyExcel解析文件,最重要的是要弄清楚整个读取文件的流程是什么。在EasyExcel.read(参数1,参数2,参数3)中,最重要的是第三个参数读的监听器,该参数中重写的两个方法:
1、invoke()每读取一行调用一次,它的第一个参数ExcelWgsUserInfoDto就是我们最初定义的dto,是文件中一行的数据,做的操作为将该行数据加入到集合中,当到达集合缓冲区最大值1000时调用存数据的方法。
2、doAfterAllAnalysed()该方法是当Excel文件全部读取完成后会调用一次存数据的方法。
以上两个重写的方法主要作用是对Excel文件进行操作,拿取数据,这两个方法照着写即可,但是对于我们来说业务处理同样重要,在saveData中主要做的是业务处理(判断表格格式、存入数据库、是否有必填项为空等),存入数据库操作不多赘述,下面讲一下代码的大概思路:
进入该方法会对存入集合的数据进行遍历,取数据存入数据库,在存数据之前便是判断时间。首先进行的是表头格式的判断,根据Excel模板文件第几行为模板文字进行判断,由于我的是第二行,所以有if(temp<1&&i<2)的判断,对Excel每行必填项进行if判断,若为null,返回提示,行数根据temp和i计算得来。
Excel文件格式如下:
@Transactional@Overridepublic APIResponse loadScoreInfo(MultipartFile file) throws IOException {APIResponse result = new APIResponse();try {/** * 构建一个读的工作簿对象 * @param file * 第一个参数:文件输入流 * @param head * 第二个参数:文件中每一行对应的实体类 * @param ReadListener * 第三个参数:读的监听器,每读一行内容,都会调用一次该对象的invoke方法,在invoke可以操作使用读到的数据 */EasyExcel.read(file.getInputStream(), ExcelWgsUserInfoFileDto.class, new ReadListener() {/** * 单次缓存的数据量 */public static final int BATCH_COUNT = 1000;//判断执行了几次saveDataint temp = 0;boolean isError = false;/** *临时存储 */private List cacheDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);//导入数据库的明细表listprivate List wgsUserInfoTempList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);/** * 每第一行调一次的方法 * @param data * @param analysisContext */@Overridepublic void invoke(ExcelWgsUserInfoFileDto data, AnalysisContext analysisContext) {cacheDataList.add(data);if (cacheDataList.size() >= BATCH_COUNT && !isError) {saveData();temp++;// 存储完成清理 listcacheDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);}}/** * 读取完整个文档后调用一次 * @param analysisContext */@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {if(!isError){saveData();}temp = 0;}/** * 存储数据库 */private void saveData() {System.out.println("进入存储文件!!");wgsUserInfoTempList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);for (int i = 0; i < cacheDataList.size(); i++) {//取出每一行数据ExcelWgsUserInfoFileDto data = cacheDataList.get(i);//表头判断if ( i == 1 && !"学号".equals(cacheDataList.get(1).getId()) && !"姓名".equals(cacheDataList.get(1).getName()) &&!"性别".equals(cacheDataList.get(1).getGender()) && !"手机号码".equals(cacheDataList.get(1).getMobile()) &&!"身份证号".equals(cacheDataList.get(1).getIdCard()) && temp < 1 ) {System.out.println("进入格式检查");result.setMsg("请检查表格格式是否符合规范!");result.setMsgCode(201);isError= true;return;}//因为我的表头有两行if (temp < 1 && i < 2) {continue;}if(isNull(data)){result.setMsg("第"+(temp*BATCH_COUNT+i+1)+"行必填项为空!");result.setMsgCode(201);isError= true;return;}WgsUserInfoTempEntity wgsUserInfoTempEntity = new WgsUserInfoTempEntity();//数据准备,每一行的数据存入对应entity中wgsUserInfoTempEntity.setId(data.getId());wgsUserInfoTempEntity.setBh(data.getId());wgsUserInfoTempList.add(wgsUserInfoTempEntity);}if (wgsUserInfoTempList.size() == 0) {System.out.println("Excel表中未读取到数据");result.setMsg("文件数据异常!");result.setMsgCode(201);return;}List wgsUserInfoTempEntities = wgsUserInfoTempRepository.saveAll(wgsUserInfoTempList);if (wgsUserInfoTempEntities.size() > 0) {logger.info("武工商UserInfo存入临时表数据成功");result.setMsg("文件导入成功");result.setMsgCode(200);} else {result.setMsg("文件导入失败");result.setMsgCode(201);}}public Boolean isNull(ExcelWgsUserInfoFileDto excelWgsUserInfoFileDto){if(excelWgsUserInfoFileDto.getId() == null || excelWgsUserInfoFileDto.getId().isEmpty()){return true;}return false;}}).sheet().headRowNumber(0).doRead();} catch (IOException e) {e.printStackTrace();}return result;}