【日常需求】一次使用EasyExcel而引发的问题与思考~

2,649 阅读6分钟

前言

大家好啊,我是皮皮虾~,快一年没写文章了,写的有什么不对的大家直接在评论区批评😂
最近接的个需求中有个小功能是要上传用户id或email的excel,解析返回出正常的用户数和异常的用户数(简单来说就是能查到用户信息的数量和不能查到的数量)。

擦,我寻思这不得涉及到流的操作,这可是我的弱项啊~

image-20220925142658348

当然来,正经人谁直接手写,当然要拥抱开源啦,这不就遇到了阿里的EasyExcel

image-20220925143034134


如何利用EasyExcel解析Excel

1. 首先要定义一个对象

根据EasyExcel的思想,excel的每一行都认为是一个对象,每一列可以认为是一个字段

image-20220925143842276

那么对应到Java中,我们就可以建一个实体类,来对应我们需要解析的Excel。

实习类中的字段注意要加上@ExcelProperty注解

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserData {
​
    @ExcelProperty(index = 0)
    private Long uid;
    
    @ExcelProperty(index = 1)
    private String email;
​
}

@ExcelProperty注解,可以设置如下四个字段的值,一般情况下,我们需要设置 index 代表这个字段是对应的第几列(从0开始),或者设置 value 代表列名称,也就是通过列名称来匹配

image-20220925144249978

要注意的是,在官方文档中也提示了我们,不要同时使用index和value,如果excel没有设置标题或者标题重复可能导致解析不尽人意~

image-20220925144457493


2. 创建监听器

创建一个监听器类,可以选择继承AnalysisEventListener类或者实现ReadListener接口

那么这两个有什么区别嘞?

其实区别不大,AnalysisEventListener实现了ReadListener的四个方法,主要的话还是在于对于表头数据的读取,通过重写invokeHead方法,帮我们手动将表头数据转换了一下,这样如果我们需要读取表头数据的话,直接继承AnalysisEventListener,实现invokeHeadMap即可,不然如果是依然实现ReadListener,我们还需要手动ConverterUtils.convertToStringMap转换

image-20220925151124594

@EqualsAndHashCode(callSuper = true)
@Slf4j
@Data
@AllArgsConstructor
public class UserDataListener extends AnalysisEventListener<UserData> {
​
    /**
     * uid集合
     */
    private List<Long> uidList;
​
    /**
     * 邮箱集合
     */
    private List<String> emailList;
​
    /**
     * 有效用户数量
     */
    private Integer validUserNum;
​
    /**
     * 无效用户数量
     */
    private Integer invalidUserNum;
​
    /**
     * 类型(0:uid,1:email)
     */
    private Integer type;
​
    public UserDataListener(Integer type) {
        this.type = type;
        if (type.equals(0)) {
            uidList = new ArrayList<>();
            return;
        }
        emailList = new ArrayList<>();
    }
​
    @Override
    public void invoke(UserData data, AnalysisContext context) {
        if (type.equals(0)) {
            uidList.add(data.getUid());
            return;
        }
        emailList.add(data.getEmail());
    }
​
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 解析完成之后调用该方法 
        /**
         * rpc批量查询用户信息,通过type来区分是通过uidList查询还是emailList查询
         * ps:批量查询中,如果用户uid或者email不存在,最终返回的结果里则不包含该用户信息
         * 所以,rpc用户集合数量 <= uidList.size() or emailList.size()
         * 通过这个来计算validUserNum 和 invalidUserNum
         */
​
    }
​
    @Override
    public void onException(Exception exception, AnalysisContext context) {
        throw new RuntimeException("解析user excel异常", exception);
    }
​
}

3. 解析excel

获取前端传过来的excel文件和上传类型,进行基本的参数校验之后,进行excel的解析

要注意的是监听器不能被Spring管理,每次都要去new一个新的对象

@Override
public UserUploadVO uploadFile(MultipartFile file, Integer type) throws Exception {
​
    if (file == null || type == null) {
        throw new RuntimeException("Param error");
    }
​
    UserUploadVO userUploadVO = new UserUploadVO();
    
    String fileName = file.getOriginalFilename();
    if (StringUtils.isEmpty(fileName)) {
        return userUploadVO;
    }
​
    String fileXlsx = fileName.substring(fileName.length() - 5);
    String fileXls = fileName.substring(fileName.length() - 4);
    if (!".xlsx".equals(fileXlsx) && !".xls".equals(fileXls)) {
        return userUploadVO;
    }
​
    UserDataListener userDataListener = new UserDataListener(type);
    // 解析excel
    EasyExcel.read(file.getInputStream(), UserData.class, userDataListener).sheet().doRead();
​
    // 设置有效用户数和无效用户数
    userUploadVO.setInvalidUserNum(userDataListener.getInvalidUserNum());
    userUploadVO.setValidUserNum(userDataListener.getValidUserNum());
    return userUploadVO;
}

由于计算有效用户数和无效用户数是口述了下逻辑,这边就不给小伙伴们去展示结果啦,有兴趣的小伙伴们可以自己试试

image-20220925153242280


完结撒花 ???

本来写到这,皮皮虾已经自测通过了,觉得没啥问题了,但我突然又去嘴贱的问了下产品姐姐

皮皮虾:产品姐姐,那上传的时候,如果是传uid的excel都从第一列第二行开始,传email的excel都从第二列第二行开始可以不?

产品姐姐:为啥要这样嘞?都从第一列第二行开始不好嘛,我觉得统一一点比较好,不然上传的时候有问题,大家都以为是bug。

image-20220925153802200

前面已经跟大家提过了,EasyExcel每一行都认为是一个对象,我们在字段上添加@ExcelProperty(index = 0)注解时,已经表明了这个字段是第几列的数据...,现在都搞到第一列,本质上还一个是String一个是Long

image-20220925154326053

不过想想,人家产品说的也有道理,于是我礼貌的回复了下:好的啊~


如何解决?

看者眼前的聊天框,我陷入的沉思..,艹,大不了我写两套代码,能实现需求就行

一段狂敲代码过后,原本的一个实体类一个监听器,变成了两个实体类两个监听器,解析exel逻辑里也充满了if-else...

测试过后也没发现有啥问题,十分的nice

image-20220925155109928

虽然功能实现了,但是看着眼前屎山般的代码,我恨不得抽自己两巴掌,这写的什么玩意😭。

于是,我开始思索解决之道,虽然@ExcelProperty(index = 0)限定了该字段是哪一列的数据,但是我可以反射修改啊哈哈,这样不就行了嘛,但转念一想,这玩意不是监听器,不是每次都new的一个新的出来,那如果在并发上传的时候,我要是动态修改了字段对应的列,那岂不是完蛋......

这也不行,那也不行,搜Google也没有个文章能给个解决方案,没办法的我只能继续去看看官方文档

于是,我就发现了下面这个不创建对象的读

image-20220925155732237

之前解析excel,我们都是通过创建对象来做与excel行的映射,对象中固定了字段与列的对应,此方法满足不了我的需求

那么这个不创建对象的读,是怎么实现的,该怎么去理解呢?

AnalysisEventListener<Map<Integer, String>>,在不创建对象的情况下,泛型类型传递的是Map<Integer, String>

那这是如何与Excel对应的?我也没发现官方文档中的解释

搜索了一圈,网上的波客文章跟官方文档的一摸一样,我也是醉了,还得得靠自己~

快速修改了代码,启动项目进行debug测试

image-20220925161157302

第一次

image-20220925161115454

第二次

image-20220925161131769

通过Debug测试,我们也是能快速理解,这种不创建对象的读,采用了Map<Integer, String>来代替了我们自定义的对象,

整个map就是当前行的数据,key就是列数,value就是列对应的值

通过这种方式完全可以满足此次的需求,我们们只需要get(0)即可,通过type来区分是uid还是email

public void invoke(Map<Integer, String> data, AnalysisContext context) {
    String userInfo = data.get(0);
    if (type.equals(0)) {
        uidList.add(Long.valueOf(userInfo));
        return;
    }
    emailList.add(userInfo);
}

后话

虽然这个功能比较简单,但是实现过程中也能暴露出我们许多问题,首先在于沟通,我们不能直接想当然的开发,功能开发之前要与产品沟通好,不然写了半天发现与产品想要结果存在出入,等于白写,其次是一定要对自己的代码有所要求,不能只是完成需求就行,这样对自己是没有提升的,总之一句话,逆水行舟,不进则退!

我是 皮皮虾 ,会在以后的日子里跟大家一起学习,一起进步!

觉得文章不错的话,可以在 掘金 关注我,或者是我的公众号——JavaCodes