前言
最近发现有一个很厉害的前端框架 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