EasyExcel的基础使用
我又来水一篇博客了,以前接触的excel需要记住非常难用的api,使用起来非常繁琐,
最近碰到项目上需要将数据导出为excel,这次又去找找看看有没有更好的方式,easyExcel久仰大名啊
今天看了很久EasyExcel,感觉非常好用,果然非常easy啊,舒服了,终于解决掉了这方面的不足。
下面来看看具体使用步骤吧~
maven引入
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
EasyExcel读取和写入案例
案例其实就是EasyExcel文档中的案例,推荐直接看官方文档,这是我见过最详细的官方文档
Student实体类,StudentService接口,StudentServiceImpl实现类
//Student实体类 /** * @ContentRowHeight(10) 内容单元格行高 * @HeadRowHeight(20) 头单元格行高 * @ColumnWidth(25) 单元格列宽 */ @Data @AllArgsConstructor @NoArgsConstructor @ContentRowHeight(20) @HeadRowHeight(20) @ColumnWidth(25) public class Student { @ExcelProperty("姓名") private String name; @ExcelProperty("性别") private String sex; @ExcelProperty("拟录取学院") private String academy; @ExcelProperty("拟录取专业代码") private String professionalCode; @ExcelProperty("拟录取专业名称") private String professionalName; @ExcelProperty("推荐单位") private String recommendedUnit; @ExcelProperty("复试总成绩") private String score; @ExcelProperty("备注") @ColumnWidth(50) private String remark; /** * @DateTimeFormat 该注解用于识别excel表中该格式的数据 * @ExcelProperty 如果没有该注解,也能读取excel,但是会根据实体类字段的顺序绑定excel中的数据 */ @DateTimeFormat("yyyy年MM月dd日") @ExcelProperty("出生日期") private String birthday; /** * @ExcelIgnore 写数据到excel时,忽略掉该字段数据 */ @ExcelIgnore private String mentor; }
//StudentService接口 public interface StudentService { void save(List<Student> studentList); } //StudentServiceImpl实现类 public class StudentServiceImpl implements StudentService { @Override public void save(List<Student> studentList) { System.out.println("mybatis批量插入数据"); } }
StudentReadListener自定义监听器,读取excel数据时需要使用
@Slf4j public class StudentReadListener implements ReadListener<Student> { /** * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收 */ private static final int BATCH_COUNT = 100; /** * 缓存的数据 */ private List<Student> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); /** * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储,那这个对象没用。 */ private StudentService studentService; public StudentReadListener() { // 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数 studentService = new StudentServiceImpl(); } /** * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来 */ public StudentReadListener(StudentService studentService) { this.studentService = studentService; } /** * 这个每一条数据解析都会来调用,data是解析的每一条数据 */ @Override public void invoke(Student data, AnalysisContext context) { log.info("解析到一条数据:{}", JSON.toJSONString(data)); cachedDataList.add(data); // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM if (cachedDataList.size() >= BATCH_COUNT) { saveData(); // 存储完成清理 list cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); } } /** * 所有数据解析完成了 都会来调用 * 这里的意思是:当一个sheet数据解析完了,缓存中可能还有数据,将缓存中的数据持久化到数据库中 */ @Override public void doAfterAllAnalysed(AnalysisContext context) { // 这里也要保存数据,确保最后遗留的数据也存储到数据库 saveData(); log.info("所有数据解析完成!"); } /** * 一旦缓存满了,就将数据保存到数据库中 */ private void saveData() { log.info("{}条数据,开始存储数据库!", cachedDataList.size()); studentService.save(cachedDataList); log.info("存储数据库成功!"); } @Override public void onException(Exception exception, AnalysisContext context) throws Exception { log.error("解析失败,但是继续解析下一行:{}", exception.getMessage()); // 如果是某一个单元格的转换异常 能获取到具体行号 // 如果要获取头的信息 配合invokeHeadMap使用 if (exception instanceof ExcelDataConvertException) { ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception; log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(), excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData()); } } @Override public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) { log.info("解析到一条头数据:{}", JSON.toJSONString(headMap)); //key是头数据字段的下表,value是头数据的值 Map<Integer, String> stringMap = ConverterUtils.convertToStringMap(headMap, context); } /** * 额外信息(批注、超链接、合并单元格信息读取) * @param extra * @param context */ @Override public void extra(CellExtra extra, AnalysisContext context) { log.info("读取到了一条额外信息:{}", JSON.toJSONString(extra)); switch (extra.getType()) { case COMMENT: log.info("额外信息是批注,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(), extra.getColumnIndex(), extra.getText()); break; case HYPERLINK: if ("Sheet1!A1".equals(extra.getText())) { log.info("额外信息是超链接,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(), extra.getColumnIndex(), extra.getText()); } else if ("Sheet2!A1".equals(extra.getText())) { log.info( "额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{}," + "内容是:{}", extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(), extra.getLastColumnIndex(), extra.getText()); } else { // Assert.fail("Unknown hyperlink!"); System.out.println("Unknown hyperlink!"); } break; case MERGE: log.info( "额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{}", extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(), extra.getLastColumnIndex()); break; default: } } }
ReadExcelTest,读取Excel测试类
@Slf4j public class ReadExcelTest { public static void main(String[] args) { String fileName = "C:\\Users\\Coco\\Desktop\\江苏大学2023年推免生(含直博生)拟录取名单.xlsx"; //在easyExcel内部将数据持久化到数据库中 EasyExcel.read(fileName,Student.class,new StudentReadListener()) .excelType(ExcelTypeEnum.XLSX) .sheet() .headRowNumber(4) .doRead(); } /** * 同步的返回,不推荐使用,如果数据量大会把数据放到内存里面 */ public void synchronousRead() { String fileName = "C:\\Users\\Coco\\Desktop\\江苏大学2023年推免生(含直博生)拟录取名单.xlsx"; // 这里 需要指定读用哪个class去读,然后读取第一个sheet 同步读取会自动finish List<Student> list = EasyExcel.read(fileName).head(Student.class).sheet().doReadSync(); for (Student data : list) { log.info("读取到数据:{}", JSON.toJSONString(data)); } // 这里 也可以不指定class,返回一个list,然后读取第一个sheet 同步读取会自动finish List<Map<Integer, String>> listMap = EasyExcel.read(fileName).sheet().doReadSync(); for (Map<Integer, String> data : listMap) { // 返回每条数据的键值对 表示所在的列 和所在列的值 log.info("读取到数据:{}", JSON.toJSONString(data)); } } /** * 额外信息(批注、超链接、合并单元格信息读取) * <p> * 由于是流式读取,没法在读取到单元格数据的时候直接读取到额外信息,所以只能最后通知哪些单元格有哪些额外信息 * * @since 2.2.0-beat1 */ public void extraRead() { String fileName = "C:\\Users\\Coco\\Desktop\\江苏大学2023年推免生(含直博生)拟录取名单.xlsx"; // 这里需要指定读用哪个class去读,然后读取第一个sheet,这里可能不应该是student.class,回去看看文档 EasyExcel.read(fileName, Student.class, new StudentReadListener()) // 需要读取批注 默认不读取 .extraRead(CellExtraTypeEnum.COMMENT) // 需要读取超链接 默认不读取 .extraRead(CellExtraTypeEnum.HYPERLINK) // 需要读取合并单元格信息 默认不读取 .extraRead(CellExtraTypeEnum.MERGE).sheet().doRead(); } /** * 文件上传 * <p> * 1. 创建excel对应的实体对象 参照{@link UploadData} * <p> * 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UploadDataListener} * <p> * 3. 直接读即可 */ /*@PostMapping("upload") @ResponseBody public String upload(MultipartFile file) throws IOException { EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener(uploadDAO)).sheet().doRead(); return "success"; }*/ }
WriteExcelTest,写入Excel测试类
public class WriteExcelTest { public static void main(String[] args) { String fileName = "C:\\Users\\Coco\\Desktop\\拟录取名单.xlsx"; EasyExcel.write(fileName, Student.class).sheet("模板").doWrite(data()); } /** * 数据来源 * @return */ private static List<Student> data() { List<Student> list = ListUtils.newArrayList(); for (int i = 0; i < 10; i++) { Student data = new Student(); data.setName("万"+i); data.setAcademy("计算机学院"); data.setScore(""+i); list.add(data); } return list; } /** * 最简单的写 * <p> * 1. 创建excel对应的实体对象 * <p> * 2. 直接写即可 */ public void simpleWrite() { // 注意 simpleWrite在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入 // 写法1 JDK8+ // since: 3.0.0-beta1 String fileName = "C:\\Users\\Coco\\Desktop\\拟录取名单.xlsx"; // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 // 如果这里想使用03 则 传入excelType参数即可 EasyExcel.write(fileName, Student.class) .sheet("模板") .doWrite(() -> { // 分页查询数据 return data(); }); // 写法2 fileName = "C:\\Users\\Coco\\Desktop\\拟录取名单.xlsx"; // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 // 如果这里想使用03 则 传入excelType参数即可 EasyExcel.write(fileName, Student.class).sheet("模板").doWrite(data()); } /** * 文件下载(失败了会返回一个有部分数据的Excel) * <p> * 1. 创建excel对应的实体对象 参照{@link DownloadData} * <p> * 2. 设置返回的 参数 * <p> * 3. 直接写,这里注意,finish的时候会自动关闭OutputStream,当然你外面再关闭流问题不大 */ /*@GetMapping("download") public void download(HttpServletResponse response) throws IOException { // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman 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"); EasyExcel.write(response.getOutputStream(), DownloadData.class).sheet("模板").doWrite(data()); }*/ }
EasyExcel填充Excel
虽然EasyExcel的读写操作非常简单了,但是有些不足,例如我想在生成的excel中输出几行提示信息
例如生成的excel样式非常复杂,那上面的操作其实根本不够看,如果是这种情况,我们可以用填充的方式实现导出excel文件
个人认为在企业生产中,使用这种方式还是最简便的,废话不多说,看看代码咋写的
首先制作导出模板
需要注意的是,{变量名}是单个数据,如果导出的是列表数据,则需要在变量前加个`.` 例如姓名那一行,其变量名都是{.变量名}
填充excel代码,注意easyExcel版本不同代码也不同,本文基于EasyExcel3.0版本,具体代码请看官方文档
public class WriteExcelTest { public static void main(String[] args) { String templateFileName = "C:\\Users\\Coco\\Desktop\\导出模板.xlsx"; String fileName = "C:\\Users\\Coco\\Desktop\\导出excel文件.xlsx"; ExcelWriter excelWriter = null; try { excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build(); WriteSheet writeSheet = EasyExcel.writerSheet().build(); // 这里注意 入参用了forceNewRow 代表在写入list的时候不管list下面有没有空行 都会创建一行,然后下面的数据往后移动。默认 是false,会直接使用下一行,如果没有则创建。 // forceNewRow 如果设置了true,有个缺点 就是他会把所有的数据都放到内存了,所以慎用 // 简单的说 如果你的模板有list,且list不是最后一行,下面还有数据需要填充 就必须设置 forceNewRow=true 但是这个就会把所有数据放到内存 会很耗内存 // 如果数据量大 list不是最后一行 参照下一个 FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build(); excelWriter.fill(data(), fillConfig, writeSheet); Map<String, Object> map = MapUtils.newHashMap(); map.put("datetime", LocalDateTime.now()); map.put("count", 1000); map.put("person", "万一"); excelWriter.fill(map, writeSheet); } finally { // 千万别忘记关闭流 if (excelWriter != null) { excelWriter.finish(); } } } /** * 数据来源 * @return */ private static List<Student> data() { List<Student> list = ListUtils.newArrayList(); for (int i = 0; i < 10; i++) { Student data = new Student(); data.setName("万"+i); data.setAcademy("计算机学院"); data.setScore(""+i); data.setSex("男"); data.setProfessionalCode("1010"+i); data.setProfessionalName("计算机科学与技术"); data.setRecommendedUnit("湖北师范大学"); data.setRemark("无"); data.setBirthday(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"))); list.add(data); } return list; } }
导出excel文件.xlsx
结合SpringMVC使用EasyExcel实现字典数据的导入导出
导出功能
字典数据导出功能涉及到了合并单元格的问题,多个字典项有着同一个字典数据,合并单元格需要做到下图的方式经过查阅网上的资料,有一种解决方法,通过数据组装,使得每条字典项都有相同的字典数据,
再把相同的字典数据列进行合并,从而形成上图的形式,合并的代码是copy的,下面直接贴代码在Dict字典实体类中,加入DictItem字典项实体类的字段
@ContentRowHeight(30) @HeadRowHeight(167) @ColumnWidth(20) @ExcelIgnoreUnannotated public class EamsDict{ //dict字段略,lombok等注解略,无参构造方法略 /** * itemText,itemValue,itemDescription三个字段是用于导出字典数据用的 */ @TableField(exist = false) private List<EamsDictItem> dictItems; @TableField(exist = false) @ExcelProperty("字典项名称") private String itemText; @TableField(exist = false) @ExcelProperty("字典项数据值") private String itemValue; @TableField(exist = false) @ExcelProperty("字典项描述") private String itemDescription; public EamsDict(String itemText, String itemValue, String itemDescription) { this.itemText = itemText; this.itemValue = itemValue; this.itemDescription = itemDescription; } }
准备好导出的Excel模板并放到项目的resources中
在项目POM文件中配置插件
<!-- 避免font文件的二进制文件格式压缩破坏 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.1.0</version> <configuration> <nonFilteredFileExtensions> <nonFilteredFileExtension>xlsx</nonFilteredFileExtension> <nonFilteredFileExtension>xls</nonFilteredFileExtension> <nonFilteredFileExtension>docx</nonFilteredFileExtension> <nonFilteredFileExtension>doc</nonFilteredFileExtension> <nonFilteredFileExtension>woff</nonFilteredFileExtension> <nonFilteredFileExtension>woff2</nonFilteredFileExtension> <nonFilteredFileExtension>eot</nonFilteredFileExtension> <nonFilteredFileExtension>ttf</nonFilteredFileExtension> <nonFilteredFileExtension>svg</nonFilteredFileExtension> </nonFilteredFileExtensions> </configuration> </plugin>
Controller方法实现
/** * 导出excel * * @param request */ @RequestMapping(value = "/exportXlsxByEasyExcel") public Result<Object> exportExcel(EamsDict eamsDict, HttpServletRequest request, HttpServletResponse response) { if (ObjectUtil.isEmpty(eamsDict.getSystemCode())){ return Result.error("导出失败!"); } //1,准备字典数据,获取要删除的字典数据及其字典项数据,根据systemCode字段查看业务档案的字典数据 LambdaQueryWrapper<EamsDict> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(EamsDict::getSystemCode,eamsDict.getSystemCode()); queryWrapper.eq(EamsDict::getDelFlag,"0"); queryWrapper.orderByAsc(EamsDict::getCreateTime); ArrayList<EamsDict> allDictData = new ArrayList<>(); List<EamsDict> dicts = eamsDictService.list(queryWrapper); List<EamsDictItem> dictItems = eamsDictItemService.list(); //2,对数据组装,以使得输出的数据样式合理 dicts.forEach(dict->{ ArrayList<EamsDict> eamsDicts = new ArrayList<>(); dictItems.forEach(dictItem ->{ if (ObjectUtil.equals(dict.getId(),dictItem.getDictId())){ eamsDicts.add(new EamsDict(dictItem.getItemText(),dictItem.getItemValue(),dictItem.getDescription())); } }); if (ObjectUtil.isNotEmpty(eamsDicts)){ for (int i = 0; i < eamsDicts.size(); i++) { BeanUtil.copyProperties(dict,eamsDicts.get(i), CopyOptions.create().setIgnoreNullValue(true).setIgnoreError(true)); allDictData.add(eamsDicts.get(i)); } }else{ allDictData.add(dict); } }); //3,设置response的返回格式,字符集,返回文件名 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String fileName = null; try { fileName = URLEncoder.encode("嘉兴市档案馆", "UTF-8").replaceAll("\\+", "%20"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); //4,获取模板输入字节流和response输出流 InputStream templateFileInputStream = this.getClass().getClassLoader().getResourceAsStream("templateFile.xlsx"); ServletOutputStream outputStream = null; try { outputStream = response.getOutputStream(); } catch (IOException e) { e.printStackTrace(); } //需要合并的列 int[] mergeColumeIndex = {0,1,2,6,7,8,9}; // 从那一列开始合并 int mergeRowIndex = 4; ExcelWriter excelWriter = EasyExcel.write(outputStream) .excelType(ExcelTypeEnum.XLSX) .registerWriteHandler(new ExcelFillCellMergeStrategy(mergeRowIndex,mergeColumeIndex)) .withTemplate(templateFileInputStream).build(); if(ObjectUtil.isNotEmpty(excelWriter)){ WriteSheet writeSheet = EasyExcel.writerSheet().build(); FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build(); excelWriter.fill(allDictData, fillConfig, writeSheet); //准备基础数据 LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); EamsBusiness business = businessService.getOne(new LambdaQueryWrapper<EamsBusiness>().eq(EamsBusiness::getSystemCode, eamsDict.getSystemCode())); Map<String, Object> map = MapUtils.newHashMap(); map.put("realname", user.getRealname()); map.put("username", user.getUsername()); map.put("exportTime", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); //如果导出的是系统字典,那么就没有这两条数据 if (ObjectUtil.isNotEmpty(business)){ map.put("deptName",business.getDeptName()); map.put("businessName",business.getBusinessName()); }else { map.put("deptName","无"); map.put("businessName","系统字典"); } excelWriter.fill(map, writeSheet); excelWriter.finish(); return Result.OK("导出成功!"); } return Result.error("导出失败!"); }
ExcelFillCellMergeStrategy,合并策略
public class ExcelFillCellMergeStrategy implements CellWriteHandler { private int[] mergeColumnIndex; private int mergeRowIndex; public ExcelFillCellMergeStrategy() { } public ExcelFillCellMergeStrategy(int mergeRowIndex, int[] mergeColumnIndex) { this.mergeRowIndex = mergeRowIndex; this.mergeColumnIndex = mergeColumnIndex; } @Override public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) { } @Override public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { } @Override public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> list, Cell cell, Head head, Integer integer, Boolean aBoolean) { int curRowIndex = cell.getRowIndex(); int curColIndex = cell.getColumnIndex(); if (curRowIndex > mergeRowIndex) { for (int i = 0; i < mergeColumnIndex.length; i++) { if (curColIndex == mergeColumnIndex[i]) { mergeWithPrevRow(writeSheetHolder, cell, curRowIndex, curColIndex); break; } } } } //判断单元格格式并使用相应的格式读取数据 private Object judgeCellType(Cell cell){ Object data = null; switch (cell.getCellType()){ case STRING: data = cell.getStringCellValue(); break; case NUMERIC: data = cell.getNumericCellValue(); break; default: data = cell.getStringCellValue(); break; } return data; } /** * 当前单元格向上合并 * * @param writeSheetHolder * @param cell 当前单元格 * @param curRowIndex 当前行 * @param curColIndex 当前列 */ private void mergeWithPrevRow(WriteSheetHolder writeSheetHolder, Cell cell, int curRowIndex, int curColIndex) { Cell preCell = cell.getSheet().getRow(curRowIndex - 1).getCell(curColIndex); Object curData = judgeCellType(cell); Object preData = judgeCellType(preCell); // 将当前单元格数据与上一个单元格数据比较 Boolean dataBool = preData.equals(curData); //此处需要注意:因为我是按照序号确定是否需要合并的,所以获取每一行第一列数据和上一行第一列数据进行比较,如果相等合并 Boolean bool = cell.getRow().getCell(0).getStringCellValue().equals(cell.getSheet().getRow(curRowIndex - 1).getCell(0).getStringCellValue()); if (dataBool && bool) { Sheet sheet = writeSheetHolder.getSheet(); List<CellRangeAddress> mergeRegions = sheet.getMergedRegions(); boolean isMerged = false; for (int i = 0; i < mergeRegions.size() && !isMerged; i++) { CellRangeAddress cellRangeAddr = mergeRegions.get(i); // 若上一个单元格已经被合并,则先移出原有的合并单元,再重新添加合并单元 if (cellRangeAddr.isInRange(curRowIndex - 1, curColIndex)) { sheet.removeMergedRegion(i); cellRangeAddr.setLastRow(curRowIndex); sheet.addMergedRegion(cellRangeAddr); isMerged = true; } } // 若上一个单元格未被合并,则新增合并单元 if (!isMerged) { CellRangeAddress cellRangeAddress = new CellRangeAddress(curRowIndex - 1, curRowIndex, curColIndex, curColIndex); sheet.addMergedRegion(cellRangeAddress); } } } }
导入功能
导入功能一般还是按照上面的模板进行导入,但是会碰到一个问题,easyExcel在读取到合并单元格的时候
只有第一条数据能全部的数据(字典数据+一条字典项数据),而合并单元格内部的其他字典项数据,
只有字典项数据,没有字典数据,解决方法是通过判断和拼装,把字典项数据都存到字典数据中,
最后再保存到数据库中Controller方法
@PostMapping("/importXlsxByEasyExcel") public Result<Object> importExcel(MultipartHttpServletRequest request){ String systemCode = request.getParameter("systemCode"); List<MultipartFile> files = request.getFiles("file"); for (MultipartFile file : files) { InputStream fileInputStream = null; try { fileInputStream = file.getInputStream(); } catch (IOException e) { e.printStackTrace(); } EasyExcel.read(fileInputStream,new ImportExcelDataListener(eamsDictService,systemCode)).sheet().headRowNumber(4).doRead(); } return Result.OK(); }
ImportExcelDataListener,监听器
public class ImportExcelDataListener extends AnalysisEventListener<Map<Integer, String>> { /** * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收 */ private static final int BATCH_COUNT = 100; /** * 缓存的数据 */ private LinkedHashMap<String,EamsDict> dictsMap = new LinkedHashMap<>(); /** * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。 */ private IEamsDictService eamsDictService; private String systemCode; public ImportExcelDataListener() { } /** * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来 */ public ImportExcelDataListener(IEamsDictService eamsDictService,String systemCode) { this.eamsDictService = eamsDictService; this.systemCode = systemCode; } /** * 这个每一条数据解析都会来调用 * * @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()} * @param context */ @Override public void invoke(Map<Integer, String> data, AnalysisContext context) { /* 首先把map数据读取出来,转成EamsDict对象保存到List中,对于dictCode为空的数据,把它作为合并单元格保存到上一个DictCode对象中 如果是读取合并单元格的数据,会有一种情况,合并单元格第一行中有dict的数据,但是其他行中dict的数据为空 另外,如果用户上传的文件中dictCode字段本身为空呢,我这里的处理就可能把它当做合并单元格进行处理了 */ String dictCode = data.get(1); //如果读取的dictCode不为空,说明是Dict数据,其中可能保存了dictItem的数据。如果为空,则一定是dictItem的数据 if (ObjectUtil.isNotEmpty(dictCode)){ EamsDict dict = dictsMap.get(dictCode); if (ObjectUtil.isNotEmpty(dict)){ //如果有两个相同的dictCode,只能把后面的数据归到之前的dict对象中了 List<EamsDictItem> dictItems = dict.getDictItems(); if (ObjectUtil.isNotEmpty(dictItems)){ dictItems.add(assembleDictItemData(data)); }else{ ArrayList<EamsDictItem> eamsDictItems = new ArrayList<>(); eamsDictItems.add(assembleDictItemData(data)); dict.setDictItems(eamsDictItems); } }else{ //提取dict和dictItem的数据 EamsDict eamsDict = assembleDictData(data, systemCode); dictsMap.put(eamsDict.getDictCode(),eamsDict); } }else{ Map.Entry<String, EamsDict> tail = null; try { tail = getMapTailByReflection(dictsMap); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); } EamsDict eamsDict = tail.getValue(); data.put(0,eamsDict.getDictName()); data.put(1,eamsDict.getDictCode()); data.put(2,eamsDict.getDescription()); data.put(6,eamsDict.getCreateBy()); data.put(7,eamsDict.getCreateTime().toString()); data.put(8,eamsDict.getUpdateBy()); data.put(9,eamsDict.getUpdateTime().toString()); List<EamsDictItem> dictItems = eamsDict.getDictItems(); if (ObjectUtil.isNotEmpty(dictItems)){ dictItems.add(assembleDictItemData(data)); }else { ArrayList<EamsDictItem> eamsDictItems = new ArrayList<>(); eamsDictItems.add(assembleDictItemData(data)); eamsDict.setDictItems(eamsDictItems); } } } /** * 获取dictsMap中最后一个元素 * @param map * @param <K> * @param <V> * @return * @throws NoSuchFieldException * @throws IllegalAccessException */ public <K, V> Map.Entry<K, V> getMapTailByReflection(LinkedHashMap<K, V> map) throws NoSuchFieldException, IllegalAccessException { Field tail = map.getClass().getDeclaredField("tail"); tail.setAccessible(true); return (Map.Entry<K, V>) tail.get(map); } /** * 所有数据解析完成了 都会来调用 * * @param context */ @Override public void doAfterAllAnalysed(AnalysisContext context) { //判断集合中数据的字典代码是否已经存在数据库,如果存在,报错 ArrayList<String> dictCodes = eamsDictService.list().stream().map(EamsDict::getDictCode).collect(Collectors.toCollection(ArrayList::new)); Set<String> codes = dictsMap.keySet(); for (String code : codes) { if (dictCodes.contains(code)){ //这里抛异常还有待优化 try { throw new IOException(""+code+"已经存在数据库中"); } catch (IOException e) { e.printStackTrace(); } } } //此时eamsDict中的dictItem数据还差sortOrder和dictId字段 dictsMap.values().forEach(eamsDict -> { List<EamsDictItem> dictItems = eamsDict.getDictItems(); if (ObjectUtil.isNotEmpty(dictItems)){ int i = 1; for (EamsDictItem dictItem : dictItems) { dictItem.setSortOrder(i++); } } eamsDictService.saveMain(eamsDict,eamsDict.getDictItems()); }); } /* 将map中数据提取成eamsDict对象 */ private EamsDict assembleDictData(Map<Integer, String> map,String systemCode){ //组装字典数据 EamsDict eamsDict = new EamsDict(); eamsDict.setSystemCode(systemCode); eamsDict.setDelFlag(0); eamsDict.setType(0); eamsDict.setDictName(map.get(0)); eamsDict.setDictCode(map.get(1)); eamsDict.setDescription(map.get(2)); eamsDict.setCreateBy(map.get(6)); eamsDict.setCreateTime(DateTime.of(map.get(7),"yyyy-MM-dd HH:mm:ss")); eamsDict.setUpdateBy(map.get(8)); eamsDict.setUpdateTime(DateTime.of(map.get(9),"yyyy-MM-dd HH:mm:ss")); String itemValue = map.get(4); if (ObjectUtil.isNotEmpty(itemValue)){ EamsDictItem eamsDictItem = assembleDictItemData(map); ArrayList<EamsDictItem> dictItems = new ArrayList<>(); dictItems.add(eamsDictItem); eamsDict.setDictItems(dictItems); } return eamsDict; } private EamsDictItem assembleDictItemData(Map<Integer, String> map){ EamsDictItem eamsDictItem = new EamsDictItem(); eamsDictItem.setItemText(map.get(3)); eamsDictItem.setItemValue(map.get(4)); eamsDictItem.setDescription(map.get(5)); eamsDictItem.setStatus(1); eamsDictItem.setCreateBy(map.get(6)); eamsDictItem.setCreateTime(DateTime.of(map.get(7), "yyyy-MM-dd HH:mm:ss")); eamsDictItem.setUpdateBy(map.get(8)); eamsDictItem.setUpdateTime(DateTime.of(map.get(9), "yyyy-MM-dd HH:mm:ss")); return eamsDictItem; } }