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;
          }
          
      }