js-xlsx 生成 Excel 文件与 Java POI 库生成 Excel 文件的对比

前言

最近发现有一个很厉害的前端框架 js-xlsx ( npm install xlsx ),可以在浏览器端用纯前端的方式生成 Office 的 Excel 格式文件,无需再依赖后端 POI 等库。特此研究对比一下 js-xlsx 与 Java  POI 方式生成 Excel 文件的效率。

创建对比工程

咱就熟悉 Java 那一套,所以用 Spring-boot 创建一个工程是最合适的。

pom.xml 文件

除了必要的 Spring starter、poi-ooxml 外只添加了 lombok。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.demo</groupId>
    <artifactId>xlsx</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>xlsx</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <poi.version>3.17</poi.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>${poi.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml-schemas</artifactId>
            <version>${poi.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>${poi.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

XlsxApplication.java 文件

Java 端只需要创建一个 @RestController 即可,让 Spring-boot 的启动类 XlsxApplication.java 把这个功能承包了。

创建两个 @requestMapper,一个模拟数据库查询,一个实现 POI 方式 Excel 文件导出功能。

package com.demo.xlsx;

import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

@Slf4j
@RestController
@SpringBootApplication
public class XlsxApplication {

	private final Random random = new Random();

	@RequestMapping("query-items")
	public List<List<String>> qryItems() throws Exception {
		int listSize = 1000;
		int itemSize = 20;
		List<List<String>> result = new ArrayList<>(listSize);
		for (int i = 0; i < listSize; i++) {
			List<String> items = new ArrayList<>(itemSize);
			for (int j = 0; j <= itemSize; j++) {
				items.add("v" + random.nextInt());
			}
			result.add(items);
		}
		Thread.sleep(30L); //模拟一下数据库查询的耗时操作
		return result;
	}

	@RequestMapping("output-excel")
	public void outputExcel(int size, HttpServletResponse response) throws Exception {
		log.info("begin output excel file");
		long time = System.currentTimeMillis();
		response.setContentType("application/octet-stream; charset=utf-8");
		response.setHeader("Content-Disposition", "attachment; filename=Demo_POI.xlsx");
		try (SXSSFWorkbook workbook = new SXSSFWorkbook(2000)) {// 2000 为 excel 内容在内存中的行数
			SXSSFSheet sheet = workbook.createSheet("Sheet1");
			int rowIndex = 0;
			this.appendExcelCell(buildHeader(), sheet.createRow(rowIndex));
			rowIndex++;
			for (int page = 0; page < size; page++) {
				List<List<String>> lists = qryItems();
				for (List<String> items : lists) {
					this.appendExcelCell(items, sheet.createRow(rowIndex));
					rowIndex++;
				}
			}
			workbook.write(response.getOutputStream());
		}
		response.flushBuffer();
		log.info("finish output excel file, cost {}ms", System.currentTimeMillis() - time);
	}

	private void appendExcelCell(List<String> items, SXSSFRow row) {
		for (int cellIndex = 0; cellIndex < items.size(); cellIndex++) {
			row.createCell(cellIndex).setCellValue(items.get(cellIndex));
		}
	}

	private List<String> buildHeader() {
		List<String> heads = new ArrayList<>(20);
		for (int i = 0; i < 20; i++) {
			heads.add("Head-" + i);
		}
		return heads;
	}

	public static void main(String[] args) {
		SpringApplication.run(XlsxApplication.class, args);
	}

}

index.html 文件

既然是前端功能对比,自然少不了一个前端的页面,建立一个 index.html 文件,加载一下 jQuery (用于 Ajax)、xlsx.js,为了样式好看一点,加载了 tw-bootstrap 的 css。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="//cdn.bootcss.com/twitter-bootstrap/4.2.1/css/bootstrap.min.css">
    <title>Demo Page</title>
</head>
<body>
<div class="container">
    <ul>
        <li>最大页数 <input type="number" id="size" value="10"/> (导出行数 = 最大页数 * 1000)</li>
        <li>
            <button type="button" class="btn btn-link" οnclick="backExport()">后端方案导出Excel</button>
        </li>
        <li>
            <button type="button" class="btn btn-link" οnclick="pageExport()">前端方案导出Excel</button>
        </li>
        <li id="info">(消息内容)</li>
    </ul>
</div>
</body>
<script src="//cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="//cdn.bootcss.com/xlsx/0.14.1/shim.min.js"></script>
<script src="//cdn.bootcss.com/xlsx/0.14.1/xlsx.full.min.js"></script>
<script>
  var time = 0;
  var size = 10;
  var info = $('#info');

  function backExport() {
    window.location.href = '/output-excel?size=' + $('#size').val();
  }

  // view-source:https://sheetjs.com/demos/writexlsx.html
  function pageExport() {
    info.text('begin page download');
    time = new Date().getTime();
    size = Number($('#size').val());
    loadItems(buildHeader(), 0, writeExcel);
  }

  function loadItems(aoa, page, finish) {
    info.text('loading page : ' + page);
    if (page === size) return finish(aoa); // 200 * 2000 = 40W
    $.get('/query-items').done(function(resp) {
      loadItems(aoa.concat(resp), ++page, finish);
    });
  }

  function buildHeader() {
    var headers = new Array(20);
    for (var i = 0; i < 20; i++) {
      headers.push('Head-' + i);
    }
    return [headers];
  }

  function writeExcel(aoa) {
    info.text('excel row line: ' + aoa.length);
    var ws = XLSX.utils.aoa_to_sheet(aoa);
    var wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
    var url = XLSX.writeFile(wb, 'Demo_sheetjs.xlsx');
    info.text('finish page download, cost ' + (new Date().getTime() - time) + 'ms');
    url && URL && URL.revokeObjectURL && URL.revokeObjectURL(url); // 注意执行这一步来释放资源
  }
</script>
</html>

这样子基本上的功能已经齐全了。

启动项目

直接在 IDE 中运行 com.demo.xlsx.XlsxApplication 即可,自然为了避免内存过小影响测试数据,添加了一下 jvm 启动参数 -Xmx2048m -Xms1024m,启动后的页面显示如下(样子超丑,就两个按钮)

后端 POI 方案测试

首先起一下 Java VisualVM 监控一下 com.demo.xlsx.XlsxApplication,然后点击页面上的 “后端方案导出 Excel” 按钮,POI 的性能还是非常不错的,几次测试调整了一下 Excel 内容的行数,即使行数到 90W 行也没啥问题,就是堆内存消耗大概涨到了 860MB,图示为 25W行数据导出时的监控截图(为啥是 25W 行,后面会讲到):

在 GC 后,点击按钮前的内存消耗:

导出 25W 行数据后 Excel 文件大小近 228MB,运行耗时接近 29 秒,Java 堆内存峰值消耗增多大概 370MB。

前端 js-xlsx 方案测试

前端方案其实还是需要依赖后端查询的数据,因此耗时方面铁定会比后端方案多的。我启用了 Chrome 浏览器的任务管理器来监控标签的内存消耗。测试数据从 5W 起步,5W 叠加,测试到 30W 的时候浏览器标签就崩溃了,所以最多只能测试一下 25W 数据的导出了。

25W 数据导出时浏览器会显示页面无响应,点击等待按钮还是成功导出数据了:

前端方案导出 25W 行数据,导出的 Excel 文件接近 228MB ,浏览器标签的最大内存消耗达到了 2GB,运行耗时接近 92 秒:

另外,前端方案导出,后端 jvm 的堆内存也涨了近 300MB。

总结

  js-xlsx Java POI
极限导出行数

25W

100W 以上
极限导出文件大小 230MB 600MB 以上
导出 25W 行内存消耗 2GB(浏览器) 100MB(服务器)
导出 25W 行耗时 93秒 29秒
开发效率

可以看出前端 js-xlsx 的方案只能适应一定范围内数据的导出,建议导出的数据量低于 20W 行或小于 200MB,而且使用 js-xlsx 方案后会增加超过一倍的导出时间,优势是可以将后端工作转移到前端,减轻后端服务器 CPU 和内存消耗,也可以更方便前端定制导出的 Excel 文件的格式以及导出进度控制。另外,前端方案需要浏览器多加载一个近 350KB 的库,对于前端的加载优化是一个大问题。

后端 Java POI 的方案可以适应不限大小和行数的 Excel 文件导出。而且使用 POI 时配置好 rowAccessWindowSize,消耗的内存其实并不大,恰如当前例子,实际多出的内存消耗大概也就是 370-300=70MB 多点,1GB 堆内存的 jvm 可以提供 10 多次的并发导出,而且文件导出明显更加快捷,就是导出进度显示和导出 Excel 内容的样式定制可能相对没有前端方案那么友好简单。

总的来说  js-xlsx 方案对于内网项目导出少量数据的情况还是一项大优势,用户可以根据实际情况确认是否采用。不过我个人还是建议前端纯数据导出还是最好采用纯文本形式的 csv 格式,对于开发人员的开发优势非常明显。

—— author : chunlin.qiu

发布了87 篇原创文章 · 获赞 42 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/vipshop_fin_dev/article/details/85946083