当前位置: 首页 > news >正文

基于 JNI + Rust 实现一种高性能 Excel 导出方案(上篇)

每个不曾起舞的日子,都是对生命的辜负。
——尼采

一、背景:Web 导出 Excel 的场景

Web 导出 Excel 功能在数据处理、分析和共享方面提供了极大的便利,是许多 Web 应用程序中的重要功能。以下是一些典型的场景:

  1. 数据报表导出:在企业管理系统(如ERP、CRM)中,用户经常需要将系统中的数据生成报表并导出为 Excel 文件,以便进行进一步的分析和处理。例如,销售人员可以导出月度销售报告,财务人员可以导出财务报表。
  2. 数据备份与归档:用户可能需要定期将系统中的数据导出为 Excel 文件进行备份或归档,以确保数据安全和可追溯性。例如,学校的学生成绩记录、医院的病人就诊记录等。
  3. 数据共享与交流:在团队协作中,成员之间需要共享数据,Excel 文件是一种方便的格式,可以在不同的软件和平台之间轻松传递和查看。例如,项目经理可以导出项目进度数据,与团队成员或客户分享。
  4. 自定义数据分析:用户可能希望对系统中的数据进行自定义分析,而 Excel 提供了强大的数据处理和分析功能,如透视表、图表等。例如,市场分析人员可以导出市场调查数据,在 Excel 中进行详细的分析和可视化。
  5. 批量数据处理:当用户需要对大量数据进行批量处理时,导出为 Excel 文件可以方便地进行编辑和修改,然后再导入回系统。例如,人力资源部门可以导出员工信息进行批量更新后再导入系统。
  6. 合规与审计需求:某些行业和组织有合规和审计要求,需要定期导出数据以供审计和检查。例如,金融机构需要导出交易记录以满足监管要求。

二、现有解决方案与技术栈

  • 服务器端技术:如 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 文件,适用于需要处理电子表格数据的各种应用场景,如数据分析、报告生成等。

主要特性

  1. 易于使用:rust_xlsxwriter 提供了直观的 API,使得创建和操作 Excel 文件变得非常简单。
  2. 支持多种数据类型:可以在单元格中写入字符串、数字、日期和布尔值等多种数据类型。
  3. 格式化功能:支持丰富的单元格格式化选项,包括字体样式、颜色、边框和对齐方式等。
  4. 工作表管理:可以创建多个工作表,并在不同的工作表之间切换和操作。
  5. 公式支持:允许在单元格中插入 Excel 公式,自动计算结果。
  6. 图表支持:能够在工作表中插入各种类型的图表,如柱状图、折线图和饼图等。
  7. 性能优化:针对大数据量的处理进行了优化,确保生成 Excel 文件的效率。

四、实践:检验真理的唯一标准

1、使用 Rust 导出 Excel

主要使用 rust_xlsxwriter 这个库,

https://docs.rs/rust_xlsxwriter/latest/rust_xlsxwriter/

示例

创建一个 Rust 项目,添加 rust_xlsxwriter 依赖,

# Cargo.toml
[dependencies]
rust_xlsxwriter = { version = "0.79.4" , features =["zlib","ryu","polars","serde","constant_memory"]  }

修改官网示例,这里尝试导出 10 万行数据,

// 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

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 库项目,

cargo new my_excel_writer_lib --lib
[dependencies]
jni = "0.21.1"
rust_xlsxwriter = { version = "0.79.4" , features =["zlib","ryu","polars","serde","constant_memory"]  }[lib]
crate-type = ["cdylib"]

定义一个结构体,用于保存 Excel 指针,

#[repr(C)]
pub struct WorksheetHandle {workbook: *mut Workbook,worksheet: *mut Worksheet,
}

定义 createWorksheet 用于初始化创建 Excel 并返回指针,


#[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 里面写入单元格数据,


#[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 路径下,


#[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 没法自动处理这部分资源,

#[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 itlet _ = Box::from_raw(handle_ptr as *mut WorksheetHandle);}}
}

3、搭建 Spring Boot 项目

创建一个 Spring Boot 项目,这里使用的版本是 2.1.8.RELEASE

Step1、引入 POM 依赖

<!-- 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

spring:datasource:# 数据源基本配置url: jdbc:postgresql://127.0.0.1:5432/dbnameusername: postgrespassword: #################driver-class-name: org.postgresql.Drivertype: com.alibaba.druid.pool.DruidDataSource

主要配置数据源。

Step3、配置 Mybatis

/*** @version: V1.0* @author: 余衫马* @description: Mybatis 配置* @data: 2024-11-21 19:44**/
@Configuration
@MapperScan("com.yushanma.crazyexcel.dao")
public class MybatisConfig {
}

Step4、实体类封装

TestVo、TestPo 模拟用户需要导出的数据字段,

/*** @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 用于写入数据
/*** @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 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>

在数据库对应创建数据表,

create TABLE test_xls_data_c20 (
f1 varchar(50) not null,// 省略...
f20 varchar(50) not null
)

4、导出功能核心逻辑

  • MyExportResultHandler 实现 ResultHandlerhandleResult 方法用于处理数据库返回的数据
  • 引入外部 Rust 库 my_excel_writer_lib,定义本地 Native 方法
  • 构造方法调用 createWorksheet 获取指针对象
  • 每次返回一行数据时调用 writeToWorksheet 将数据写到 worksheet 中
  • 实现 AutoCloseable 接口在指针内存释放前导出 Excel
/*** @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) {// 创建一个 worksheethandle = createWorksheet();// 写入表头for (int i = 0; i < head.length; i++) {writeToWorksheet(handle, 0, i, head[i]);}}/*** 自动释放资源*/@Overridepublic void close() {saveWorkbook(handle, "out.xlsx");if (handle != 0) {freeWorksheetHandle(handle);handle = 0;}System.out.println("Auto Release!");}@Overridepublic 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 调用,

/*** @version: V1.0* @author: 余衫马* @description: 测试 impl* @data: 2024-11-21 19:59**/
@Service
public class TestServiceImpl implements TestService {@Autowiredprivate 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);}}@Overridepublic void testResultHandler() throws IOException {LocalDateTime startDatetime = LocalDateTime.now();try (MyExportResultHandler handler = new MyExportResultHandler()) {testDao.selectMillionData(handler);}// 计算两个时间点之间的 DurationDuration 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,

/*** @version: V1.0* @author: 余衫马* @description: 测试 Controller* @data: 2024-11-25 14:51**/
@RestController
@RequestMapping("api/")
public class TestController {@Autowiredprivate 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 返回给客户端,

#[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 arraylet byte_array = match env.byte_array_from_slice(&data) {Ok(array) => array,Err(e) => {eprintln!("Couldn't create byte array: {:?}", e);return;}};// 获取 OutputStream 类并获取 write 方法 IDlet 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 文件通过流返回给客户端,

/*** @version: V1.0* @author: 余衫马* @description: 测试 impl* @data: 2024-11-21 19:59**/
@Service
public class TestServiceImpl implements TestService {// 省略...@Overridepublic void testResultHandler(HttpServletResponse response) throws IOException {LocalDateTime startDatetime = LocalDateTime.now();try (MyExportResultHandler handler = new MyExportResultHandler(response)) {testDao.selectMillionData(handler);}// 计算两个时间点之间的 DurationDuration 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);}// 省略...}
/*** @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) {// 创建一个 worksheethandle = createWorksheet();// 写入表头for (int i = 0; i < head.length; i++) {writeToWorksheet(handle, 0, i, head[i]);}}/*** 自动释放资源*/@Overridepublic 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!");}}@Overridepublic 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,提升使用友好度,并且优化性能。

参考资料

  1. 探索 JNI - Rust 与 Java 互调实战
  2. ​JVisualVM 性能分析与 Mybatis ResultHandler 实战
  3. https://docs.rs/rust_xlsxwriter/latest/rust_xlsxwriter/
  4. Mybatis Plus 集成 PgSQL 指南

相关文章:

基于 JNI + Rust 实现一种高性能 Excel 导出方案(上篇)

每个不曾起舞的日子&#xff0c;都是对生命的辜负。 ——尼采 一、背景&#xff1a;Web 导出 Excel 的场景 Web 导出 Excel 功能在数据处理、分析和共享方面提供了极大的便利&#xff0c;是许多 Web 应用程序中的重要功能。以下是一些典型的场景&#xff1a; 数据报表导出&am…...

【Maven】依赖管理

4. Maven的依赖管理 在 Java 开发中&#xff0c;项目的依赖管理是一项重要任务。通过合理管理项目的依赖关系&#xff0c;我们可以有效的管理第三方库&#xff0c;模块的引用及版本控制。而 Maven 作为一个强大的构建工具和依赖管理工具&#xff0c;为我们提供了便捷的方式来管…...

springboot/ssm高校超市管理系统Java商品出入库供应商管理系统web源码wms

springboot/ssm高校超市管理系统Java商品出入库供应商管理系统web源码wms 基于springboot(可改ssm)vue项目 开发语言&#xff1a;Java 框架&#xff1a;springboot/可改ssm vue JDK版本&#xff1a;JDK1.8&#xff08;或11&#xff09; 服务器&#xff1a;tomcat 数据库&a…...

小程序-基于java+SpringBoot+Vue的微信小程序养老院系统设计与实现

项目运行 1.运行环境&#xff1a;最好是java jdk 1.8&#xff0c;我们在这个平台上运行的。其他版本理论上也可以。 2.IDE环境&#xff1a;IDEA&#xff0c;Eclipse,Myeclipse都可以。推荐IDEA; 3.tomcat环境&#xff1a;Tomcat 7.x,8.x,9.x版本均可 4.硬件环境&#xff1a…...

宠物电商对接美团闪购:实现快速配送与用户增值

随着宠物行业的快速发展&#xff0c;宠物电商市场也在不断扩张。消费者的需求不再局限于传统的线上购物模式&#xff0c;越来越多的人开始追求更快捷的配送服务和更优质的购物体验。为了适应这一趋势&#xff0c;许多宠物电商平台开始寻求与本地配送平台合作&#xff0c;以提供…...

Vue中使用<Transition>与<TransitionGroup>

目录 介绍 CSS过渡类 为过渡效果命名 CSS的transition CSS的transform 性能考量 出现时过渡 元素间过渡 过渡模式 使用Key属性过渡 和的区别 进入/离开动画 移动动画 一个购物车飞跃例子 介绍 传统HTML中&#xff0c;我们可以使用CSS属性&#xff1a;“animation”…...

Algorithms and Data Structures in C++ by Mohammed Yasir Eramangadan

MP4 创建 |视频&#xff1a;h264、1280720 |音频&#xff1a;AAC&#xff0c;44.1 KHz&#xff0c;2 通道 类型&#xff1a;在线学习 |语言&#xff1a;英文 字幕 |持续时间&#xff1a; 159 讲座 &#xff08; 10h 43m &#xff09; |大小&#xff1a; 3.5 GB “通过专家制作…...

2024广东省职业技能大赛云计算——构建CICD 部署2048小游戏

构建CI/CD 前言 题目如下&#xff1a; 构建CI/CD 编写流水线脚本.gitlab-ci.yml触发自动构建&#xff0c;具体要求如下&#xff1a; &#xff08;1&#xff09;基于镜像maven:3.6-jdk-8构建项目的drone分支&#xff1b; &#xff08;2&#xff09;构建镜像的名称&#xff1a…...

React 条件渲染

React 条件渲染 React 条件渲染是一种在 React 应用程序中根据不同的条件显示不同组件或内容的技巧。它是 React 响应用户输入、状态变化或数据变化的核心机制之一。本文将深入探讨 React 条件渲染的概念、用法和最佳实践。 目录 条件渲染的基本概念使用 JavaScript 运算符进…...

Hadoop生态圈框架部署(九)- Hive部署

文章目录 前言一、Hive部署&#xff08;手动部署&#xff09;下载Hive1. 上传安装包2. 解压Hive安装包2.1 解压2.2 重命名2.3 解决guava冲突 3. 配置Hive3.1 配置Hive环境变量3.2 修改 hive-site.xml 配置文件3.3 配置MySQL驱动包3.3.1 下在MySQL驱动包3.3.2 上传MySQL驱动包3.…...

c语言的qsort函数理解与使用

介绍&#xff1a;qsort 函数是 C 标准库中用于排序的快速排序算法函数。它的用法非常灵活&#xff0c;可以对任意类型的元素进行排序&#xff0c;只要提供了比较函数即可。 qsort 函数原型及参数解释&#xff1a; void qsort ( void* base, //指向要排序的数组的首元素…...

Java 语言的起源发展与基本概念(JDK,JRE,JVM)

Java语言的起源 源起 Java语言最初是由Sun Microsystems公司&#xff08;该公司于2009年被Oracle公司收购&#xff09;开发的一种编程语言。其创造者是詹姆斯高斯林&#xff08;James Gosling&#xff09;&#xff0c;他是一位加拿大计算机科学家。其前身名为Oak&#xff08;橡…...

03_变量

变量 var num 10; 变量的重新赋值 var num10; num 20; 变量提升 JavaScript 引擎的工作方式是&#xff0c;先解析代码&#xff0c;获取所有被声明的变量&#xff0c;然后再一行一行地运行。这造成的结果&#xff0c;就是所有的变量的声明语句&#xff0c;都会被提升到代码的…...

[论文阅读-综述]Supervised Speech Separation Based on Deep Learning: An Overview

基于深度学习的监督语音分离&#xff1a;综述 出版&#xff1a;IEEE 核心&#xff1a;使用语音分离将目标语音信号与噪声混合分离的计算 本文用于对该文章的学习&#xff0c;主要是对内容的理解翻译与笔记 1. 语音分离介绍 语音分离的目标&#xff1a;将目标语音与背景干扰分…...

群控系统服务端开发模式-应用开发-邮箱配置功能开发

邮箱配置主要是将管理员数据做归属。具体见下图&#xff1a; 一、创建表 1、语句 CREATE TABLE cluster_control.nc_param_mail (id int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 编号,title varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT…...

【机器学习】——卷积与循环的交响曲:神经网络模型在现代科技中的协奏

&#x1f3bc;个人主页&#xff1a;【Y小夜】 &#x1f60e;作者简介&#xff1a;一位双非学校的大二学生&#xff0c;编程爱好者&#xff0c; 专注于基础和实战分享&#xff0c;欢迎私信咨询&#xff01; &#x1f386;入门专栏&#xff1a;&#x1f387;【MySQL&#xff0…...

android studio引用so库

在工程中编译好的so库文件将在原始编译工程对应目录下&#xff1a;build/intermediates/cxx/Debug/xxxxxx/obj/ 其目录结构如上所示&#xff0c;包含生成的四个版本&#xff0c;每个文件夹下均包含c/c源码编译成的Android版本的libnavi.so库和提供应用接口的libnavi-lib.so库。…...

2024年信号处理与神经网络应用(SPNNA 2024)

会议官网&#xff1a;www.spnna.org 会议时间&#xff1a;2024年12月13-15日 会议地点&#xff1a;中国武汉...

wxWidgets-ImageView

wxWidgets实现图片浏览、放大缩小、另存为新的图片格式等 #include "wx/wxprec.h"#ifndef WX_PRECOMP#include "wx/wx.h" #endif#include "wx/filename.h" #include "wx/zstream.h"#include "imageviewctrl.h"class MyFrame…...

第1章-JVM和Java体系架构

虚拟机 虚拟机概念 所谓虚拟机&#xff08;Virtual Machine&#xff09;&#xff0c;就是一台虚拟的计算机。它是一款软件&#xff0c;用来执行一系列虚拟计算机指令。大体上&#xff0c;虚拟机可以分为系统虚拟机和程序虚拟机。 大名鼎鼎的Virtual Box&#xff0c;VMware就属…...

windows 服务器角色

windows 服务器角色 Active Directory Rights Management Services Active Directory RightsManagement Services (AD RS)帮助保护信息&#xff0c;防止未授权使用。AD RMS 将建立用户标识&#xff0c;并为授权用户提供受保护信息的许可证。 ServicesActive Directory 联合身…...

[OpenHarmony5.0][Docker][环境]OpenHarmony5.0 Docker编译环境镜像下载以及使用方式

T. 已测试目录 主机类型主机版本Docker镜像版本结果WSL2Ubuntu22.04Ubuntu20.04PASSWSL2Ubuntu22.04Ubuntu18.04PASS R. 软硬件要求&#xff1a; 编译硬件需求&#xff1a;做多系统测试&#xff0c;磁盘500GB起步(固态)&#xff08;机械会卡死&#xff09;&#xff0c;内存3…...

C#中判断两个 List<T> 的内容是否相等

ET实现游戏中邮件系统逻辑思路&#xff08;服务端&#xff09;_游戏邮件系统设计-CSDN博客 场景&#xff1a;今天遇到一个BUG&#xff0c;在服务器重启的时候&#xff08;体验服&#xff09;&#xff0c;玩家之前接收的邮件又重新接收了一次&#xff0c;但是两封邮件的ID是不同…...

Linux环境下配置neo4j图数据库

1.下载安装包 openjdk-11.0.1_linux-x64_bin.tar.gz neo4j-community-4.2.19-unix.tar.gz 2.之前配置好的配置文件 neo4j.conf 3.安装 3.1-jdk11的安装&#xff08;jdk1.8不够用&#xff09; 解压缩 tar -zxvf openjdk-11.0.1_linux-x64_bin.tar.gz修改系统环境变量 打开pro…...

Windows 11 搭建 Docker 桌面版详细教程

在当今的软件开发与部署领域&#xff0c;Docker 已成为一项极为重要的容器化技术。它能够让开发者轻松地打包应用及其依赖项&#xff0c;实现跨环境的一致性运行&#xff0c;大大提高了开发效率与部署的便捷性。本教程将详细介绍在 Windows 11 操作系统上搭建 Docker 桌面版的具…...

Pytest-Bdd-Playwright 系列教程(13):钩子(hooks)

Pytest-Bdd-Playwright 系列教程&#xff08;13&#xff09;&#xff1a;钩子&#xff08;hooks&#xff09; 前言一、什么是钩子&#xff1f;二、Pytest-Bdd 提供的钩子一览三、钩子用法详解1. pytest_bdd_before_scenario2. pytest_bdd_after_scenario3. pytest_bdd_before_s…...

dns 服务器简单介绍

dns 服务器分类&#xff1a; 根域名服务器顶级域名服务器权威域名服务器本地域名服务器 dns 的查询过程 国内优秀公共域名 腾讯&#xff1a;DNSPod-免费智能DNS解析服务商-电信_网通_教育网,智能DNS-烟台帝思普网络科技有限公司 119.29.29.29 和 182.254.118.118 阿里&#xf…...

Neo4j图形数据库-Cypher中常用指令

一、创建与修改 1.1 create 创建图数据库中的节点、关系等元素&#xff1a; CREATE (:Person {name: "Alice", age: 30}) CREATE (p1:Person {name: "Bob"})-[r:KNOWS]->(p2:Person {name: "Charlie"})批量创建元素 CREATE (n1:Node),(n2…...

linux安全管理-防火墙配置

1. 开启系统防火墙 1、检查内容 检查操作系统是否开启防火墙&#xff1b; 2、配置要求 操作系统开启防火墙&#xff1b; 3、配置方法 systemctl status firewalld ##查看系统防火墙运行状态 systemctl start firewalld ##启动防火墙 systemctl restart firewalld ##重启防火墙…...

什么是BIOS

BIOS&#xff08;Basic Input/Output System&#xff0c;基本输入输出系统&#xff09;是计算机启动过程中的一个关键组件&#xff0c;主要负责硬件的初始化和系统的引导。以下是关于 BIOS 的一些详细信息&#xff1a; 1. 基本功能 硬件初始化&#xff1a;当计算机启动时&…...

wordpress option/东莞seo网站排名优化

项目简述&#xff1a;基于开源Hadoop2.0架构的集群网络&#xff0c;进行海量数据的分布式计算。由于Hadoop集群规模不断扩大&#xff0c;而搭建一个同等规模的测试集群需要一笔昂贵的开销。目前有100台左右物料&#xff0c;期望预测计算节点1500的集群网络性能&#xff0c;目前…...

合肥企业网站建设工作室/网络推广的基本渠道

本次实验主要是实现站点间的复制实验&#xff0c;实验图如下&#xff1a; 1&#xff09;构建好上图的域结构以后&#xff0c;打开192.168.10.2服务器的AD站点和服务&#xff0c;可以看到站点间的默认关系。 2&#xff09;右击Sites---》新建---》站点。分别输入XM和FZ做为站点的…...

为什么打开网址都是站长工具/seo站长常用工具

支付 uni.requestPayment 是一个统一各平台的客户端支付API&#xff0c;不管是在某家小程序还是在App中&#xff0c;客户端均使用本API调用支付。本API运行在各端时&#xff0c;会自动转换为各端的原生支付调用API。 uni.requestPayment( object ) 参数说明 provider 必填&am…...

网站没有收录怎么办/百度推广登陆网址

难易程度&#xff1a;★★ 重要性&#xff1a;★★★★★ 树结构是面试中的考察的重点&#xff0c;而树的遍历又是树结构的基础。 //先序遍历&#xff0c;递归版本 public static ArrayList<Integer> preOrder(TreeNode root) {ArrayList<Integer> res new ArrayL…...

线上报名小程序怎么做/湖南seo优化价格

https://blog.csdn.net/bazhidao0031/article/details/81450815 转载于:https://www.cnblogs.com/guochen/p/10340837.html...

旅游网站建设规划书/深圳外贸网络推广渠道

推荐阅读&#xff1a; 1.Cas9稳转细胞株&#xff0c;令CRISPR/Cas9基因编辑更高效 2.【2020年】CRISPR基因编辑技术最新进展盘点解读 1月18日&#xff0c;博雅辑因宣布中国国家药品监督管理局药品审评中心已经批准其针对输血依赖型β地中海贫血的CRISPR/Cas9基因编辑疗法产品E…...