EasyExcel的使用学习

EasyExcel简介:

Java 程序员在项目上一般会经常遇到解析数据、生成Excel的需求,Java领域解析、生成Excel比较有名的框架有Apache POI、jxl等。但它们都存在一个严重的问题就是非常的耗内存。如果你的系统并发量不大的话可能还行,但是一旦并发上来后一定会OOM或者JVM频繁的full gc。比较流行的就是Apache poi框架有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。esayExcel在大数据量的时候是一行一行的解析,不同于POI的一次性解析,这样避免了内存的溢出。

EasyExcel是阿里巴巴开源的一个excel处理框架,是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。
github地址:https://github.com/alibaba/easyexcel

首先介绍一下什么是OOM?

oom就是我们常说的内存溢出,它是指需要的内存空间大于系统分配的内存空间,oom后果就是项目程序crash;

常见造成oom的原因?

内存泄漏造成;加载的文件或者图片过大造成;

解决方案:

内存泄漏是造成内存溢出的一个原因,所以避免内存泄漏的那些方法都适用与内存溢出,比如:及时回收无用的引用对象,资源回收等......

对于图片方面,如果加载过大的图片要将图片转出bitmap要压缩,异步加载图片(需要的图片或者说页面要显示的图片先加载出来),在listview中consView和Viewholder 一起使用

想具体了解OOM,可以具体了解:常见9种 OOM 原因及解决方案

EasyExcel的特点和原理:

64M内存1分钟内读取75M(46W行25列)的Excel

当然还有急速模式能更快,但是内存占用会在100M多一点

img

easyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。

下图是easyExcel和POI在解析Excel时的对比图。

easyExcel采用一行一行的解析模式,并将一行的解析结果以观察者的模式通知处理(AnalysisEventListener)。

快速使用指南:

1.maven:


        <!--easyexcel-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.2.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.4</version>
        </dependency>

2. 注解

ExcelProperty    指定当前字段对应excel中的那一列。可以根据名字或者Index去匹配。不写,默认按顺序匹配
        value     名头名称
        index    字段与列数对应索引
        converter    字段转化器
ExcelIgnore    默认所有字段都会和excel去匹配,加了这个注解会忽略该字段
DateTimeFormat    日期转换,用String去接收excel日期格式的数据会调用这个注解。
        value  SimpleDateFormat格式方式 y(年) M(月 )d(日) h(时) m(分) s(秒)
        use1904windowing 是否使用1904windowing
NumberFormat    数字转换,用String去接收excel数字格式的数据会调用这个注解。
       value    DecimalFormat格式方式 ##.00
       roundingMode    取舍方式
ExcelIgnoreUnannotated    默认不加ExcelProperty 的注解的都会参与读写,加了不会参与
ColumnWidth    设置宽
        value    数值
ContentRowHeight    设置高
        value    数值
HeadRowHeight    设置头高
        value    数值

3.文件上传读取Excel

示例代码:

DEMO代码地址:https://github.com/alibaba/easyexcel/blob/master/src/test/java/com/alibaba/easyexcel/test/demo/read/ReadTest.java

最简单的web中的读:

excel示例:

step1:建立读取excel存取数据的对象


@Data
public class DemoData {

    @ExcelProperty("测评人身份证号")
    private String assessorId;

    @ExcelProperty("被测评人身份证号")
    private String assesseeId;

    @ExcelProperty("测评评价")
    private String chkResult;

    @ExcelProperty("测评成绩")
    private String chkGrade;

    @ExcelIgnore
    private String ignore;


}

step2:监听器,用于监听excel中的每一行数据


@Component
@Scope("prototype")
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
public class DemoDataListener extends AnalysisEventListener<DemoData> {
    private static final Logger LOGGER = LoggerFactory.getLogger(DemoDataListener.class);
    /**
     * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 5;
    List<DemoData> list = new ArrayList<DemoData>();
    /**
     * 这个是一个service,也可以是一个dao。当然如果不用存储这个对象没用。
     */
    @Autowired
    private CadreAssessmentService cadreAssessmentService;

    /**
     * 这个每一条数据解析都会来调用
     *
     * @param data
     *            one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context
     */
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));
        list.add(data);
        // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
        if (list.size() >= BATCH_COUNT) {
            cadreAssessmentService.saveData(list);
            // 存储完成清理 list
            list.clear();
        }
    }

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


}

step3:service层,用于处理业务逻辑

public interface CadreAssessmentService {
    void saveData(List<DemoData> list);
}

@Service
public class CadreAssessmentServiceImpl implements CadreAssessmentService {
 
    /**
     *这是dao,用于存储数据到数据库
     */
    @Autowired
    private PerEvaRepository perEvaRepository;
   

    @Override
    public void saveData(List<DemoData> list) {
       
        for (DemoData demoData : list) {
            //数据库对应的表domain实体类
            PersonEvaluateDtl personEvaluateDtl = new PersonEvaluateDtl();
            personEvaluateDtl.setAssessorId(demoData.getAssessorId());
            personEvaluateDtl.setAssesseeId(demoData.getAssesseeId());
            personEvaluateDtl.setChkResult(demoData.getChkResult());
            personEvaluateDtl.setChkGrade(demoData.getChkGrade());
            personEvaluateDtl.setYear(new Date());
            perEvaRepository.saveAll(personEvaluateDtl);
        }
    }

step4:持久层,用于保存数据到数据库

@Repository
public interface PerEvaRepository  extends JpaRepository<PersonEvaluateDtl,Long>, JpaSpecificationExecutor<PersonEvaluateDtl> {

}

controller层代码示例:

    @Autowired
    private DemoDataListener demoDataListener;

    
    /**
     * 最简单的读
     * <p>1. 创建excel对应的实体对象 参照{@link DemoData}
     * <p>2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoDataListener}
     * <p>3. 直接读即可
     */

    @PostMapping(value = "/import")
    public ResponseEntity<Object> simpleRead(MultipartFile uploadFile){
        try {
            EasyExcel.read(uploadFile.getInputStream(), DemoData.class, demoDataListener).sheet().doRead();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

4.文件下载最简单的写:

easyExcel简单的导出示例代码

DEMO代码地址:https://github.com/alibaba/easyexcel/blob/master/src/test/java/com/alibaba/easyexcel/test/demo/write/WriteTest.java

excel示例

img

step1:对象

@Data
public class DemoData {
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
    @ExcelProperty("数字标题")
    private Double doubleData;
    /**
     * 忽略这个字段
     */
    @ExcelIgnore
    private String ignore;
}

step2:TestDemo

 /**
     * 最简单的写
     * <p>1. 创建excel对应的实体对象 参照{@link DemoData}
     * <p>2. 直接写即可
     */
    @GetMapping(value = "/simpleWrite")
    public void simpleWrite(HttpServletResponse response) throws IOException {

        //可直接用浏览器或者用postman
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
        String fileName = URLEncoder.encode("测试", "UTF-8");
        response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
        ServletOutputStream outputStream = response.getOutputStream();
        EasyExcel.write(outputStream, DemoData.class).sheet("模板").doWrite(data());
    }

    //通用数据生成后面不会重复写
    private List<DemoData> data() {
        List<DemoData> list = new ArrayList<DemoData>();
        for (int i = 0; i < 10; i++) {
            DemoData data = new DemoData();
            data.setString("字符串" + i);
            data.setDate(new Date());
            data.setDoubleData(0.56);
            list.add(data);
        }
        return list;
    }

easyExcel多sheet页导出:

excel示例

step1:对象

写一个dto作为模型,在属性的名称加上注解,作为表头显示在表格中。

@ContentRowHeight 单元格行高

@ColumnWidth 单元格列宽

@HeadRowHeight 头的行高

@ExcelProperty输出在表格的字段value值代表输出的值index代表是列数(列数是从0开始的所有第0列也就是第一

@ExcelIgnore不输出在表格中的字段

@Data
@ContentRowHeight(20)
@HeadRowHeight(30)
@ColumnWidth(25)
public class WeightGradeExportResp implements Serializable {
    @ExcelProperty("原因")
    private String reason;

    @ExcelProperty("考评人岗位")
    private String assessorJobId;

    @ExcelProperty("考评人岗位名称")
    private String assessorJobName;

    @ExcelProperty("被考评人岗位")
    private String assesseeJobId;

    @ExcelProperty("被考评人岗位名称")
    private String assesseeJobName;

    @ExcelProperty("权重分值")
    private String weightGrade;

}

step2:TestDemo

   /**
     * 测评统计:考核成绩的导出
     */
    @GetMapping(value = "/downLoadEvaInfos")
    public ResponseEntity<Object> tableWrite(HttpServletResponse response){
        try {
            List<WeightGradeExportResp> weightGradeExportResps = dataList();
            List<WeightGradeExportResp> weightGradeExportResps1 = dataList11();
            response.setContentType("application/vnd.ms-excel");
            response.setCharacterEncoding("utf-8");
            String fileName = URLEncoder.encode("考核统计", "UTF-8");
            response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
            ServletOutputStream outputStream = response.getOutputStream();
            ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
            WriteSheet writeSheet = EasyExcel.writerSheet(0, "考核统计").head(WeightGradeExportResp.class).build();
            WriteSheet writeSheet1 = EasyExcel.writerSheet(1, "考核统计11").head(WeightGradeExportResp.class).build();

            excelWriter.write(weightGradeExportResps,writeSheet);
            excelWriter.write(weightGradeExportResps1,writeSheet1);
            excelWriter.finish();
        } catch (Exception e) {
            response.reset();
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            Map<String, String> map = new HashMap<String, String>();
            map.put("status", "failure");
            map.put("message", "下载文件失败" + e.getMessage());
            try {
                response.getWriter().println(JSON.toJSONString(map));
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
            return new ResponseEntity<>(map,HttpStatus.OK);
        }
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
    

    private List<WeightGradeExportResp> dataList() {

        List<WeightGradeExportResp> list = new ArrayList<WeightGradeExportResp>();

        for (int i = 0; i < 10; i++) {
            WeightGradeExportResp weightGradeExportResp = new WeightGradeExportResp();
            weightGradeExportResp.setReason(i+"hah");
            weightGradeExportResp.setAssesseeJobId(String.valueOf(i));
            weightGradeExportResp.setAssesseeJobName(i+"assesseeJobName");
            weightGradeExportResp.setAssessorJobId(String.valueOf(i+1));
            weightGradeExportResp.setAssessorJobName(i+"assessorJoobName");
            weightGradeExportResp.setWeightGrade(String.valueOf(i));
            list.add(weightGradeExportResp);
        }

        return list;
    }

    private List<WeightGradeExportResp> dataList11() {

        List<WeightGradeExportResp> list = new ArrayList<WeightGradeExportResp>();

        for (int i = 0; i < 100; i++) {
            WeightGradeExportResp weightGradeExportResp = new WeightGradeExportResp();
            weightGradeExportResp.setReason(i + "hah");
            weightGradeExportResp.setAssesseeJobId(String.valueOf(i));
            weightGradeExportResp.setAssesseeJobName(i + "assesseeJobName");
            weightGradeExportResp.setAssessorJobId(String.valueOf(i + 1));
            weightGradeExportResp.setAssessorJobName(i + "assessorJoobName");
            weightGradeExportResp.setWeightGrade(String.valueOf(i));
            list.add(weightGradeExportResp);
        }

        return list;
    }

上面就是excel的模板导出,表头都是固定死的,数据也必须跟模板中的属性对应,所以限制比较大,也不过灵活。

不用模板导出方式:需要自己手写一个方法,然后自己把表头的数据插入进去

 private List<List<String>> dataList11() {
        List<List<String>> list = new ArrayList<List<String>>();

        List<String> evaPerInfoData = new ArrayList<String>();
        evaPerInfoData.add("姓名");
        evaPerInfoData.add("所在部门");
        evaPerInfoData.add("岗位名称");
        evaPerInfoData.add("考评人姓名");
        evaPerInfoData.add("考评人部门");
        evaPerInfoData.add("考评人岗位名称");
        evaPerInfoData.add("权重分值");
        evaPerInfoData.add("考评类别");
        evaPerInfoData.add("考核成绩");
        list.add(evaPerInfoData);

        for (int i = 0; i < 100; i++) {
            List<String> evaPerInfoVosData = new ArrayList<String>();
            evaPerInfoVosData.add(i+"name");
            evaPerInfoVosData.add(i+"branchName");
            evaPerInfoVosData.add(i+"assesseeJobName");
            evaPerInfoVosData.add(i+"assessorName");
            evaPerInfoVosData.add(i+"assessorBranchName");
            evaPerInfoVosData.add(i+"assessorJobName");
            evaPerInfoVosData.add(i+"weightGrade");
            evaPerInfoVosData.add(i+"chkEvaluateType");
            evaPerInfoVosData.add(i+"chkGrade");
            list.add(evaPerInfoVosData);
        }

        List<String> blankData = new ArrayList<String>();
        for (int i = 0; i < 3; i++) {
            list.add(blankData);
        }

        List<String> evaDeptInfoData = new ArrayList<String>();
        evaDeptInfoData.add("考评人岗位名称");
        evaDeptInfoData.add("被考人岗位名称");
        evaDeptInfoData.add("权重分值");
        evaDeptInfoData.add("优秀票数");
        evaDeptInfoData.add("称职票数");
        evaDeptInfoData.add("基本称职票数");
        evaDeptInfoData.add("不称职票数");
        evaDeptInfoData.add("总票数");
        evaDeptInfoData.add("优秀类率");
        evaDeptInfoData.add("称职率");
        evaDeptInfoData.add("基本称职率");
        evaDeptInfoData.add("不称职率");
        list.add(evaDeptInfoData);


        for (int i = 0; i < 100; i++) {
            List<String> evaDeptInfoVosData = new ArrayList<String>();
            evaDeptInfoVosData.add(i+"assessorJobName");
            evaDeptInfoVosData.add(i+"assesseeJobName");
            evaDeptInfoVosData.add(i+"weightGrade");
            evaDeptInfoVosData.add(i+"chkResultA");
            evaDeptInfoVosData.add(i+"chkResultB");
            evaDeptInfoVosData.add(i+"chkResultC");
            evaDeptInfoVosData.add(i+"chkResultD");
            evaDeptInfoVosData.add(i+"chkResultTotal");
            evaDeptInfoVosData.add(i+"proStaA");
            evaDeptInfoVosData.add(i+"proStaB");
            evaDeptInfoVosData.add(i+"proStaC");
            evaDeptInfoVosData.add(i+"proStaD");
            list.add(evaDeptInfoVosData);
        }

        return list;
    }

    然后把 EasyExcel.writerSheet(1, "考核统计11").head(WeightGradeExportResp.class).build();里面的WeightGradeExportResp.class替换成dataList11()方法名就可以了;

WriteSheet writeSheet1 = EasyExcel.writerSheet(1, "考核统计11").head(dataList11() ).build();

切记,head()方法中替换成的方法名,该方法返回值一定是List<List<String>>类型的,因为AbstractParameterBuilder类只有两个head()方法,如果有需求,我觉得可以自己可以仿写:

   /**
     * You can only choose one of the {@link #head(List)} and {@link #head(Class)}
     *
     * @param head
     * @return
     */
    public T head(List<List<String>> head) {
        parameter().setHead(head);
        return self();
    }

    /**
     * You can only choose one of the {@link #head(List)} and {@link #head(Class)}
     *
     * @param clazz
     * @return
     */
    public T head(Class clazz) {
        parameter().setClazz(clazz);
        return self();
    }

在导出Excel的部分,easyExcel还提供了自定义样式,插入表格,插入图片等其他功能,还有一个比较有意思的功能就是Excel模板填充的功能。详细的功能信息参考官方文档:https://www.yuque.com/easyexcel/doc/easyexcel

持续更新中.......

猜你喜欢

转载自blog.csdn.net/LOVE_Me__/article/details/107601655