Excel 文件的生成与下载

一、Apache 开源框架 poi、jxl 的缺陷

两者都存在生成 excel 文件不够简单优雅快速的问题。而且它们都还存在一个严重的问题,那就是非常耗内存,严重时会导致内存溢出。POI 虽然目前来说,是 excel 解析框架中被使用最广泛的,但这个框架并不完美。为什么这么说呢?

开发者们大部分使用 POI,都是使用其 userModel 模式。而 userModel 的好处是上手容易使用简单,随便拷贝个代码跑一下,剩下就是写业务转换了,虽然转换也要写上百行代码,但是还是可控的。

然而 userModel 模式最大的问题是在于,对内存消耗非常大,一个几兆的文件解析甚至要用掉上百兆的内存。现实情况是,很多应用现在都在采用这种模式,之所以还正常在跑是因为并发不大,并发上来后,一定会OOM或者频繁的FULL GC

二、阿里的 EasyExcel

官方对其的简介是:快速、简单避免OOM的java处理Excel工具。

三、EasyExcel 解决了什么

  • 传统 Excel 框架,如 Apache poi、jxl 都存在内存溢出的问题;
  • 传统 excel 开源框架使用复杂、繁琐;
  • EasyExcel 底层还是使用的 poi,但是做了很多优化,比如修复了并发情况下的一些 bug,具体修复细节,可阅读官方文档 https://github.com/alibaba/easyexcel

四、pom 依赖

<!--alibaba easyexcel-->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>easyexcel</artifactId>
  <version>1.1.2-beta5</version>
</dependency>

五、代码示例

@Test
public void writeExcel() throws Exception {
    
    

    // 文件输出位置
    OutputStream out = new FileOutputStream("d:/test.xlsx");
    ExcelWriter writer = EasyExcelFactory.getWriter(out);

    // 写仅有一个 Sheet 的 Excel 文件, 此场景较为通用
    Sheet sheet = new Sheet(1, 0, WriteModel.class);

    // 第一个 sheet 名称
    sheet.setSheetName("first sheet");

    // 写数据到 Writer 上下文中
    // 入参1: 创建要写入的模型数据
    // 入参2: 要写入的目标 sheet
    writer.write(createModelList(), sheet);

    // 将上下文中的最终 outputStream 写入到指定文件中
    writer.finish();
    // 关闭流
    out.close();
}

上面这段示例代码中,有两点很重要:
①WriteModel 这个对象就是要写入 Excel 的数据模型对象(表头 head,以及每个单元格内的数据顺序下文说明)
②创建需要写入的数据集,(正常业务中,这块都是从数据库中查询出来的)

1️⃣createModelList

private List<WriteModel> createModelList() {
    
    

    List<WriteModel> writeModels = new ArrayList<>();
     for (int i = 0; i < 100; i++) {
    
    
        WriteModel writeModel = WriteModel.builder()
                .name("test" + i).password("123456")
                .age(i + 1).build();
        writeModels.add(writeModel);
    }
    return writeModels;
}

2️⃣WriteModel

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.metadata.BaseRowModel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class WriteModel extends BaseRowModel {
    
    

    @ExcelProperty(value = "姓名", index = 0)
    private String name;

    @ExcelProperty(value = "密码", index = 1)
    private String password;

    @ExcelProperty(value = "年龄", index = 2)
    private Integer age;
}

ExayExcel 提供注解的方式,来方便的定义 Excel 需要的数据模型:
①首先,定义的写入模型必须要继承自 BaseRowModel.java;
②通过 @ExcelProperty 注解来指定每个字段的列名称,以及下标位置

六、️动态生成 Excel 内容

上面的例子是基于注解的,也就是说表头 head以及内容都是写死的,换句话说,定义好了一个数据模型,那么,生成的 Excel 文件也就是只能遵循这种模型来了。但是,实际业务中可能会存在动态变化的需求,要怎么做?

   @Test
    public void writeExcel() throws Exception {
    
    

        // 文件输出位置
        OutputStream out = new FileOutputStream("d:/dynamic.xlsx");
        ExcelWriter writer = EasyExcelFactory.getWriter(out);

        // 动态添加表头,适用一些表头动态变化的场景
        Sheet sheet = new Sheet(1, 0);

        sheet.setSheetName("first sheet");

        // 创建一个表格,用于 Sheet 中使用
        Table table = new Table(1);
        //自定义表格样式
        table.setTableStyle(createTableStyle());
        // 无注解的模式,动态添加表头
        table.setHead(createTestListStringHead());
        // 写数据
        writer.write1(createDynamicModelList(), sheet, table);

        //可以通过 merge()方法来合并单元格
        //注意下标是从 0 开始的,也就是说合并了第六行到第七行,其中的第一列到第五列
        writer.merge(5, 6, 0, 4);

        // 将上下文中的最终 outputStream 写入到指定文件中
        writer.finish();

        // 关闭流
        out.close();
    }

1️⃣无注解模式,动态添加表头,也可自由组合复杂表头,代码如下:

 public static List<List<String>> createTestListStringHead() {
    
    
        // 模型上没有注解,表头数据动态传入
        List<List<String>> head = new ArrayList<List<String>>();
        List<String> headCoulumn1 = new ArrayList<String>();
        List<String> headCoulumn2 = new ArrayList<String>();
        List<String> headCoulumn3 = new ArrayList<String>();
        List<String> headCoulumn4 = new ArrayList<String>();
        List<String> headCoulumn5 = new ArrayList<String>();

        headCoulumn1.add("第一列");
        headCoulumn1.add("第一列");
        headCoulumn1.add("第一列");
        headCoulumn2.add("第一列");
        headCoulumn2.add("第一列");
        headCoulumn2.add("第一列");

        headCoulumn3.add("第二列");
        headCoulumn3.add("第二列");
        headCoulumn3.add("第二列");
        headCoulumn4.add("第三列");
        headCoulumn4.add("第三列2");
        headCoulumn4.add("第三列2");
        headCoulumn5.add("第四列");
        headCoulumn5.add("第4列");
        headCoulumn5.add("第5列");

        head.add(headCoulumn1);
        head.add(headCoulumn2);
        head.add(headCoulumn3);
        head.add(headCoulumn4);
        head.add(headCoulumn5);
        return head;
    }

2️⃣创建动态数据,注意这里的数据类型是 Object:

 private List<List<Object>> createDynamicModelList() {
    
    
        //所有行数据
        List<List<Object>> rows = new ArrayList<>();

        for (int i = 0; i < 100; i++) {
    
    
            //一行数据
            List<Object> row = new ArrayList<>();
            row.add("第" + i+"行");
            row.add(Long.valueOf(187837834L + i));
            row.add(Integer.valueOf(2233 + i));
            row.add("NMB");
            row.add("CBF");
            rows.add(row);
        }
        return rows;
    }

3️⃣自定义表头以及内容样式

public static TableStyle createTableStyle() {
    
    
        TableStyle tableStyle = new TableStyle();
        // 设置表头样式
        Font headFont = new Font();
        // 字体是否加粗
        headFont.setBold(true);
        // 字体大小
        headFont.setFontHeightInPoints((short) 12);
        // 字体
        headFont.setFontName("楷体");
        tableStyle.setTableHeadFont(headFont);
        // 背景色
        tableStyle.setTableHeadBackGroundColor(IndexedColors.BLUE);

        // 设置表格主体样式
        Font contentFont = new Font();
        contentFont.setBold(true);
        contentFont.setFontHeightInPoints((short) 12);
        contentFont.setFontName("黑体");
        tableStyle.setTableContentFont(contentFont);
        tableStyle.setTableContentBackGroundColor(IndexedColors.GREEN);
        return tableStyle;
    }

七、 自定义处理

对于更复杂的处理,EasyExcel 预留了 WriterHandler 接口来,允许你自定义处理代码:

接口中定义了三个方法:

  • sheet(): 在创建每个 sheet 后自定义业务逻辑处理;
  • row(): 在创建每个 row 后自定义业务逻辑处理;
  • cell(): 在创建每个 cell 后自定义业务逻辑处理;

我们实现了该接口后,编写自定义逻辑处理代码,然后调用 getWriterWithTempAndHandler() 静态方法获取 ExcelWriter 对象时,传入 WriterHandler 的实现类即可。

例:

ExcelWriter writer = EasyExcelFactory
.getWriterWithTempAndHandler(null, out, ExcelTypeEnum.XLSX, true, new MyWriterHandler());

八、Web 下载示例代码

public class Down {
    
    
    @GetMapping("/a.htm")
    public void cooperation(HttpServletRequest request, HttpServletResponse response) {
    
    
        ServletOutputStream out = response.getOutputStream();
        ExcelWriter writer = new ExcelWriter(out, ExcelTypeEnum.XLSX, true);
        String fileName = new String(("UserInfo " + new SimpleDateFormat("yyyy-MM-dd").format(new Date()))
                .getBytes(), "UTF-8");
        Sheet sheet1 = new Sheet(1, 0);
        sheet1.setSheetName("第一个sheet");
        writer.write0(getListString(), sheet1);
        writer.finish();
        response.setContentType("multipart/form-data");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-disposition", "attachment;filename="+fileName+".xlsx");
        out.flush();
        }
    }

九、需要注意的点

1️⃣写入大数据时,需分片。比如说,从数据库中查询出数据量较大时,需要在业务层做分片处理,也就是需要分多次查询,再写入,防止内存溢出 OOM。

2️⃣Excel 最大行数问题。
Excel 03、07 版本均有行数、列数的限制:

版本	            最大行	           最大列
Excel 2003	      65536	            256
Excel 2007	      1048576	        16384

csv 由于是文本文件,实际上没有最大行数的限制,但是用 Excel 客户端打开还是多了不显示。也就是说,如果想写入更多的行数是不行的,强行这么做,程序会报类似如下异常:
Invalid row number (1048576) outside allowable range (0..1048575)

如何解决呢?
分多个 Excel 文件写入;
同一个 Excel 文件,分多个 Sheet 写入。

猜你喜欢

转载自blog.csdn.net/ChineseSoftware/article/details/123814045
今日推荐