Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

aStudyMachine/easyexcel-utils

Open more actions menu

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

1. 前言

最近阿里开源的excel读写项目EasyExcel又火了起来 . 原来是项目又开始维护了, 从1.x 更新到2.x 了 , 而且迭代迅速 , 目前已经更新到 2.1.0-beta3 版本.

关于easyexcel , 其主要目的为降低读取excel时的内存损耗 , 简化读写excel的api .

同时2.x版本提供了很多新功能 , 具体大家可以直接参考官方说明吧 , github文档上写一清二楚 , 这里就不传播一些没啥必要的二三手知识了 , 而且目前该项目还在不停地迭代 , 给contributor一个star也是很有必要的 . 😄

官方地址 : easyexcel仓库地址 , easyexcel官网


这里我基于easyexcel 2.0.5版本简单封装了一个web读写excel的工具类 , 主要封装了如下功能 :

  • 通过注解自定义LocalDateTime的读写格式

  • 通过注解自定义枚举类型的读写格式

  • 自定义BaseExcelListener抽象类封装了常用的数据处理逻辑 , 以及补充读取excel过程中读取发生错误被跳过的行号记录 .

  • 封装了web的读写excel操作

  • ...

下面列举主要功能以及相关示例 , 可以直接看源码 , 每个方法都有写完整的注释 , 如果觉得写得还凑合能看的话 , 给我这个刚毕业没多久的小菜鸡点个star呗 😄

附上源码地址 : https://github.com/aStudyMachine/easyexcel-utils

2. 主要功能

2.1 建立excel表每行数据与Java模型的映射

easyexcel读写excel可以基于java 模型的方式 , 也可以使用List<List<String>> 的方式读写excel , 这里我读写操作使用基于java模型的方式 , 通过java类的属性与excel每一列的数据进行对应

关键注解 : @ExcelProperty

具体如何使用注解建立java模型与Excel表数据的映射可以参考 com.luwei.module.easyexcel.pojo下的两个java模型类Order 类与User

/**
 * @author WuKun
 * @since 2019/10/09
 */
@Data
@AllArgsConstructor
@NoArgsConstructor //必须要保证无参构造方法存在,否则会报初始化对象失败
// @Accessors(chain = true) 使用lombok该注解会导致无法正常读取到该数据
public class User {

    /**
     * {@code @ExcelIgnore} 用于标识该字段不用做excel读写过程中的数据转换
     */
    @ExcelIgnore
    private Integer userId;

    /**
     * <pre>
     * {@code @ExcelIgnore} 中的属性 不建议 index 和 name 同时用
     *
     * 要么一个对象统一只用index表示列号,
     * 例如 : {@code @ExcelProperty(index = 0)}
     *
     * 要么一个对象统一只用value去匹配列名
     * 例如 : {@code @ExcelProperty("姓名")}
     *
     * 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据
     * </pre>
     */
    @ExcelProperty("姓名")
    private String name;

    @ExcelProperty("年龄")
    private Integer age;

    @ExcelProperty("地址")
    private String address;

    /**
     * <pre>
     * {@code @EnumFormat} 注解 :
     *  作用 : 与 {@code @ExcelProperty(converter = EnumExcelConverter.class)} 搭配使用
     *         转换java枚举与excel中指定的内容
     *  属性 :
     *   - value : 要转换的枚举类class对象
     *   - fromExcel : 指定excel中用户输入的枚举值的名字的字符串形式,与toJavaEnum中指定的枚举值一一对应
     *                 以下面的示例来说,fromExcel指定的 "男" 对应 toJavaEnum中的 "MAN" ,
     *                 当excel中该列读取到"男" 这个字符串时,会自动转化为枚举{@code GenderEnum.MAN},
     *                 同理在写excel时,如果该字段为{@code GenderEnum.MAN} 时, 写到excel时则转化为 "男"
     *   - toJavaEnum : 如上所述
     *
     *  注意 : fromExcel 与 toJavaEnum 这两个属性必须同时使用, 而且两个属性的字符串的数组长度必须相同,
     *        若两个属性都不指定 , 则默认 枚举值名字符串转化为对应的枚举 例如: "MAN" <--> {@code GenderEnum.MAN}
     * </pre>
     */
    @EnumFormat(value = GenderEnum.class,
            fromExcel = {"男", "女"},
            toJavaEnum = {"MAN", "WOMAN"}) // "男" <--> GenderEnum.MAN ; "女" <--> GenderEnum.WOMAN
    @ExcelProperty(value = "性别", converter = EnumExcelConverter.class)
    private GenderEnum gender;

    /**
     * <pre>
     * {@code @LocalDateTimeFormat} 注解
     *  作用: 与 {@code  @ExcelProperty(converter = LocalDateTimeExcelConverter.class)} 搭配使用,
     *        指定导入导出的时间格式.
     *  属性 :
     *   - value : 日期格式字符串 
     * </pre>
     */
    @ExcelProperty(value = "生日", converter = LocalDateTimeExcelConverter.class)
    @LocalDateTimeFormat("yyyy-MM-dd HH:mm:ss")
    private LocalDateTime birthday;
}

2.2 导出excel

2.2.1 相关API介绍

web导出excel 根据03 / 07版本分为两个不同的方法 ,分别为EasyExcelUtil类中以下两个方法 :

  • 导出03版本 : exportExcel2003Format(EasyExcelParams excelParams)

  • 导出07版本 : exportExcel2007Format(EasyExcelParams excelParams)

EasyExcelParams是使用EasyExcel导出excel需要设置的相关参数 , 包括需要导出的List<T>数据以及对应的Java模型 , 使用时根据实际情况设置相应的参数即可.

/**
 * @author WuKun
 * @since 2019/10/14
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class EasyExcelParams implements Serializable {


    /**
     * excel文件名(不带拓展名)
     */
    private String excelNameWithoutExt;
    /**
     * sheet名称
     */
    private String sheetName;

    /**
     * 数据
     */
    private List data;

    /**
     * 数据模型类型
     */
    private Class dataModelClazz;

    /**
     * 响应
     */
    private HttpServletResponse response;


    public EasyExcelParams() {
    }

    /**
     * 检查不允许为空的属性
     *
     * @return this
     */
    public EasyExcelParams checkValid() {
        Assert.isTrue(ObjectUtils.allNotNull(excelNameWithoutExt, data, dataModelClazz, response), "导出excel参数不合法!");
        return this;
    }
}

2.2.2 导出excel示例

/**
* 使用EasyExcelUtils 导出Excel 2007
*
* @param response HttpServletResponse
* @throws Exception exception
*/
@GetMapping("/easy2007")
public void easy2007(HttpServletResponse response) throws Exception {
    initData();

    //设置参数
    EasyExcelParams params = new EasyExcelParams().setResponse(response)
        .setExcelNameWithoutExt("Order(xlsx)")
        .setSheetName("第一张sheet")
        .setData(data)
        .setDataModelClazz(Order.class)
        .checkValid();

    long begin = System.currentTimeMillis();
    EasyExcelUtil.exportExcel2007(params);
    long end = System.currentTimeMillis();

    log.info("-----EasyExcelUtils : 导出成功,导出excel花费时间为 : " + ((end - begin) / 1000) + "秒");
}

private void initData() {
    if (CollectionUtils.isEmpty(data)) {
        for (int i = 0; i < 60000; i++) {
            Order order = new Order();
            order.setPrice(BigDecimal.valueOf(11.11));
            order.setCreateTime(LocalDateTime.now());
            order.setGoodsName("香蕉");
            order.setOrderId(i);
            order.setNum(11);
            order.setOrderStatus(OrderStatusEnum.PAYED);
            data.add(order);
        }
    }
}

2.3 读取excel

2.3.1 相关API介绍

  • 读取excel时用到的是EasyExcelUtilsreadExcel方法 ;
/**
 * 读取 Excel(支持单个model的多个sheet)
 *
 * @param excel    文件
 * @param rowModel 实体类映射
 * @param listener 用于读取excel的listener
 */
public static void readExcel(MultipartFile excel, Class rowModel, BaseExcelListener listener) {
    ExcelReader reader = getReader(excel, rowModel, listener);
    try {
        Assert.notNull(reader, "导入Excel失败!");
        Integer totalSheetCount = reader.getSheets().size();
        for (Integer i = 0; i < totalSheetCount; i++) {
            reader.read(EasyExcel.readSheet(i).build());
        }
    } finally {
        // 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的
        Optional.ofNullable(reader).ifPresent(ExcelReader::finish);
    }
}
  • easyexcel的读取操作需要自建一个类继承AnalysisEventListener抽象类 , 这里我创建BaseExcelListener类继承并重写读取excel的相关方法 , 每个方法的具体作用可直接查看方法头部注释 , 使用时直接创建一个listener类继承BaseExcelListener即可 , 如果默认的BaseExcelListener不满足需求 , 也可以直接自定义一个Listener 类继承 BaseExcelListener并重写相应方法.
package com.luwei.module.easyexcel.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelAnalysisException;
import com.alibaba.fastjson.JSON;
import com.luwei.module.easyexcel.pojo.ErrRows;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.util.*;

/**
 * @author WuKun
 * @since 2019-10-10
 * <p>
 * 由于在实际中可能会根据不同的业务场景需要的读取到的不同的excel表的数据进行不同操作,
 * 所以这里将ExcelListener作为所有listener的基类,根据读取不同的java模型自定义一个listener类继承ExcelListener,
 * 根据不同的业务场景选择性对以下方法进行重写,具体如com.luwei.listener.OrderListener所示
 * </p>
 *
 * <p>如果默认实现的方法不满足业务,则直接自定义一个listener实现AnalysisEventListener,重写一遍方法即可.</p>
 */
@Slf4j
public abstract class BaseExcelListener<Model> extends AnalysisEventListener<Model> {

    /**
     * 自定义用于暂时存储data。
     * 可以通过实例获取该值
     * 可以指定AnalysisEventListener的泛型来确定List的存储类型
     */
    @Getter
    private List<Model> data = new ArrayList<>();

    /**
     * 每隔N条存执行一次{@link BaseExcelListener#doService()}方法,
     * 如果是入库操作,可使用默认的3000条,然后清理list,方便内存回收
     */
    private int batchCount = 3000;

    /**
     * @param batchCount see batchCount
     * @return this
     * @see BaseExcelListener#batchCount
     */
    public BaseExcelListener batchCount(int batchCount) {
        this.batchCount = batchCount;
        return this;
    }

    /**
     * <p>读取时抛出异常是否继续读取.</p>
     * <p>true:跳过继续读取 , false:停止读取 , 默认true .</p>
     */
    private boolean continueAfterThrowing = true;

    /**
     * 设置抛出解析过程中抛出异常后是否跳过继续读取下一行
     *
     * @param continueAfterThrowing 解析过程中抛出异常后是否跳过继续读取下一行
     * @return this
     */
    public BaseExcelListener continueAfterThrowing(boolean continueAfterThrowing) {
        this.continueAfterThrowing = continueAfterThrowing;
        return this;
    }

    /**
     * 读取过程中发生异常被跳过的行数记录
     * String 为 sheetNo
     * List<Integer> 为 错误的行数列表
     */
    // TODO: 2019/10/28 改为不需要通过Map进行转换
    private Map<String, List<Integer>> errRowsMap = new HashMap<>();

    /**
     * 错误行号的pojo形式
     */
    private List<ErrRows> errRowsList = new ArrayList<>();

    /**
     * 获取错误的行号,以pojo的形式返回
     *
     * @return 错误的行号
     */
    public List<ErrRows> getErrRowsList() {
        errRowsMap.forEach((sheetNo, rows) -> errRowsList.add(new ErrRows().setSheetNo(sheetNo).setErrRows(rows)));
        return errRowsList;
    }

    /**
     * 每解析一行会回调invoke()方法。
     * 如果当前行无数据,该方法不会执行,
     * 也就是说如果导入的的excel表无数据,该方法不会执行,
     * 不需要对上传的Excel表进行数据非空判断
     *
     * @param object  当前读取到的行数据对应的java模型对象
     * @param context 定义了获取读取excel相关属性的方法
     */
    @Override
    public void invoke(Model object, AnalysisContext context) {
        log.info("解析到一条数据:{}", object);

        if (!validateBeforeAddData(object)) {
            throw new ExcelAnalysisException("数据校验不合法!");
        }

        // 数据存储到list,供批量处理,或后续自己业务逻辑处理。
        data.add(object);

        //如果continueAfterThrowing 为false 时保证数据插入的原子性
        if (data.size() >= batchCount && continueAfterThrowing) {
            doService();
            data.clear();
        }
    }

    /**
     * 该方法用于对读取excel过程中对每一行的数据进行校验操作,
     * 如果不需要对每行数据进行校验,则直接返回true即可.
     *
     * @param object 读取到的数据对象
     * @return 校验是否通过 true:通过 ; false:不通过
     */
    public abstract boolean validateBeforeAddData(Model object);

    /**
     * 对暂存数据的业务逻辑方法 .
     * 相关逻辑可以在该方法体内编写, 例如入库.
     */
    public abstract void doService();
//    {
//        log.info("模拟写入数据库");
//        log.info("/*------- {} -------*/", JSON.toJSONString(data));
//        data.clear();
//    }

    /**
     * 解析监听器
     * 每个sheet解析结束会执行该方法
     *
     * @param context 定义了获取读取excel相关属性的方法
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        doService();
        log.info("/*------- 当前sheet读取完毕,sheetNo : {} , 读取错误的行号列表 : {} -------*/",
                getCurrentSheetNo(context), JSON.toJSONString(errRowsMap));
        data.clear();//解析结束销毁不用的资源
    }

    /**
     * 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则继续读取下一行。
     * 如果不重写该方法,默认抛出异常,停止读取
     *
     * @param exception exception
     * @param context   context
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        // 如果continueAfterThrowing为false,则直接将异常抛出
        if (!continueAfterThrowing) {
            throw exception;
        }

        Integer sheetNo = getCurrentSheetNo(context);
        Integer rowIndex = context.readRowHolder().getRowIndex();
        log.error("/*------- 读取发生错误! 错误SheetNo:{},错误行号:{} -------*/ ", sheetNo, rowIndex, exception);

        List<Integer> errRowNumList = errRowsMap.get(String.valueOf(sheetNo));
        if (Objects.isNull(errRowNumList)) {
            errRowNumList = new ArrayList<>();
            errRowNumList.add(rowIndex);
            errRowsMap.put(String.valueOf(sheetNo), errRowNumList);
        } else {
            errRowNumList.add(rowIndex);
        }
    }

    /**
     * 获取当前读取的sheet no
     *
     * @param context 定义了获取读取excel相关属性的方法
     * @return current sheet no
     */
    private Integer getCurrentSheetNo(AnalysisContext context) {
        return context.readSheetHolder().getSheetNo();
    }

}
  • 读取时不区分03或07版本 , 底层会自动判断 ;

2.3.2 读取excel示例

  1. 自定义一个listener类继承BaseExcelListener
package com.luwei.module.easyexcel.listener;

import com.luwei.module.easyexcel.pojo.User;
import lombok.extern.slf4j.Slf4j;

/**
 * @author WuKun
 * @since 2019/10/10
 */
@Slf4j
public class UserListener extends BaseExcelListener<User> {
    /**
     * 这里需要注意入库使用到的Service或者DAO层需要使用到的相关方法时,
     * 不要通过Spring 使用{@code @Autowired}注入,同时该Listener也不要交由Spring IOC进行管理
     * 直接通过构造方法传入相关`xxxService` 或者 `xxxMapper`
     */
    private UserService userService;

    public UserListener(UserService userService) {
        this.userService = userService;
    }
    
    @Override
    void saveData() {
        // 批量插入数据
        userService.saveBatchUsers(this.getData())
        log.info("/*------- 写入数据 -------*/");
    }
}
  1. 调用工具方法
@Autowired
private UserService userService;

/**
* 读取测试
*
* @param excel excel文件
*/
@PostMapping("/readExcel")
public void readExcel(@RequestParam MultipartFile excel) {
     List<ErrRows> errRows = EasyExcelUtil.readExcel(excel, User.class, new UserListener(userService));
     log.info("/*------- 错误的行号数为 :  {}-------*/", JSON.toJSONString(errRows));
}

3. 注意事项

  • java模型必须要保证无参构造方法存在 , 否则会在读写excel时报无法初始化java模型对象的异常

  • 使用java模型读取excel时不能对Java模型使用@Accessors(chain = true)注解, 会导致数据无法转换

  • sheetNo 从 0开始 , 行号不包括表头 , 例如log中打印的是第9行, 实际在excel中对应的是第10行

    2019-10-20 15:34:57.236  INFO 38012 --- [nio-8081-exec-8] c.l.e.listener.BaseExcelListener         : /*------- 当前sheet读取完毕,sheetNo : 1 , 读取错误的行号列表 : {"1":[9]} -------*/

    image.png

About

EasyExcel 简单封装 , 通过修改源码增加更多的model注解支持

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

Morty Proxy This is a proxified and sanitized view of the page, visit original site.