EasyExcel

提起Excel框架,比较知名的Apache-POI等框架,针对Excel的处理提供了非常丰富的功能,但是在处理大数据量级的Excel文件时,会占用极高的内存,

而且无论是导入还是导出速度都相对较慢,于是阿里巴巴的EasyExcel应运而生。EasyExcel是一个高效、低内存占用的Excel处理框架,提供了简洁易用的API接口,

使得我们能更加高效、灵活的处理Excel文件。当然,EasyExcel并非一个完全从零构建的开源项目,而是在Apache-POI的基础上做了进一步封装得到的。

接下来,以如下Excel文件为例,讲解基于EasyExcel的数据解析

读取Excel数据

我们只需要调用EasyExcel的read方法的即可,该方法需要传入两个参数,inputStream代表所读取的Excel文件输入流,ReadListener用来处理每一行读取到的数据。又因为每一行数据的处理随着业务的不同而不同,所以我们需要在调用read方法时自己传入ReadListener接口的实现子类对象(可以使用匿名内部类对象,或者lambda表达式)

/**
    inputStream: 表示我们读取的Excel文件的输入流
    clz: Excel文件实体类对应的映射实体类的Class对象
    readListener: 数据读取监听器,每一行的数据都交给readListner来处理
*/
public static ExcelReaderBuilder read(InputStream inputStream, Class head, ReadListener readListener)

如果我们希望通过前端页面以文件上传的方式读取Excel文件内容,代码如下:

    @PostMapping("upload")
    @ResponseBody
    public String upload(MultipartFile file) throws IOException {
        
        // 1.创建用于读取数据的ReadListener对象, 泛型类型表示封装Excel每行数据的实体类
        ReadListener<XXX> readListener= ....
        
        // 2. 定义用来封装Excel每行数据的实体类的Class对象
        Class<XXX> rowClass = XXX.class;
        
        // 3. 调用EeasyExcel类的静态方法read,按行读取Excel文件中的数据
        EasyExcel.read(file.getInputStream(), rowClass, readListener)
        // 指定读取的sheet我们的一个excel只使用默认的一个sheet所以不用指定
        .sheet()
        // 开始读取,方法执行完毕,那么整个Excel文件也读取完了
        .doRead();
        return "success";
    }

也就是说,我们只需要调用EeasyExcel.read(…).sheet().doRead()方法即可完成按行对Excel文件内容的读取了。但是,在read方法中涉及到了如下两个参数,我们需要详细了解:

  • 用来封装Excel行数据的映射实体类(有点类似于读取数据库表中的行数据)
  • 用来读取到Excel行数据的ReadListener。(我们的主要代码就在这个ReadListener中)

映射实体类

Excel中的一行数据可以映射为一个对象(类似于数据库表中的一行数据也可以映射为一个对象)。因此我们在解析数据前需要先定义实体类,并且完成实体类属性与Excel文件字段的映射。

实体类成员变量与Excel字段的映射可以使用@ExcelProperty注解的index属性或者value属性定义:

  • index属性: 定义的是实体类属性与Excel字段的列下标之间的映射(Excel中的列下标从0开始)
  • value属性: 定义的是实体类属性与Excel字段名称之间的映射

两种方式都可以完成实体类属性与Excel字段的映射,但是对于某一个属性而言要么使用index,要么使用value属性,不要同时使用

@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
    /**
     * 将该成员变量映射到excel的第3列即数字标题那个字段(列数从0开始所以第3列的index是2)
     */
    @ExcelProperty(index = 2)
    private Double doubleData;
    /**
     * 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据
     */
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
}

ReadListener数据读取监听器

实际上EasyExcel读取Excel文件数据的方式是一行一行读取的,每读取一行数据就会将数据转化为实体类对象,并且交给ReadListener来处理,ReadListener的泛型类型在使用时需要指定为Excel实体类。

可以看到ReadListener主要包含3个方法:

  • invoke方法: 通过data接收当前读取的一行数据转化而成的对象,还可以通过context.readRowHolder().getRowIndex()方法指定当前读取的行下标(行下标从0开始)
  • hasNext方法: 决定是否继续接着继续解析剩下的Excel数据,如果返回false则不会继续向下读取数据,相当于终止数据读取过程,默认始终返回true。
  • doAfterAllAnalysed:当excel数据全部成功解析完毕时,doAfterAllAnalysed方法会被调用。

/**
   实际上如果我们定义了Excel文件的映射实体类
*/
public interface ReadListener<T> extends Listener {

    /**
     * When analysis one row trigger invoke function.
     *
     * @param data    one row value. It is same as {@link AnalysisContext#readRowHolder()}
     * @param context analysis context
     */
    void invoke(T data, AnalysisContext context);
    
    /**
     * Verify that there is another piece of data.You can stop the read by returning false
     *
     * @param context
     * @return
     */
    default boolean hasNext(AnalysisContext context) {
        return true;
    }


    /**
     * if have something to do after all analysis
     *
     * @param context
     */
    void doAfterAllAnalysed(AnalysisContext context);
}

定义处理示例Excel文件的ReadLisner子类如下(实际使用的时候也可以不用定义子类,使用lambda表达式或者匿名内部类对象都可以):

// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {

    /**
     * 假设这个是一个Mapper,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
     */
    private DemoMapper demoMapper;


    /**
     * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
     *
     * @param demoDAO
     */
    public DemoDataListener(DemoMapper demoMapper) {
        this.demoMapper = demoMapper;
    }

    /**
     * 这个每一条数据解析都会来调用
     *
     * @param data    one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context
     */
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSON.toJSONString(data));
        // 向数据库保存一条数据
        saveData(data);
    }

    /**
     * 所有数据解析完成了 都会来调用
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        log.info("所有数据解析完成!");
    }

    /**
     * 加上存储数据库
     */
    private void saveData(DemoData data) {
        log.info("开始存储数据库!");
        demoMapper.insert(data);
        log.info("存储数据库成功!");
    }
}

所以针对课件中给出的Excel数据的读取,完整的读取代码为:

    @PostMapping("upload")
    @ResponseBody
    public String upload(MultipartFile file) throws IOException {
        
        // 1.创建用于读取数据的ReadListener对象, 泛型类型表示封装Excel每行数据的实体类
        ReadListener<DemoData> readListener = new  DemoDataListener(demoMapper);
        
        // 2. 定义用来封装Excel每行数据的实体类的Class对象
        Class<DemoData> rowClass = DemoData.class;
        
        // 3. 调用EeasyExcel类的静态方法read,按行读取Excel文件中的数据
        EasyExcel.read(file.getInputStream(), rowClass, readListener)
        // 指定读取的sheet我们的一个excel只使用默认的一个sheet所以不用指定
        .sheet()
        // 开始读取,方法执行完毕,那么整个Excel文件也读取完了
        .doRead();
        return "success";
    }

读取Excel头

ReadLisner中的invoke方法可以让我们获取到Excel文件中的Excel数据,但是如果我们还想获取Excel文件头数据呢?

image-20250428171755817

此时我们可以使用ReadListener的一个抽象子类来完成——AnalysisEventListener,定义如下:

/**
 * Receives the return of each piece of data parsed
 *
 * @author jipengfei
 */
public abstract class AnalysisEventListener<T> implements ReadListener<T> {

    // 此方法我们无需关心,是框架内部使用
    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
        invokeHeadMap(ConverterUtils.convertToStringMap(headMap, context), context);
    }

    /**
     * 这个方法才是我们用来获取Excel文件头的
     * Returns the header as a map.Override the current method to receive header data.
     * 
     * @param headMap: key为Excel列下标,value为该列的名称
     */
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {}

}

假设我们要是用AnalysisEventListener既读取示例Excel文件头数据,又要读取文件内容,我们可以定义自己的AnalysisEventListener的子类即可

// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
public class MyHeaderReadListener extends AnalysisEventListener<DemoData> {
    
    // 如果需要使用其他Mapper,或者Service的话,可以定义成员变量,以及对应的构造方法
    

    // 我们只需要重写invokeHeadMap方法即可获取Excel文件头
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
       // 通过headMap这个方法参数,获取Excel文件头对应下标的列名称  
    }

    // 该方法就是ReadListener父接口中定义的方法,用来读取行数据
    @Override
    public void invoke(DemoData data, AnalysisContext context) {

    }

    // 该方法就是ReadListener父接口中定义的方法,在excel读取完毕后执行
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {

    }
}

在使用的时候,代码几乎是一样的

    @PostMapping("upload")
    @ResponseBody
    public String upload(MultipartFile file) throws IOException {
        
        // 1.创建用于读取数据的ReadListener对象, 泛型类型表示封装Excel每行数据的实体类
        AnalysisEventListener<DemoData> analysisEventListener = new  MyHeaderReadListener(....);
        
        // 2. 定义用来封装Excel每行数据的实体类的Class对象
        Class<DemoData> rowClass = DemoData.class;
        
        // 3. 调用EeasyExcel类的静态方法read,按行读取Excel文件中的数据
        EasyExcel.read(file.getInputStream(), rowClass, readListener)
        // 指定读取的sheet我们的一个excel只使用默认的一个sheet所以不用指定
        .sheet()
        // 开始读取,方法执行完毕,那么整个Excel文件也读取完了
        .doRead();
        return "success";
    }

批量处理Excel数据

一个Excel数据中可能有上万条数据,有时我们想要批量处理,一次处理多行Excel数据怎么办呢?此时我们就可以对读取数据的DemoDataListener或者MyHeaderReadListener稍加改造即可,这里以DemoDataListener为例:

// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {

    /**
     * 每100条数据处理一次,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 100;
    /**
     * 缓存的数据
     */
    private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
    /**
     * 假设这个是一个Mapper,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
     */
    private DemoMapper demoMapper;

    /**
     * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
     *
     * @param demoDAO
     */
    public DemoDataListener(DemoMapper demoMapper) {
        this.demoMapper = demoMapper;
    }

    /**
     * 这个每一条数据解析都会来调用
     *
     * @param data    one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context
     */
    @Override
    public void invoke(DemoData 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);
        }
    }

    /**
     * 所有数据解析完成了 都会来调用
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 这里也要保存数据,确保最后遗留的数据也存储到数据库
        saveData();
        log.info("所有数据解析完成!");
    }

    /**
     * 加上存储数据库
     */
    private void saveData() {
        log.info("{}条数据,开始存储数据库!", cachedDataList.size());
        // 利用通用mapper,或者通用Service批量保存数据到数据库
        log.info("存储数据库成功!");
    }
}

改造完成之后的使用还是和之前一模一样:

    @PostMapping("upload")
    @ResponseBody
    public String upload(MultipartFile file) throws IOException {
        
        // 1.创建用于读取数据的ReadListener对象, 泛型类型表示封装Excel每行数据的实体类
        AnalysisEventListener<DemoData> analysisEventListener = new  MyHeaderReadListener(....);
        
        // 2. 定义用来封装Excel每行数据的实体类的Class对象
        Class<DemoData> rowClass = DemoData.class;
        
        // 3. 调用EeasyExcel类的静态方法read,按行读取Excel文件中的数据
        EasyExcel.read(file.getInputStream(), rowClass, readListener)
        // 指定读取的sheet我们的一个excel只使用默认的一个sheet所以不用指定
        .sheet()
        // 开始读取,方法执行完毕,那么整个Excel文件也读取完了
        .doRead();
        return "success";
    }

Excel的上传分为了两个步骤:

  • 校验: 当我们点击上传,并选择Excel文件后,前端页面首先会调用后端接口,校验Excel文件内容的正确性
  • 上传: 当Excel文件中的数据都校验无误,才能点击确定,开始上传Excel数据

所以接下来,简单说明一下校验和上传

Excel数据校验

关于词汇管理的Excel文件我们可以校验如下内容:

  • 校验单元格中是否有空数据(是否有对象的属性值为null)
  • 校验单词格式是否为 词性.词义
  • 校验例句格式是否为: 英文例句\n例句中文释义
  • 校验单词所属的书籍或者章节是否存在

在以上校验中,如何一个校验有问题,都可以抛出异常ExcelParseException(项目中给大家提供),例如

public class ExcelParseException extends BusinessException {
    // 行数
    private int rowNum;
    // 列数
    private int colNum;
    // 单元格值
    private String cellValue;

    public ExcelParseException(ResultCodeEnum codeEnum, int rowNum, int colNum, String cellValue, String message) {
        super(message, codeEnum.getCode());
        this.rowNum = rowNum;
        this.colNum = colNum;
        this.cellValue = cellValue;
    }

    public ExcelParseException(ResultCodeEnum codeEnum, int rowNum, String message) {
        super(message, codeEnum.getCode());
        this.rowNum = rowNum;
        this.colNum = -1;
        this.cellValue = null;
    }

    public ExcelParseException(ResultCodeEnum codeEnum, String message) {
        super(message, codeEnum.getCode());
        this.rowNum = -1;
        this.colNum = -1;
        this.cellValue = null;
    }

    @Override
    public String getMessage() {
        if (rowNum >= 0 && colNum >= 0 && cellValue != null) {
            return String.format("Excel解析错误(行:%d,列:%d,值:%s) - %s",
                                rowNum, colNum, cellValue, super.getMessage());
        } else if (rowNum >= 0) {
            return String.format("Excel解析错误(行:%d) - %s", rowNum, super.getMessage());
        } else {
            return String.format("Excel解析错误 - %s", super.getMessage());
        }
    }
}
   
    @Autowired
    VocService vocService;
   
   
    @PostMapping("/admin/voc/excel/validate")
    public Result vocExcelVerify(@RequestParam("vocExcel") MultipartFile file) throws IOException {
        // 在该service方法中完成Excel文件内容的读取
        vocService.vocExcelValidate(file);
        return Result.ok();
    }

Excel数据上传

关于Excel文件上传需要注意的是:

  • 最好能够批量处理,将多行数据在一个事务中处理
  • 上传数据的过程中一旦出现问题,抛出ExcelParseException

    @Autowired
    VocService vocService;

    @PostMapping("/admin/voc/excel")
    public Result addVocExcel(@RequestParam("vocExcel") MultipartFile file) throws IOException {
        // 获取当前登录的后台用户id
        Long employeeId = StpKit.ADMIN.getLoginIdAsLong();
        // 调用该方法完成Excel上传单词功能
        vocService.addVocExcel(file, employeeId);
        return Result.ok();
    }

Excel模版下载

excel文件的下载没什么技术难度,代码直接给到大家,代码如下:


public ResponseEntity<byte[]> downloadTemplate() throws IOException {
           public ResponseEntity<byte[]> downloadTemplate() throws IOException {
        // 使用ClassPathResource读取资源文件

        ClassPathResource resource;
        resource = new ClassPathResource("templates/单词上传模版.xlsx");

        // 检查文件是否存在
        if (!resource.exists()) {
            return ResponseEntity.notFound().build();
        }

        // 读取文件内容
        InputStream inputStream = resource.getInputStream();
        byte[] fileBytes = IOUtils.toByteArray(inputStream);

        // 设置响应头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        headers.setContentDispositionFormData("attachment",
                URLEncoder.encode(resource.getFilename(), "UTF-8"));

        return ResponseEntity.ok()
                .headers(headers)
                .body(fileBytes);
    }
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇