每个不曾起舞的日子,都是对生命的辜负。
------尼采
一、背景:Web 导出 Excel 的场景
Web 导出 Excel 功能在数据处理、分析和共享方面提供了极大的便利,是许多 Web 应用程序中的重要功能。以下是一些典型的场景:
- 数据报表导出:在企业管理系统(如ERP、CRM)中,用户经常需要将系统中的数据生成报表并导出为 Excel 文件,以便进行进一步的分析和处理。例如,销售人员可以导出月度销售报告,财务人员可以导出财务报表。
- 数据备份与归档:用户可能需要定期将系统中的数据导出为 Excel 文件进行备份或归档,以确保数据安全和可追溯性。例如,学校的学生成绩记录、医院的病人就诊记录等。
- 数据共享与交流:在团队协作中,成员之间需要共享数据,Excel 文件是一种方便的格式,可以在不同的软件和平台之间轻松传递和查看。例如,项目经理可以导出项目进度数据,与团队成员或客户分享。
- 自定义数据分析:用户可能希望对系统中的数据进行自定义分析,而 Excel 提供了强大的数据处理和分析功能,如透视表、图表等。例如,市场分析人员可以导出市场调查数据,在 Excel 中进行详细的分析和可视化。
- 批量数据处理:当用户需要对大量数据进行批量处理时,导出为 Excel 文件可以方便地进行编辑和修改,然后再导入回系统。例如,人力资源部门可以导出员工信息进行批量更新后再导入系统。
- 合规与审计需求:某些行业和组织有合规和审计要求,需要定期导出数据以供审计和检查。例如,金融机构需要导出交易记录以满足监管要求。
二、现有解决方案与技术栈
- 服务器端技术:如 Python 的 Pandas 库、Node.js 的 ExcelJS 库、Java 的 Apache POI 库等,用于生成 Excel 文件。
- 前端技术:如 JavaScript 的 SheetJS 库,用于在浏览器中生成和下载 Excel 文件。
- API 接口:通过 RESTful API 或 GraphQL 接口,从服务器获取数据并生成 Excel 文件。
在 Java 中,常见的 Excel 导出方案包括:
1、Apache POI:
优点:
- 功能强大,支持读写 Microsoft Excel 97-2003 (XLS) 和 Excel 2007+ (XLSX) 文件。
- 社区活跃,文档和示例丰富。
- 支持复杂的 Excel 操作,如公式、图表、样式等。
缺点:
- API 较为复杂,上手需要一定时间。
- 性能在处理大文件时可能不够理想。
2、JExcelAPI (JXL)
优点:
- 简单易用,适合处理基本的 Excel 操作。
- 对 Excel 97-2003 (XLS) 文件有较好的支持。
缺点:
- 不支持 Excel 2007+ (XLSX) 文件。
- 功能相对有限,不支持复杂的操作如图表和公式。
3、EasyExcel
EasyExcel 因其高性能、简单易用、功能丰富以及良好的社区支持,成为了许多企业在实现 Excel 导出功能时的首选工具。
优点:
- 由阿里巴巴开源,专注于高性能的 Excel 读写。
- 支持 Excel 2007+ (XLSX) 文件。
- API 简洁,易于使用,特别适合大数据量的导入导出。
缺点:
- 相对于 Apache POI,功能较少,不支持一些复杂的操作。
4、OpenCSV (用于处理 CSV 文件)
优点:
- 轻量级,简单易用。
- 专注于 CSV 格式,性能好。
缺点:
- 仅支持 CSV 文件,不支持 XLS 或 XLSX 格式。
- 功能较为单一,只能处理简单的文本数据。
5、JasperReports
优点:
- 强大的报表生成工具,支持多种格式(包括 Excel)。
- 可以结合 iReport 等工具进行可视化设计。
- 支持复杂的报表需求,如分组、汇总、图表等。
缺点:
- 学习曲线较陡,需要掌握报表设计和配置。
- 配置和使用较为复杂,适合有报表需求的场景。
6、Spire.XLS for Java
优点:
- 商业库,提供全面的 Excel 操作功能。
- 支持所有版本的 Excel 文件格式。
- 易于使用,文档和技术支持完善。
缺点:
- 商业软件,需要购买许可证。
- 社区支持和开源资源较少。
7、Aspose.Cells for Java
优点:
- 商业库,功能非常强大,支持所有 Excel 文件格式。
- 提供丰富的 API,可以处理复杂的 Excel 操作,如公式、图表、数据透视表等。
- 性能优秀,适合处理大数据量的 Excel 文件。
缺点:
- 商业软件,价格较高。
- 学习成本较高,需要深入了解其 API。
三、思考:基于 JNI + Rust 的导出方案
1、优势
将 Excel 文件的生成任务交给 Rust 来处理,可以充分利用 Rust 的高性能和内存安全特性,确保文件生成过程高效且可靠。同时,Java 负责 API 接口请求和业务逻辑处理,利用其成熟的生态系统和广泛的企业应用经验,使得整个系统更加易于维护和扩展。
2、职责
Rust 部分:
- 使用 rust-xlsxwriter 等库来处理 Excel 文件的生成和导出。
- 将生成的 Excel 文件保存到服务器的某个位置,或者直接返回给调用方。
Java 部分:
- 使用 Spring Boot 或其他框架来创建 RESTful API。
- 在接收到请求后,通过 JNI(Java Native Interface)或通过命令行调用 Rust 程序来生成 Excel 文件。
- 将生成的文件返回给客户端,或者提供下载链接。
补充:rust_xlsxwriter 介绍
rust_xlsxwriter 是一个用于在 Rust 编程语言中创建和写入 Excel 文件(.xlsx 格式)的库。这个库提供了一种简单且高效的方法来生成 Excel 文件,适用于需要处理电子表格数据的各种应用场景,如数据分析、报告生成等。
主要特性
- 易于使用:rust_xlsxwriter 提供了直观的 API,使得创建和操作 Excel 文件变得非常简单。
- 支持多种数据类型:可以在单元格中写入字符串、数字、日期和布尔值等多种数据类型。
- 格式化功能:支持丰富的单元格格式化选项,包括字体样式、颜色、边框和对齐方式等。
- 工作表管理:可以创建多个工作表,并在不同的工作表之间切换和操作。
- 公式支持:允许在单元格中插入 Excel 公式,自动计算结果。
- 图表支持:能够在工作表中插入各种类型的图表,如柱状图、折线图和饼图等。
- 性能优化:针对大数据量的处理进行了优化,确保生成 Excel 文件的效率。
四、实践:检验真理的唯一标准
1、使用 Rust 导出 Excel
主要使用 rust_xlsxwriter 这个库,
示例
创建一个 Rust 项目,添加 rust_xlsxwriter 依赖,
bash
# Cargo.toml
[dependencies]
rust_xlsxwriter = { version = "0.79.4" , features =["zlib","ryu","polars","serde","constant_memory"] }
修改官网示例,这里尝试导出 10 万行数据,
rust
// main.rs
fn main() -> Result<(), XlsxError> {
// Create a new Excel file object.
let mut workbook = Workbook::new();
// Create some formats to use in the worksheet.
let date_format = Format::new().set_num_format("yyyy-mm-dd");
// Add a worksheet to the workbook.
let worksheet = workbook.add_worksheet();
// Set the column width for clarity.
worksheet.set_column_width(0, 16)?;
worksheet.set_column_width(5, 22)?;
// Generate data in parallel.
let data: Vec<Vec<CellValue>> = (0..ROWS)
.into_par_iter()
.map(|i| generate_row_data(i))
.collect();
// Write data to the worksheet in batches.
for (i, row_data) in data.chunks(BATCH_SIZE).enumerate() {
for (j, row) in row_data.iter().enumerate() {
let row_index = i * BATCH_SIZE + j;
for (col, value) in row.iter().enumerate() {
match value {
CellValue::String(s) => worksheet.write(row_index as u32, col as u16, *s)?,
CellValue::Number(n) => worksheet.write(row_index as u32, col as u16, *n)?,
CellValue::Date(d) => worksheet.write_with_format(
row_index as u32,
col as u16,
d,
&date_format,
)?,
CellValue::Url(u) => {
worksheet.write(row_index as u32, col as u16, Url::new(*u))?
}
};
}
}
}
// Save the file to disk.
workbook.save("parallel_demo.xlsx")?;
Ok(())
}
编译运行后报错 attempt to add with overflow ,
bash
thread 'main' panicked at /home/sam/.cargo/registry/src/mirrors.ustc.edu.cn-61ef6e0cd06fb9b8/rust_xlsxwriter-0.79.4/src/relationship.rs:112:9:
attempt to add with overflow
原因分析 :点击报错提示查看源码,发现 id_num定义的数据类型是 u16 ,在 Rust 中 u16 表示一个无符号 16 位整数类型,它的取值范围是从 0 到 65535(即 2^16 - 1),而我们导出的数据行数是 10 万,所以在加法运算的时候溢出了数值范围。
解决方案:使用 u32 类型,重新编译。
运行效果:可以看到 Excel 文件已经成功导出了,
10W数据量也可以正常导出,
2、导出 Rust 库文件
创建一个 Rust 库项目,
bash
cargo new my_excel_writer_lib --lib
bash
[dependencies]
jni = "0.21.1"
rust_xlsxwriter = { version = "0.79.4" , features =["zlib","ryu","polars","serde","constant_memory"] }
[lib]
crate-type = ["cdylib"]
定义一个结构体,用于保存 Excel 指针,
rust
#[repr(C)]
pub struct WorksheetHandle {
workbook: *mut Workbook,
worksheet: *mut Worksheet,
}
定义 createWorksheet 用于初始化创建 Excel 并返回指针,
rust
#[no_mangle]
pub extern "system" fn Java_com_yushanma_crazyexcel_handler_MyExportResultHandler_createWorksheet(
_env: JNIEnv,
_class: JClass,
) -> jlong {
let workbook = Box::new(Workbook::new());
let workbook_ptr = Box::into_raw(workbook);
// SAFETY: We just created the raw pointer from a Box, so it's valid.
let worksheet = unsafe { (*workbook_ptr).add_worksheet() };
let handle = Box::new(WorksheetHandle {
workbook: workbook_ptr,
worksheet: worksheet,
});
Box::into_raw(handle) as jlong
}
定义 writeToWorksheet 用于往 worksheet 里面写入单元格数据,
rust
#[no_mangle]
pub extern "system" fn Java_com_yushanma_crazyexcel_handler_MyExportResultHandler_writeToWorksheet(
env: JNIEnv,
_class: JClass,
handle_ptr: jlong,
row: u32,
col: u16,
text: JObject,
) {
if handle_ptr == 0 {
eprintln!("Invalid handle pointer");
return;
}
if text.is_null() {
eprintln!("Text argument is null");
return;
}
let handle = unsafe { &*(handle_ptr as *mut WorksheetHandle) };
// let content: String = match env.get_string(&text.into()) {
// Ok(java_str) => java_str.into(),
// Err(e) => {
// eprintln!("Couldn't get java string: {:?}", e);
// return;
// }
// };
let content: String = match unsafe { env.get_string_unchecked(&text.into()) } {
Ok(java_str) => java_str.into(),
Err(e) => {
eprintln!("Couldn't get java string: {:?}", e);
return;
}
};
let worksheet: &mut Worksheet = unsafe { &mut *handle.worksheet };
if let Err(e) = worksheet.write_string(row, col, content) {
eprintln!("Failed to write to worksheet: {:?}", e);
}
}
定义 saveWorkbook 用于导出 Excel 文件到某个 path 路径下,
rust
#[no_mangle]
pub extern "system" fn Java_com_yushanma_crazyexcel_handler_MyExportResultHandler_saveWorkbook(
env: JNIEnv,
_class: JClass,
handle_ptr: jlong,
file_path: JObject,
) {
if handle_ptr == 0 {
eprintln!("Invalid handle pointer");
return;
}
if file_path.is_null() {
eprintln!("File path argument is null");
return;
}
let handle = unsafe { &*(handle_ptr as *mut WorksheetHandle) };
// let path: String = match env.get_string(&file_path.into()) {
// Ok(java_str) => java_str.into(),
// Err(e) => {
// eprintln!("Couldn't get java string: {:?}", e);
// return;
// }
// };
let path: String = match unsafe { env.get_string_unchecked(&file_path.into()) } {
Ok(java_str) => java_str.into(),
Err(e) => {
eprintln!("Couldn't get java string: {:?}", e);
return;
}
};
let workbook = unsafe { &mut *handle.workbook };
if let Err(e) = workbook.save(path) {
eprintln!("Failed to save workbook: {:?}", e);
}
}
定义 freeWorksheetHandle 用于释放指针内存,因为 JVM 没法自动处理这部分资源,
rust
#[no_mangle]
pub extern "system" fn Java_com_yushanma_crazyexcel_handler_MyExportResultHandler_freeWorksheetHandle(
_env: JNIEnv,
_class: JClass,
handle_ptr: jlong,
) {
if handle_ptr != 0 {
unsafe {
// Convert the raw pointer back to a Box and drop it
let _ = Box::from_raw(handle_ptr as *mut WorksheetHandle);
}
}
}
3、搭建 Spring Boot 项目
创建一个 Spring Boot 项目,这里使用的版本是 2.1.8.RELEASE 。
Step1、引入 POM 依赖
xml
<!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.6.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
主要引入 PostgreSQL、MyBatis-Plus 依赖。
Step2、配置 YML
yaml
spring:
datasource:
# 数据源基本配置
url: jdbc:postgresql://127.0.0.1:5432/dbname
username: postgres
password: #################
driver-class-name: org.postgresql.Driver
type: com.alibaba.druid.pool.DruidDataSource
主要配置数据源。
Step3、配置 Mybatis
java
/**
* @version: V1.0
* @author: 余衫马
* @description: Mybatis 配置
* @data: 2024-11-21 19:44
**/
@Configuration
@MapperScan("com.yushanma.crazyexcel.dao")
public class MybatisConfig {
}
Step4、实体类封装
TestVo、TestPo 模拟用户需要导出的数据字段,
java
/**
* @version: V1.0
* @author: 余衫马
* @description: 测试 VO
* @data: 2024-11-21 19:48
**/
@Data
public class TestVo {
private String f1;
// 省略...
private String f20;
}
Step5、Dao 层定义
TestDao 中定义两个方法,
- selectMillionData 用于查询数据,特别注意使用的是 ResultHandler 作为参数,void 作为返回值
- insertMillionData 用于写入数据
java
/**
* @version: V1.0
* @author: 余衫马
* @description: 测试 Dao
* @data: 2024-11-21 19:46
**/
@Mapper
public interface TestDao {
/**
* 返回百万数据
* @param resultHandler 结果处理器
*/
void selectMillionData(ResultHandler<TestVo> resultHandler);
/**
* 批量写入数据
* @param poList
*/
void insertMillionData(@Param("poList") List<TestPo> poList);
}
Mapper XML 编写具体 SQL 语句,
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yushanma.crazyexcel.dao.TestDao">
<insert id="insertMillionData">
INSERT INTO public.test_xls_data_c20(f1, f2, f3, f4, f5, f6, f7, f8, f9, f10,
f11, f12, f13, f14, f15, f16, f17, f18, f19, f20)
VALUES
<foreach collection="poList" item="po" separator=",">
(#{po.f1}, #{po.f2}, #{po.f3}, #{po.f4}, #{po.f5}, #{po.f6}, #{po.f7}, #{po.f8}, #{po.f9}, #{po.f10},
#{po.f11}, #{po.f12}, #{po.f13}, #{po.f14}, #{po.f15}, #{po.f16}, #{po.f17}, #{po.f18}, #{po.f19}, #{po.f20})
</foreach>
</insert>
<select id="selectMillionData" resultType="com.yushanma.crazyexcel.vo.TestVo">
SELECT * FROM test_xls_data_c20 LIMIT 100000
</select>
</mapper>
在数据库对应创建数据表,
sql
create TABLE test_xls_data_c20 (
f1 varchar(50) not null,
// 省略...
f20 varchar(50) not null
)
4、导出功能核心逻辑
- MyExportResultHandler 实现 ResultHandler 的 handleResult 方法用于处理数据库返回的数据
- 引入外部 Rust 库 my_excel_writer_lib,定义本地 Native 方法
- 构造方法调用 createWorksheet 获取指针对象
- 每次返回一行数据时调用 writeToWorksheet 将数据写到 worksheet 中
- 实现 AutoCloseable 接口在指针内存释放前导出 Excel
java
/**
* @version: V1.0
* @author: 余衫马
* @description: Excel 导出处理器
* @data: 2024-11-21 19:56
**/
public class MyExportResultHandler implements ResultHandler<TestVo>, AutoCloseable {
static {
System.loadLibrary("my_excel_writer_lib");
}
/**
* Excel 指针
*/
private long handle;
/**
* 创建一个 Worksheet
*
* @return 指针 handle
*/
private native long createWorksheet();
/**
* 把数据写入 Worksheet
*
* @param handle 指针
* @param row 行
* @param col 列
* @param text 内容
*/
private native void writeToWorksheet(long handle, int row, int col, String text);
/**
* 保存 Excel 到某个路径下
*
* @param handle 指针
* @param filePath 保存路径
*/
private native void saveWorkbook(long handle, String filePath);
/**
* 释放 Worksheet 内存
*
* @param handle 指针
*/
private native void freeWorksheetHandle(long handle);
/**
* 构造方法
* 初始化一个 Excel 对象
*/
public MyExportResultHandler() {
handle = createWorksheet();
}
/**
* 构造方法
* 初始化一个 Excel 对象
*
* @param head 表头
*/
public MyExportResultHandler(String[] head) {
// 创建一个 worksheet
handle = createWorksheet();
// 写入表头
for (int i = 0; i < head.length; i++) {
writeToWorksheet(handle, 0, i, head[i]);
}
}
/**
* 自动释放资源
*/
@Override
public void close() {
saveWorkbook(handle, "out.xlsx");
if (handle != 0) {
freeWorksheetHandle(handle);
handle = 0;
}
System.out.println("Auto Release!");
}
@Override
public void handleResult(ResultContext<? extends TestVo> resultContext) {
TestVo testVo = resultContext.getResultObject();
try {
// 处理逻辑
writeToWorksheet(handle, resultContext.getResultCount(), 0, testVo.getF1());
// 省略...
writeToWorksheet(handle, resultContext.getResultCount(), 19, testVo.getF20());
} catch (Exception e) {
e.printStackTrace();
}
}
}
ServiceImpl 调用,
java
/**
* @version: V1.0
* @author: 余衫马
* @description: 测试 impl
* @data: 2024-11-21 19:59
**/
@Service
public class TestServiceImpl implements TestService {
@Autowired
private TestDao testDao;
/**
* 生成百万数据
*/
public void generateMillionData(){
for (int i = 0; i < 1000; i++) {
List<TestPo> poList = new ArrayList<>(1000);
for (int j = 0; j < 1000; j++) {
TestPo po = new TestPo();
po.setF1(UUID.randomUUID().toString());
// 省略...
po.setF20(UUID.randomUUID().toString());
poList.add(po);
}
testDao.insertMillionData(poList);
}
}
@Override
public void testResultHandler() throws IOException {
LocalDateTime startDatetime = LocalDateTime.now();
try (MyExportResultHandler handler = new MyExportResultHandler()) {
testDao.selectMillionData(handler);
}
// 计算两个时间点之间的 Duration
Duration duration = Duration.between(startDatetime, LocalDateTime.now());
// 获取分钟、秒和毫秒
long minutes = duration.toMinutes();
long seconds = duration.getSeconds() - minutes * 60;
long millis = duration.toMillis() - minutes * 60 * 1000 - seconds * 1000;
System.out.printf("数量:10W20行,耗时:%d 分 %d 秒 %d 毫秒\n", minutes, seconds, millis);
}
}
测试 Controller,
java
/**
* @version: V1.0
* @author: 余衫马
* @description: 测试 Controller
* @data: 2024-11-25 14:51
**/
@RestController
@RequestMapping("api/")
public class TestController {
@Autowired
private TestService testService;
@GetMapping("/testResultHandler")
public void testResultHandler() throws IOException {
testService.testResultHandler();
}
}
运行效果:数量 10W20行,总字符数 36 * 20 * 100000 = 7.2 千万,耗时:12 秒 918 毫秒**
5、Web 下载实现
Rust 端封装 writeToStream 函数,Java 把响应流 output_stream 传给 Rust,Rust 将 Excel 数据转为流之后,写入到 output_stream 返回给客户端,
rust
#[no_mangle]
pub extern "system" fn Java_com_yushanma_crazyexcel_handler_MyExportResultHandler_writeToStream(
mut env: JNIEnv,
_class: JClass,
handle_ptr: jlong,
output_stream: JObject,
) {
if handle_ptr == 0 {
eprintln!("Invalid handle pointer");
return;
}
if output_stream.is_null() {
eprintln!("output stream is null");
return;
}
let handle = unsafe { &*(handle_ptr as *mut WorksheetHandle) };
let workbook = unsafe { &mut *handle.workbook };
// 要写入的数据
let data = match workbook.save_to_buffer() {
Ok(data) => data,
Err(e) => {
eprintln!("Failed to save workbook to buffer: {:?}", e);
return;
}
};
// 将数据转换为 Java byte array
let byte_array = match env.byte_array_from_slice(&data) {
Ok(array) => array,
Err(e) => {
eprintln!("Couldn't create byte array: {:?}", e);
return;
}
};
// 获取 OutputStream 类并获取 write 方法 ID
let write_method_id = match env.get_method_id("java/io/OutputStream", "write", "([B)V") {
Ok(id) => id,
Err(e) => {
eprintln!("Couldn't find write method: {:?}", e);
return;
}
};
// 准备参数
let byte_array_jobject = JObject::from(byte_array);
let args = [jvalue {
l: *byte_array_jobject,
}];
// 调用 OutputStream 的 write 方法
if let Err(e) = unsafe {
env.call_method_unchecked(
output_stream,
write_method_id,
ReturnType::Primitive(Primitive::Void),
&args,
)
} {
eprintln!("Call to write method failed: {:?}", e);
}
}
Java 端把 HttpServletResponse 传给 MyExportResultHandler ,在资源释放前将 Excel 文件通过流返回给客户端,
java
/**
* @version: V1.0
* @author: 余衫马
* @description: 测试 impl
* @data: 2024-11-21 19:59
**/
@Service
public class TestServiceImpl implements TestService {
// 省略...
@Override
public void testResultHandler(HttpServletResponse response) throws IOException {
LocalDateTime startDatetime = LocalDateTime.now();
try (MyExportResultHandler handler = new MyExportResultHandler(response)) {
testDao.selectMillionData(handler);
}
// 计算两个时间点之间的 Duration
Duration duration = Duration.between(startDatetime, LocalDateTime.now());
// 获取分钟、秒和毫秒
long minutes = duration.toMinutes();
long seconds = duration.getSeconds() - minutes * 60;
long millis = duration.toMillis() - minutes * 60 * 1000 - seconds * 1000;
System.out.printf("数量:10W20行,耗时:%d 分 %d 秒 %d 毫秒\n", minutes, seconds, millis);
}
// 省略...
}
java
/**
* @version: V1.0
* @author: 余衫马
* @description: Excel 导出处理器
* @data: 2024-11-21 19:56
**/
public class MyExportResultHandler implements ResultHandler<TestVo>, AutoCloseable {
static {
System.loadLibrary("my_excel_writer_lib");
}
/**
* Excel 指针
*/
private long handle;
/**
* 响应流
*/
private HttpServletResponse httpServletResponse;
/**
* 创建一个 Worksheet
*
* @return 指针 handle
*/
private native long createWorksheet();
/**
* 把数据写入 Worksheet
*
* @param handle 指针
* @param row 行
* @param col 列
* @param text 内容
*/
private native void writeToWorksheet(long handle, int row, int col, String text);
/**
* 保存 Excel 到某个路径下
*
* @param handle 指针
* @param filePath 保存路径
*/
private native void saveWorkbook(long handle, String filePath);
/**
* 释放 Worksheet 内存
*
* @param handle 指针
*/
private native void freeWorksheetHandle(long handle);
/**
* 写到响应流
*
* @param handle 指针
* @param os OutputStream
*/
private native void writeToStream(long handle, OutputStream os);
/**
* 构造方法
* 初始化一个 Excel 对象
*/
public MyExportResultHandler() {
handle = createWorksheet();
}
/**
* 构造函数
*
* @param response HttpServletResponse
*/
public MyExportResultHandler(HttpServletResponse response) {
handle = createWorksheet();
httpServletResponse = response;
}
/**
* 构造方法
* 初始化一个 Excel 对象
*
* @param head 表头
*/
public MyExportResultHandler(String[] head) {
// 创建一个 worksheet
handle = createWorksheet();
// 写入表头
for (int i = 0; i < head.length; i++) {
writeToWorksheet(handle, 0, i, head[i]);
}
}
/**
* 自动释放资源
*/
@Override
public void close() throws IOException {
httpServletResponse.setContentType("application/vnd.ms-excel");
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setHeader("Content-disposition", "attachment;filename=out.xlsx");
OutputStream outputStream = httpServletResponse.getOutputStream();
try {
writeToStream(handle, outputStream);
} finally {
if (handle != 0) {
freeWorksheetHandle(handle);
handle = 0;
}
// 关闭流
if (outputStream != null) {
outputStream.flush();
outputStream.close();
}
System.out.println("Auto Release!");
}
}
@Override
public void handleResult(ResultContext<? extends TestVo> resultContext) {
TestVo testVo = resultContext.getResultObject();
try {
// 处理逻辑
writeToWorksheet(handle, resultContext.getResultCount(), 0, testVo.getF1());
// 省略...
writeToWorksheet(handle, resultContext.getResultCount(), 19, testVo.getF20());
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行效果:数量 10W20行,总字符数 36 * 20 * 100000 = 7.2 千万,耗时:17 秒 496 毫秒
五、JVM 分析
应用刚启动时,
导出100W20行 Excel 时,
导出完成,
总耗时:3 分 38 秒 341 毫秒,100W20行,共 7.2 亿字符,
至此,已经初步实现了导出功能,下一篇文章我们将继续拓展 API,提升使用友好度,并且优化性能。