Skip to content

解决导出excel含图片写的图片越多,占用内存越大,导致频繁GC,甚至OOM

License

Notifications You must be signed in to change notification settings

mwk719/excel-batch-picture-support

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Excel支持大量图片导出

Build Status codecov

背景

用户在导出统计数据时需要导出大量图片.目前用的比较多的poi导出能支持批量导出大量数据(不包括自媒体).但是当需要导出大量图片时,即使设置了flushSize ,但是对于图片对象却没有效果,图片在内存中无法释放,写的图片越多,占用内存越大,导致频繁GC,甚至OOM

思路

excel文件由声明,表数据,单元格数据,媒体文件等等组件组成, 这些组件分别对应了不同的数据单元.只要把数据分别写入对应的组件,最后构建成一个需要的excel文件.

原理

生成excel时先将读取到的图片缓存在磁盘上,然后逐步将磁盘上的图片写入到excel中。不像poi导出图片时它是把所有的图片数据放在内存里,然后写入到excel中。

功能

采用流式方法写入文件,不会导致内存堆积而占用太多系统资源,有效避免频繁GC问题

  1. 支持自动合并单元格
  2. 使用流式处理,支持大量图片导出
  3. 支持注解导出,在实体上添加注解,自动生成标题

测试

经测试,可以生成几个G的文件。(保证生成的文件没问题,文件是否能打开由使用者计算机决定)

问题反馈

快速使用

Maven导入

在项目的pom.xml的dependencies中加入以下内容: 点击查看最新版本 ${excel-x.version}

<dependency>
    <groupId>top.minwk</groupId>
    <artifactId>excel-x</artifactId>
    <version>${excel-x.version}</version>
</dependency>

示例

具体功能使用示例可见版本更迭

  • 注解使用示例代码

    @GetMapping("/export/lastversion/{row}")
    public void exportLastVersion(HttpServletResponse response, @PathVariable int row) throws IOException {
         /*
         操作窗口
         当写入excel数据行数大于flushSize时{@link Sheet.SheetHandler#createRow(int)},
         会刷新数据到流,调用该方法
         {@link  com.ibiz.excel.picture.support.flush.DrawingXmlRelsHandler#copyPictureAppendDrawingRelsXML(Sheet, Picture)}
         将图片刷新在磁盘中
         不会占用内存空间
         flushSize = -1 时不刷新流
         */
         Workbook workBook = Workbook.getInstance(1);
         Sheet sheet = workBook.createSheet("测试");
         // 给标题行加上背景色,加颜色时,会对字体加粗
         sheet.addCellStyle(new CellStyle(0, "66cc66"));
         List<UserPicture> list = new ArrayList<>();
         UserPicture userPicture;
         for (int r = 0; r < row; r++) {
             userPicture = new UserPicture();
             userPicture.setAge(15);
             userPicture.setName("测试-" + r);
             // 导出本地单张图片
             userPicture.setPicture("E:\\test\\img\\1.jpg");
             // 导出url单张图片
             userPicture.setHeaderPicture("https://portrait.gitee.com/uploads/avatars/user/552/1657608_mwk719_1641537497.png");
             // 导出本地图片集合
             userPicture.setPictures(Arrays.asList("E:\\test\\img\\1.jpg","E:\\test\\img\\2.jpg"));
             // 导出url图片集合
             userPicture.setUrlPictures(Arrays.asList("https://portrait.gitee.com/uploads/avatars/user/552/1657608_mwk719_1641537497.png",
                     "https://img2.baidu.com/it/u=2602880481,728201544&fm=26&fmt=auto"));
             list.add(userPicture);
         }
         sheet.write(UserPicture.class).createRow(list);
         WebUtil.writeExcel(workBook, "最新使用示例代码导出".concat(String.valueOf(System.currentTimeMillis())).concat(".xlsx"), response);
     }
    /**
     * @auther 喻场
     * @date 2020/7/813:41
     */
    public class UserPicture {
    
        public UserPicture() {
        }
    
        @ExportModel( sort = 0, title = "姓名")
        private String name;
        @ExportModel(sort = 1, title = "年龄")
        private Integer age;
        @ExportModel(sort = 3, title = "部门")
        private String department;
        @ExportModel(sort = 2, isPicture = true, title = "图片1")
        private String picture;
        @ExportModel(sort = 4, isPicture = true, title = "图片2")
        private String headerPicture;
        @ExportModel(sort = 5, isPicture = true, title = "多图片")
        private List<String> pictures;
        @ExportModel(sort = 6, isPicture = true, title = "url多图片")
        private List<String> urlPictures;
    
        public UserPicture(String name, Integer age, String department, String picture) {
            this.name = name;
            this.age = age;
            this.department = department;
            this.picture = picture;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    
        public String getDepartment() {
            return department;
        }
    
        public void setDepartment(String department) {
            this.department = department;
        }
    
        public String getPicture() {
            return picture;
        }
    
        public void setPicture(String picture) {
            this.picture = picture;
        }
    
        public String getHeaderPicture() {
            return headerPicture;
        }
    
        public void setHeaderPicture(String headerPicture) {
            this.headerPicture = headerPicture;
        }
    
        public List<String> getPictures() {
            return pictures;
        }
    
        public void setPictures(List<String> pictures) {
            this.pictures = pictures;
        }
    
        public List<String> getUrlPictures() {
            return urlPictures;
        }
    
        public void setUrlPictures(List<String> urlPictures) {
            this.urlPictures = urlPictures;
        }
    }
  • 动态配置表头excel导出示例代码

    实体对象需要实现BizExcelPojoInterface接口

    @GetMapping("/export/dynamic-config-header")
        public void exportDynamicConfigHeader(HttpServletResponse response) throws IOException {
            // 模拟需要导出的数据集合
            List<Student> students = new ArrayList<>();
            students.add(new Student("李四", 16, null, null, 0));
            students.add(new Student("张三", 17, null,
                    Arrays.asList("https://portrait.gitee.com/uploads/avatars/user/552/1657608_mwk719_1641537497.png",
                    "https://img2.baidu.com/it/u=2602880481,728201544&fm=26&fmt=auto"), 1));
            students.add(new Student("王五", 15, IMG_PATH_1, null, 2));
    
            // 配置导出excel的表头、顺序、对应导出的数据集合的字段、是否是图片、单元格宽度等
            List<BizExcelRel> excels = new ArrayList<>();
            excels.add(new BizExcelRel("姓名", "name", 2));
            excels.add(new BizExcelRel("年龄", "age", 3));
            excels.add(new BizExcelRel("表现", "performance", 4));
            excels.add(new BizExcelRel("头像", "headPicture", 5, true, 20));
            excels.add(new BizExcelRel("相册", "album", 6, true));
    
            // 创建excel
            Workbook workBook = Workbook.getInstance(100);
            Sheet sheet = workBook.createSheet("测试");
            // 创建样式
            CellStyle cellStyle = new CellStyle(0, "F0F0F0");
            // 创建数据字典
            Map<String, String> performanceMap = new HashMap<>(3);
            performanceMap.put("0", "一般");
            performanceMap.put("1", "良好");
            performanceMap.put("2", "优秀");
    
            // 构建sheet
            ExcelTableProcessor.sheet(sheet)
                    // 添加样式
                    .addCellStyle(cellStyle)
                    // 添加对应属性字段的数据字典
                    .registryEnumMap("performance", performanceMap)
                    // 构建excel
                    .buildExcel(excels, students);
            WebUtil.writeExcel(workBook, "ExportExampleDynamicConfigHeader".concat(String.valueOf(System.currentTimeMillis())).concat(".xlsx"), response);
        }
    public class Student implements BizExcelPojoInterface {
    
        public Student(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
    
        public Student(String name, Integer age, String headPicture) {
            this.name = name;
            this.age = age;
            this.headPicture = headPicture;
        }
    
        public Student(String name, Integer age, String headPicture, List<String> album, Integer performance) {
            this.name = name;
            this.age = age;
            this.headPicture = headPicture;
            this.album = album;
            this.performance = performance;
        }
    
        private String name;
    
        private Integer age;
    
        private String headPicture;
    
        /**
         * 相册
         */
        private List<String> album;
    
        /**
         * 表现 0一般;1良好;2优秀
         */
        private Integer performance;
    
        public Integer getPerformance() {
            return performance;
        }
    
        public void setPerformance(Integer performance) {
            this.performance = performance;
        }
    
        public List<String> getAlbum() {
            return album;
        }
    
        public void setAlbum(List<String> album) {
            this.album = album;
        }
    
        public String getHeadPicture() {
            return headPicture;
        }
    
        public void setHeadPicture(String headPicture) {
            this.headPicture = headPicture;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    }
  • excel含图片导出demo地址,具体使用以后缀最新日期为准,其他示例仅供测试

  • 微云-6767张图片共800mb资源.rar 可用于测试

  • 项目中导出下载excel使用示例

项目中测试使用

  1. 设置项目jvm堆栈大小都是20m

    -Xms20m -Xmx20m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\log\springlearn.hprof
  2. 复制上方 【最新使用示例代码】到项目中

  3. 找一堆图片随机添加到UserPicture中

  4. 导出一个5000条的记录,在最大堆栈占用为20m的情况下,导出excel大小为700m,未发生内存溢出情况

版本更迭

点击可跳转链接可以查看功能使用示例

2.4.3(2023.07.20)

  • 修复单元格合并错误

2.4.2(2023.07.17)

2.4.1(2023.06.06)

2.4.0(2023.01.12)

2.3.1(2022.12.19)

  • 修复渲染生成列元素A-Z坐标时,使用到Z时,下一列从AA开始
  • 修复行元素为空时合并单元格错误

2.3.0(2022.02.23)

2.2.2(2022.02.08)

  • 修复合并后的单元格没有边框线

2.2.1(2022.01.28)

2.2.0(2022.01.27)

2.1.0(2022.01.14)

2.0.0(2021.12.30)

1.0.4(2021.12.08)

  • 添加使用注解导出含图片或文本的使用示例
  • 修复图片遮挡所在单元格边框线
  • 修复f使用注解导出图片所在下边框不是加粗实线

1.0.3(2021.02.26)

  • 简化使用示例
  • 修复flushSize = -1 时不刷新流
  • 修复其他未知问题

1.0.2(2021.01.26)

  • 修复MD5时未关闭流

1.0.1(2021.01.23)

  • 添加合并单元列值
  • 添加设置单元格背景色
  • 添加可自定义单元格宽度
  • 添加设置字体,目前有默认字体
  • 修复office打开提示需修复的问题

组件介绍

EXCEL由几大组件构建而成

具体部分:

在这里插入图片描述

工作文件实例

  1. workbook代表一个excel工作文件
  2. Sheet对应文件中多个sheet页
  3. Row 为sheet页中的行数据,包含多个Cell单元格
  4. Cell 具体单元格数据
  5. MergeCell 合并单元格信息,指定了合并起始行和结束行,起始列和结束列
  6. Picture 为图片信息,一个Sheet有n个Picture

在这里插入图片描述

注解 在这里插入图片描述

需要导出的字段使用ExportModel注解,程序会自动识别标题,合并单元格,插入图片

/**
 * 导出模型
 * 使用该注解程序会根据相应属性对单元格做设置
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface ExportModel {
    /**排序*/
    int sort() default 0;
    /**是否是图片*/
    boolean isPicture() default false;
    /**表头*/
    String title() default "";
    /**已这列为准进行合并列*/
    boolean mergeMaster() default false;
    /**这一列是否要合并*/
    boolean merge() default false;
}

创建一个工作文件,需要经过一个工作周期,分别对应init(初始化) , write(写数据), close(关闭)

init 初始化各个组件,构建流并写入组件头部信息 write 写入内容 close 写入剩余内存中的数据到流, 写入组件尾部信息,关闭工作文件

init, write, close分别对应了三种事件,程序使用事件驱动机制,当监听器监听到组件在执行对应注册事件时分别进行相应操作

事件

在这里插入图片描述

监听器

在这里插入图片描述

部分代码

public interface WorkbookEvent<E extends ContentListener> {
    WorkbookEvent registry(E listener);
    void onEvent(Sheet sheet);
}
public class InitListener extends AbstractContentListener {
    @Override
    public void invoke(Sheet sheet) {
        repositories.forEach(r -> r.write(sheet));
    }
}

触发init事件,最后一行

void init() {
            sheetContext = SheetContext.getInstance(Sheet.this);
            ContentListener init = ListenerFactory.getInstance(InitListener.class);
            ContentListener flush = ListenerFactory.getInstance(FlushListener.class);
            ContentListener close = ListenerFactory.getInstance(CloseListener.class);
            sheetContext.getRepositoryHolder().forEach((alias, rep) -> {
                if (!closeAlias.contains(alias)) {
                    //app.xml workbook.xml最后写,考虑getSheetName
                    init.addRepository(rep);
                    flush.addRepository(rep);
                }
                close.addRepository(rep);
            });
            sheetContext.getEvents().add(EventFactory.getInstance(InitEvent.class).registry(init));
            sheetContext.getEvents().add(EventFactory.getInstance(FlushEvent.class).registry(flush));
            sheetContext.getEvents().add(EventFactory.getInstance(CloseEvent.class).registry(close));
            //创建组件文件
            sheetContext.getEvents().stream().filter(e -> e instanceof InitEvent).forEach(e -> e.onEvent(Sheet.this));
        }

Write方法很简单,只是把内存中的缓存写入流。

public void write(Sheet sheet) {
        try {
            if (null == write) {
                return;
            }
            writeToStream();
        } catch (Exception e) {
            throw new RuntimeException("写文件异常 :" + file.getAbsolutePath(), e);
        }
    }

    private void writeToStream() {
        try {
            if (!hasWriteXmlHead && writeXmlHead) {
                write.write(AutoXmlHeadEndContent.XML_HEAD);
                hasWriteXmlHead = true;
            }
            write.write(content.toString());
            clearContent();
        } catch (IOException e) {
            throw new RuntimeException("写文件异常 :" + file.getAbsolutePath(), e);
        }
    }

在写入流之前,需要把写入的数据先写入缓存中,这里使用代理。部分代码:

public class Sheet1Handler implements InvocationHandler {
    private IRepository target;
    public Sheet1Handler(IRepository proxy) {
        this.target = proxy;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Sheet sheet = (Sheet)args[0];
        if (method.getName().equals("write")) {
            List<Row> rows = sheet.getRows();
            if (!rows.isEmpty()) {
                //未刷新过说明没有写入过流,这里主要为了写表头
                //如果写过了,则从脚标1开始,原因是为了对比合并单元格在row1中保存上一次刷新的最后一条数据
                int subIndex = !sheet.hasFlush() ? 0 : 1;
                setMergeCell(sheet, rows);
                rows.subList(subIndex, rows.size()).stream().forEach(r -> 	          writeSheetXML(r));
            }
        } else if (method.getName().equals("close")) {
            setEndSheetData();
            setMergeContent(sheet);
        }
        return method.invoke(target, args);
    }
}

About

解决导出excel含图片写的图片越多,占用内存越大,导致频繁GC,甚至OOM

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Languages