欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 资讯 > 基于 JNI + Rust 实现一种高性能 Excel 导出方案(下篇)

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

2024/12/22 0:01:36 来源:https://blog.csdn.net/weixin_47560078/article/details/144102138  浏览:    关键词:基于 JNI + Rust 实现一种高性能 Excel 导出方案(下篇)

衡量一个人是否幸福,不应看他有多少高兴的事,而应看他是否为小事烦扰。只有幸福的人,才会把无关痛痒的小事挂心上。那些真正经历巨大灾难和深重痛苦的人,根本无暇顾及这些小事的。因此人们往往在失去幸福之后,才会发现它们曾经存在。

在上篇我们实现了最基础的导出功能,本篇我们拓展样式 API,封装为通用 Jar 包,提升开发者的使用友好度,并且优化性能以及基本的测试。

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

一、写入表头

1、实现逻辑

1、定义一个注解,作用与类字段上,用于标识字段对应的表头名称;
2、定义一个 Excel 导出 VO 类,为每个字段添加注解,如果没有注解,则默认采用字段名;
3、创建 WorkSheet 时,通过反射获取所有字段与注解,整合到一个 TreeMap,然后写入表头

2、Java 部分

新增注解类 ExcelColumn

/*** @version: V1.0* @author: 余衫马* @description: Excel导出属性* @data: 2024-11-28 10:26**/
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时可用
@Target(ElementType.FIELD) // 注解作用于字段
public @interface ExcelColumn {/*** 列名*/String value();}

调整构造方法

调整构造方法的参数,通过参数指定是否导出表头。

/*** @version: V1.0* @author: 余衫马* @description: Excel 导出处理器* @data: 2024-11-21 19:56**/
public class MyExportResultHandler implements ResultHandler<TestVo>, AutoCloseable {// 省略.../*** 构造方法* 初始化一个 Excel 对象*/public MyExportResultHandler() {this(DEFAULT_SHEET_NAME, true, ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse());}/*** 构造方法* 初始化一个 Excel 对象** @param writeHeader 是否需要写入表头,默认 true*/public MyExportResultHandler(String sheetName, boolean writeHeader, HttpServletResponse response) {// 基本配置this.sheetName = StringUtils.isEmpty(sheetName) ? DEFAULT_SHEET_NAME : sheetName;this.writeHeader = writeHeader;this.startRowIndex = writeHeader ? 1 : 0;this.httpServletResponse = response;// 解析字段this.fieldMap = getExportFieldMap();// 创建 Excel 指针Object[] objects = this.fieldMap.keySet().toArray();this.handle = writeHeader ? createWorksheet(this.sheetName, objects, objects.length) : createWorksheet(this.sheetName);}// 省略...
}

控制数据顺序

封装方法 getExportFieldMap 将导出类字段转为 TreeMap,这用于控制表头顺序与写入数据的顺序。

/*** @version: V1.0* @author: 余衫马* @description: Excel 导出处理器* @data: 2024-11-21 19:56**/
public class MyExportResultHandler implements ResultHandler<TestVo>, AutoCloseable {// 省略.../*** 将导出类字段转为 TreeMap** @return TreeMap*/private TreeMap<String, String> getExportFieldMap() {// 创建 TreeMap 来存储字段名和注解值TreeMap<String, String> fieldMap = new TreeMap<>();// 获取 TestVo 类的所有字段Field[] fields = TestVo.class.getDeclaredFields();// 遍历所有字段for (Field field : fields) {// 检查字段是否有 ExcelColumn 注解if (field.isAnnotationPresent(ExcelColumn.class)) {// 获取注解ExcelColumn excelColumn = field.getAnnotation(ExcelColumn.class);// 将注解值和字段名存储到 TreeMap 中fieldMap.put(excelColumn.value(), field.getName());} else {// 如果没有注解,使用字段名作为键fieldMap.put(field.getName(), field.getName());}}// 打印 TreeMap 内容for (String key : fieldMap.keySet()) {System.out.println(key + " -> " + fieldMap.get(key));}return fieldMap;}// 省略...
}

写入数据逻辑

写入数据时,按照 TreeMap 顺序。

/*** @version: V1.0* @author: 余衫马* @description: Excel 导出处理器* @data: 2024-11-21 19:56**/
public class MyExportResultHandler implements ResultHandler<TestVo>, AutoCloseable {// 省略.../*** 获取类字段值* @param obj 类实例* @param fieldName 字段名* @return Object* @throws Exception*/public static Object getFieldValueUsingGetter(Object obj, String fieldName) throws Exception {// 获取类对象Class<?> clazz = obj.getClass();// 构造getter方法名String getterName = "get" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);// 获取getter方法Method getterMethod = clazz.getMethod(getterName);// 调用getter方法并返回值return getterMethod.invoke(obj);}@Overridepublic void handleResult(ResultContext<? extends TestVo> resultContext) {TestVo testVo = resultContext.getResultObject();int row = startRowIndex == 0 ? resultContext.getResultCount() - 1 : resultContext.getResultCount();int col = 0;try {// 处理逻辑:按照 TreeMap 顺序写入数据for (String key : fieldMap.keySet()) {String fieldName = fieldMap.get(key);writeToWorksheet(handle, row, col, getFieldValueUsingGetter(testVo, fieldName).toString());col = col + 1;}} catch (Exception e) {e.printStackTrace();}}// 省略...}    

3、Rust部分

改动 createWorksheet 函数,新建参数解析。

#[no_mangle]
pub extern "system" fn Java_com_yushanma_crazyexcel_handler_MyExportResultHandler_createWorksheet(mut env: JNIEnv,_class: JClass,sheet_name: JObject,header_array: JObjectArray,header_length: jint,
) -> jlong {// worksheet 为空if sheet_name.is_null() {eprintln!("sheet name argument is null");return 0;}// 需要写表头但是为空if header_length > 0 && header_array.is_null() {eprintln!("excel header argument is null");return 0;}let name: String = match unsafe { env.get_string_unchecked(&sheet_name.into()) } {Ok(java_str) => java_str.into(),Err(e) => {eprintln!("Couldn't get java string: {:?}", e);return 0;}};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().set_name(name.as_str()).unwrap() };// 写入表头if header_length > 0 {let mut data: Vec<String> = Vec::with_capacity(header_length as usize);for i in 0..header_length {let e = env.get_object_array_element(&header_array, i).unwrap();data.push(unsafe { env.get_string_unchecked(&e.into()).unwrap().into() });}if let Err(e) = worksheet.write_row(0, 0, &data) {eprintln!("Failed to write to worksheet: {:?}", e);// 释放指针unsafe {let _ = Box::from_raw(workbook_ptr);}return 0;}}let handle = Box::new(WorksheetHandle {workbook: workbook_ptr,worksheet,});Box::into_raw(handle) as jlong
}

4、运行效果

给 TestVo 的后 5 个字段添加表头注解,

在这里插入图片描述
在这里插入图片描述

可以看到表头与数据的顺序保持一致。

二、配置样式

1、常见样式

宽度高度

// Set the column width for clarity.
worksheet.set_column_width(0, 22)?;// Set the row height in Excel character units.
worksheet.set_row_height(0, 30)?;

字体加粗

let bold_format = Format::new().set_bold();
// Write a string with the bold format defined above.
worksheet.write_with_format(1, 0, "World", &bold_format)?;

日期格式

let date_format = Format::new().set_num_format("yyyy-mm-dd");
// Write a date.
let date = ExcelDateTime::from_ymd(2024, 11, 30)?;
worksheet.write_with_format(6, 0, &date, &date_format)?;

小数格式

let decimal_format = Format::new().set_num_format("0.000");
// Write a number with formatting.
worksheet.write_with_format(4, 0, 3.00, &decimal_format)?;

颜色相关

// 字体颜色
let format = Format::new().set_font_color(Color::Red);
// let format = Format::new().set_font_color(Color::RGB(0x000000));
worksheet.write_string_with_format(0, 0, "Wheelbarrow", &format)?;// 背景颜色
let format1 = Format::new().set_background_color(Color::Green);
// let format1 = Format::new().set_font_color(Color::RGB(0x000000));
worksheet.write_string_with_format(0, 0, "Rust", &format1)?;

Url 超链接

// Write some links.
worksheet.write(7, 0, Url::new("https://www.rust-lang.org"))?;
worksheet.write(8, 0, Url::new("https://www.rust-lang.org").set_text("Rust"))?;

2、实现逻辑

  • Java:添加注解属性,拓展对应字段的属性,同样通过反射机制将属性解析到 TreeMap<String,Object>
  • Rust:解析是否有格式,没有则采用默认格式

3、实现表头样式

Java 部分

新增注解属性,

/*** @version: V1.0* @author: 余衫马* @description: Excel导出属性* @data: 2024-11-28 10:26**/
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时可用
@Target(ElementType.FIELD) // 注解作用于字段
public @interface ExcelColumn {/*** 列名*/String value();/*** 宽度* @return int*/int columnWidth() default 0;/*** 高度* @return int*/int rowHeight() default 0;/*** 字体加粗* @return boolean*/boolean bold() default false;/*** 时间格式* @return String*/String dateFormat() default "yyyy-mm-dd";/*** 小数格式* @return String*/String decimalFormat() default "0.000";/*** 字体颜色* @return int*/int fontColor() default 0x000000;/*** 背景颜色* @return int*/int backgroundColor() default 0xFFFFFF;/*** 是否超链接* @return boolean*/boolean isLink() default false;
}

构造函数解析导出属性,

/*** @version: V1.0* @author: 余衫马* @description: Excel 导出处理器* @data: 2024-11-21 19:56**/
public class MyExportResultHandler implements ResultHandler<TestVo>, AutoCloseable {// 省略.../*** 解析字段类型** @param fieldType 字段* @return int*/private int parseFieldType(Class<?> fieldType) {// 确定字段类型if (fieldType == String.class) {// "String"return 0;} else if (fieldType == Date.class) {// "Date"return 1;} else if (fieldType == java.sql.Timestamp.class || fieldType == java.time.LocalDateTime.class) {// "DateTime"return 2;} else if (Number.class.isAssignableFrom(fieldType) || fieldType.isPrimitive()) {// "Number"return 3;} else {// "Unknown"return 99;}}/*** 将导出类字段转为 TreeMap** @return TreeMap*/public TreeMap<String, Object> getExportFieldMap() {// 创建 TreeMap 来存储字段名和注解值TreeMap<String, Object> fieldMap = new TreeMap<>();// 获取 TestVo 类的所有字段Field[] fields = TestVo.class.getDeclaredFields();// 遍历所有字段for (Field field : fields) {int fieldType = parseFieldType(field.getType());// 创建 ExcelColumnProperties 对象ExcelColumnProperties properties = new ExcelColumnProperties();properties.setFieldType(fieldType);properties.setFieldName(field.getName());// 检查字段是否有 ExcelColumn 注解if (field.isAnnotationPresent(ExcelColumn.class)) {// 获取注解ExcelColumn excelColumn = field.getAnnotation(ExcelColumn.class);// 设置属性properties.setColumnName(excelColumn.value());properties.setColumnWidth(excelColumn.columnWidth());properties.setRowHeight(excelColumn.rowHeight());properties.setBold(excelColumn.bold());properties.setDateFormat(excelColumn.dateFormat());properties.setDecimalFormat(excelColumn.decimalFormat());properties.setFontColor(excelColumn.fontColor());properties.setBackgroundColor(excelColumn.backgroundColor());properties.setIsLink(excelColumn.isLink());// 将注解值和字段名存储到 TreeMap 中fieldMap.put(excelColumn.value(), properties);} else {// 设置默认属性properties.setColumnName(field.getName());properties.setColumnWidth(0);properties.setRowHeight(0);properties.setBold(false);properties.setDateFormat(DEFAULT_DATE_FORMAT);properties.setDecimalFormat(DEFAULT_DECIMAL_FORMAT);properties.setFontColor(DEFAULT_FONT_COLOR_RGB);properties.setBackgroundColor(0xFFFFFF);properties.setIsLink(DEFAULT_IS_LINK);// 将字段名存储到 TreeMap 中fieldMap.put(field.getName(), properties);}}// 打印 TreeMap 内容for (String key : fieldMap.keySet()) {System.out.println(key + " -> " + fieldMap.get(key));}return fieldMap;}// 省略...
}

创建 worksheet 时使用默认表头样式,

/*** @version: V1.0* @author: 余衫马* @description: Excel 导出处理器* @data: 2024-11-21 19:56**/
public class MyExportResultHandler implements ResultHandler<TestVo>, AutoCloseable {// 省略...private final static Integer DEFAULT_COLUMN_WIDTH = 50;private final static Integer DEFAULT_ROW_HEIGHT = 26;private final static Boolean DEFAULT_BOLD = true;private final static Integer DEFAULT_FONT_COLOR_RGB = 0x000000;private final static Integer DEFAULT_BG_COLOR_RGB = 0x808080;private final static Boolean DEFAULT_IS_LINK = false;private final static String DEFAULT_DATE_FORMAT = "yyyy-mm-dd";private final static String DEFAULT_DECIMAL_FORMAT = "0.000";/*** 创建一个 Worksheet** @param sheetName          worksheet name* @param headers            表头数组* @param length             表头列数* @param columnWidth        列宽度* @param rowHeight          行高度* @param bold               字体加粗* @param fontColorRgb       字体颜色* @param backgroundColorRgb 背景颜色* @return 指针 handle*/private native long createWorksheet(String sheetName, Object[] headers, int length,int columnWidth, int rowHeight, boolean bold,int fontColorRgb, int backgroundColorRgb);/*** 构造方法* 初始化一个 Excel 对象** @param writeHeader 是否需要写入表头,默认 true*/public MyExportResultHandler(String sheetName, boolean writeHeader, HttpServletResponse response) {// 基本配置this.sheetName = StringUtils.isEmpty(sheetName) ? DEFAULT_SHEET_NAME : sheetName;this.writeHeader = writeHeader;this.startRowIndex = writeHeader ? 1 : 0;this.httpServletResponse = response;// 解析字段this.fieldMap = getExportFieldMap();// 创建 Excel 指针Object[] objects = this.fieldMap.keySet().toArray();this.handle = writeHeader ?createWorksheet(this.sheetName, objects, objects.length,HEADER_COLUMN_WIDTH, HEADER_ROW_HEIGHT, HEADER_BOLD, HEADER_FONT_COLOR_RGB, HEADER_BG_COLOR_RGB): createWorksheet(this.sheetName);}// 省略...
}    

Rust 部分


#[no_mangle]
pub extern "system" fn Java_com_yushanma_crazyexcel_handler_MyExportResultHandler_createWorksheet(mut env: JNIEnv,_class: JClass,sheet_name: JObject,header_array: JObjectArray,header_length: jint,column_width: jint,row_height: jint,bold: jboolean,font_color: jint,bg_color: jint,
) -> jlong {// worksheet 为空if sheet_name.is_null() {eprintln!("sheet name argument is null");return 0;}// 需要写表头但是为空if header_length > 0 && header_array.is_null() {eprintln!("excel header argument is null");return 0;}let name: String = match unsafe { env.get_string_unchecked(&sheet_name.into()) } {Ok(java_str) => java_str.into(),Err(e) => {eprintln!("Couldn't get java string: {:?}", e);return 0;}};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().set_name(name.as_str()).unwrap()};// 写入表头if header_length > 0 {// Set the row height in Excel character units.if row_height > 0 {worksheet.set_row_height(0, row_height).unwrap();}let mut data: Vec<String> = Vec::with_capacity(header_length as usize);for i in 0..header_length {// Set the column width for clarity.if column_width > 0 {worksheet.set_column_width(i as u16, column_width).unwrap();}let e = env.get_object_array_element(&header_array, i).unwrap();data.push(unsafe { env.get_string_unchecked(&e.into()).unwrap().into() });}// 样式let mut format = Format::new();// 字体颜色format = format.set_font_color(Color::RGB(font_color as u32));// 背景颜色format = format.set_background_color(Color::RGB(bg_color as u32));// 字体大小format = format.set_font_size(20);// 边框format = format.set_border(FormatBorder::Thin);// 字体加粗if bold == 1 {format = format.set_bold();}// 写入数据if let Err(e) = worksheet.write_row_with_format(0, 0, &data, &format) {eprintln!("Failed to write to worksheet: {:?}", e);// 释放指针unsafe {let _ = Box::from_raw(workbook_ptr);}return 0;}}let handle = Box::new(WorksheetHandle {workbook: workbook_ptr,worksheet,});Box::into_raw(handle) as jlong
}

4、实现内容样式

Java 部分

/*** @version: V1.0* @author: 余衫马* @description: Excel 导出处理器* @data: 2024-11-21 19:56**/
public class MyExportResultHandler implements ResultHandler<TestVo>, AutoCloseable {// 省略.../*** 把数据写入 Worksheet** @param handle             指针* @param row                行* @param col                列* @param text               内容* @param columnWidth        列宽度* @param rowHeight          行高度* @param bold               字体加粗* @param dateFormat         日期格式* @param decimalFormat      小数格式* @param fontColorRgb       字体颜色* @param backgroundColorRgb 背景颜色* @param isLink             是否超链接* @param fieldType          字段类型*/private native void writeToWorksheet(long handle, int row, int col, String text,int columnWidth, int rowHeight, boolean bold,String dateFormat, String decimalFormat,int fontColorRgb, int backgroundColorRgb, boolean isLink,int fieldType);@Overridepublic void handleResult(ResultContext<? extends TestVo> resultContext) {//invoke(resultContext.getResultObject());TestVo testVo = resultContext.getResultObject();int row = startRowIndex == 0 ? resultContext.getResultCount() - 1 : resultContext.getResultCount();int col = 0;try {// 处理逻辑:按照 TreeMap 顺序写入数据for (String key : fieldMap.keySet()) {ExcelColumnProperties properties = (ExcelColumnProperties) fieldMap.get(key);String fieldName = properties.getFieldName();writeToWorksheet(handle, row, col, getFieldValueUsingGetter(testVo, fieldName).toString(),properties.getColumnWidth(),properties.getRowHeight(),properties.getBold(),properties.getDateFormat(),properties.getDecimalFormat(),properties.getFontColor(),properties.getBackgroundColor(),properties.getIsLink(),);col = col + 1;}} catch (Exception e) {e.printStackTrace();}}// 省略...}

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,column_width: jint,row_height: jint,bold: jboolean,date_format: JString,decimal_format: JString,font_color: jint,bg_color: jint,is_link: jboolean,field_type: jint,
) {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 worksheet: &mut Worksheet = unsafe { &mut *handle.worksheet };// 列宽度if column_width > 0 {worksheet.set_column_width(col, column_width).unwrap();}// 行高度if row_height > 0 {worksheet.set_row_height(row, row_height).unwrap();}// 样式let mut format = Format::new();// 字体颜色format = format.set_font_color(Color::RGB(font_color as u32));// 背景颜色format = format.set_background_color(Color::RGB(bg_color as u32));// 边框format = format.set_border(FormatBorder::Thin);// 字体加粗if bold == 1 {format = format.set_bold();}// 写入 URLif is_link == 1 {// 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;}};if let Err(e) = worksheet.write_with_format(row, col, Url::new(content), &format) {eprintln!("Failed to write to worksheet: {:?}", e);}return;}// 写入字符串if field_type == 0 {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;}};if let Err(e) = worksheet.write_string_with_format(row, col, content, &format) {eprintln!("Failed to write to worksheet: {:?}", e);}return;}// 写入日期时间if field_type == 1 || field_type == 2 {// 日期格式if !date_format.is_null() {let date_format: String = match unsafe { env.get_string_unchecked(&date_format.into()) }{Ok(java_str) => java_str.into(),Err(e) => {eprintln!("Couldn't get java string: {:?}", e);return;}};format = format.set_num_format(date_format);}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 datetime = ExcelDateTime::parse_from_str(content.as_str()).unwrap();if let Err(e) = worksheet.write_datetime_with_format(row, col, datetime, &format) {eprintln!("Failed to write to worksheet: {:?}", e);}return;}// 写入数字if field_type == 3 {//小数格式if !decimal_format.is_null() {let decimal_format: String =match unsafe { env.get_string_unchecked(&decimal_format.into()) } {Ok(java_str) => java_str.into(),Err(e) => {eprintln!("Couldn't get java string: {:?}", e);return;}};format = format.set_num_format(decimal_format);}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 number = match content.parse::<f64>() {Ok(number) => number,Err(e) => {eprintln!("Failed to parse the string: {}", e);return;}};if let Err(e) = worksheet.write_number_with_format(row, col, number, &format) {eprintln!("Failed to write to worksheet: {:?}", e);}return;}
}

效果,

在这里插入图片描述

三、泛型抽象

/*** @version: V1.0* @author: 余衫马* @description: BaseExcel 导出处理器* @data: 2024-12-03 18:00**/
public abstract class BaseExportResultHandler<T> implements ResultHandler<T> {// 省略.../*** 构造方法* 初始化一个 Excel 对象** @param writeHeader 是否需要写入表头,默认 true*/public BaseExportResultHandler(String sheetName, boolean writeHeader, HttpServletResponse response) {// 基本配置this.sheetName = StringUtils.isEmpty(sheetName) ? DEFAULT_SHEET_NAME : sheetName;this.writeHeader = writeHeader;this.startRowIndex = writeHeader ? 1 : 0;this.httpServletResponse = response;// 解析字段this.fieldMap = getExportFieldMap();// 创建 Excel 指针Object[] objects = this.fieldMap.keySet().toArray();this.handle = writeHeader ?createWorksheet(this.sheetName, objects, objects.length,DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, DEFAULT_BOLD, DEFAULT_FONT_COLOR_RGB, DEFAULT_BG_COLOR_RGB): createWorksheet(this.sheetName);}/*** 通过 ResultHandler 流式查询数据,由子类自定义实现*/public abstract void fetchDataByStream();/*** 开始导出* @throws IOException*/public void startExportExcel() 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 {// 子类流式获取数据this.fetchDataByStream();// 写入响应流给客户端writeToStream(handle, outputStream);} finally {if (handle != 0) {freeWorksheetHandle(handle);handle = 0;}// 关闭流if (outputStream != null) {outputStream.flush();outputStream.close();}}}// 省略...}

ServiceImpl 调用,

/*** @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("mysheet1", true, response)) {
//            testDao.selectMillionData(handler);
//        }BaseExportResultHandler<TestVo> handler = new BaseExportResultHandler<TestVo>("sheetName1", true, response) {/*** 子类重写 fetchDataByStream ,自定义获取数据的方式*/@Overridepublic void fetchDataByStream() {// 这里的this 指的就是 BaseExportResultHandler<TestVo> handler 这个对象,在这里写 mapper 调用获取数据的调用testDao.selectMillionData(this);}};// startExportExcel 方法中调用 fetchDataByStream 方法,// 而 fetchDataByStream 方法中 selectMillionData 方法会调用 handler 中的 handleResult 方法handler.startExportExcel();// 计算两个时间点之间的 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("数量:100W20行,耗时:%d 分 %d 秒 %d 毫秒\n", minutes, seconds, millis);}
}

导出一波10W行20列的数据,效果如下:

在这里插入图片描述
在这里插入图片描述

耗时:0 分 17 秒 37 毫秒

四、反射优化

这里使用了反射机制,会消耗性能,

    /*** 获取类字段值** @param obj       类实例* @param fieldName 字段名* @return Object* @throws Exception*/public static Object getFieldValueUsingGetter(Object obj, String fieldName) throws Exception {// 获取类对象Class<?> clazz = obj.getClass();// 构造getter方法名String getterName = "get" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);// 获取getter方法Method getterMethod = clazz.getMethod(getterName);// 调用getter方法并返回值return getterMethod.invoke(obj);}

使用缓存优化一下,

    /*** 获取类字段值** @param obj       类实例* @param fieldName 字段名* @return Object* @throws Exception*/public Object getFieldValueUsingGetter(Object obj, String fieldName) throws Exception {Class<?> clazz = obj.getClass();String getterName = "get" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);// 从缓存中获取方法Method getterMethod = methodCache.computeIfAbsent(getterName, k -> {try {return clazz.getMethod(k);} catch (NoSuchMethodException e) {throw new RuntimeException(e);}});// 调用getter方法并返回值return getterMethod.invoke(obj);}

数量:100W20行,耗时:3 分 35 秒 792 毫秒,生成文件大小约 485M。而同样的数据量级下,EasyExcel 需要 4分20秒左右。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以看到 CPU 跟堆内存使用情况表现良好,

在这里插入图片描述
在这里插入图片描述

五、导出 Jar 包

新建一个 Maven 项目,

在这里插入图片描述

1、修改外部库加载方式

在Java中,System.loadLibrary 方法只能加载系统库路径或 java.library.path 中的库,而不能直接从JAR包内部加载DLL文件。为了从JAR包内加载DLL,我们需要先将DLL提取到一个临时目录,然后再加载它。

/*** @version: V1.0* @author: 余衫马* @description: 从 Jar 包内加载外部库* @data: 2024-12-06 13:46**/
public class NativeUtils {public static void loadLibraryFromJar(String path) throws IOException {// 获取操作系统的临时目录String tempDir = System.getProperty("java.io.tmpdir");File tempFile = new File(tempDir, new File(path).getName());try (InputStream is = NativeUtils.class.getResourceAsStream(path);FileOutputStream fos = new FileOutputStream(tempFile)) {if (is == null) {throw new IllegalArgumentException("File " + path + " was not found inside JAR.");}byte[] buffer = new byte[1024];int bytesRead;while ((bytesRead = is.read(buffer)) != -1) {fos.write(buffer, 0, bytesRead);}}System.load(tempFile.getAbsolutePath());}public static void loadLibrary(String libName) throws IOException {String osName = System.getProperty("os.name").toLowerCase();String libPath = "/libs/";if (osName.contains("win")) {libPath += "windows/";} else if (osName.contains("mac")) {libPath += "macos/";} else if (osName.contains("nix") || osName.contains("nux") || osName.contains("aix")) {libPath += "linux/";} else {throw new UnsupportedOperationException("Unsupported operating system: " + osName);}libPath += "x86_64/";libPath += System.mapLibraryName(libName);loadLibraryFromJar(libPath);}}
/*** @version: V1.0* @author: 余衫马* @description: BaseExcel 导出处理器* @data: 2024-12-03 18:00**/
public abstract class BaseExportResultHandler<T> implements ResultHandler<T> {// 省略...static {try {NativeUtils.loadLibrary("my_excel_writer_lib");} catch (IOException e) {e.printStackTrace();throw new RuntimeException("Failed to load native library", e);}}// 省略...
}

2、重新编译 Rust 库文件

查看支持的 target

rustc --print target-list

在这里插入图片描述

分别编译三个平台的库文件,

cargo build --release --target x86_64-pc-windows-msvc
cargo build --release --target x86_64-unknown-linux-gnu
cargo build --release --target x86_64-apple-darwin

win10

cargo build --release --target x86_64-pc-windows-msvc

在这里插入图片描述

可以看到 build 成功。

linux

cargo build --release --target x86_64-unknown-linux-gnu

如果编译 x86_64-unknown-linux-gnu 报错,则需要先安装目标架构,

error[E0463]: can't find crate for `core`|= note: the `x86_64-unknown-linux-gnu` target may not be installed= help: consider downloading the target with `rustup target add x86_64-unknown-linux-gnu`For more information about this error, try `rustc --explain E0463`.
error: could not compile `cfg-if` (lib) due to 1 previous error
warning: build failed, waiting for other jobs to finish...
error[E0463]: can't find crate for `std`                                                                                           |= note: the `x86_64-unknown-linux-gnu` target may not be installed= help: consider downloading the target with `rustup target add x86_64-unknown-linux-gnu`

在这里插入图片描述

执行指令安装,

# 安装 x86_64-unknown-linux-gnu 目标架构
rustup target add x86_64-unknown-linux-gnu

在这里插入图片描述

没有编译环境的话,直接到 Linux 上编译也可以,

在这里插入图片描述

这里在 Deepin 系统下 build 成功。

macos

cargo build --release --target x86_64-apple-darwin

如果编译 x86_64-apple-darwin 报错,则需要先安装目标架构,

error[E0463]: can't find crate for `core`|= note: the `x86_64-apple-darwin` target may not be installed= help: consider downloading the target with `rustup target add x86_64-apple-darwin`For more information about this error, try `rustc --explain E0463`.
error: could not compile `cfg-if` (lib) due to 1 previous error
warning: build failed, waiting for other jobs to finish...
error[E0463]: can't find crate for `std`|= note: the `x86_64-apple-darwin` target may not be installed= help: consider downloading the target with `rustup target add x86_64-apple-darwin`

在这里插入图片描述

如果是 MacOS 直接安装工具链编译即可,

# rustup toolchain install stable-x86_64-apple-darwin

在这里插入图片描述

这里我是 Deepin 系统,即便安装 toolchain 也用不了,不过可以先安装 target,然后交叉编译,

rustup target add x86_64-apple-darwin

在这里插入图片描述

直接跑编译会报错,

cargo build --release --target x86_64-apple-darwin

在这里插入图片描述

接下来我们安装 MacOS 跨平台编译工具链:

# Step1
git clone https://github.com/tpoechtrager/osxcross
# Step2
cd osxcross
# Step3
wget -nc https://s3.dockerproject.org/darwin/v2/MacOSX10.10.sdk.tar.xz
# Step4
mv MacOSX10.10.sdk.tar.xz tarballs/
# Step5
UNATTENDED=yes OSX_VERSION_MIN=10.10 ./build.sh

需要 clang ,

在这里插入图片描述

# Required dependency 'clang' is not installed
# 安装基础构建工具
# sudo apt install build-essential
sudo apt install clang

在这里插入图片描述
在这里插入图片描述

需要 cmake ,

(base) sam@sam-PC:~/AwesomeWorkSpace/RustStudy/osxcross$ UNATTENDED=yes OSX_VERSION_MIN=10.10 ./build.sh
Required dependency 'cmake' is not installed
sudo apt install cmake

在这里插入图片描述

所需环境可以在 readme 文档中看到,

在这里插入图片描述
在这里插入图片描述

重新执行,

UNATTENDED=yes OSX_VERSION_MIN=10.10 ./build.sh

在这里插入图片描述

遇到报错 error while loading shared libraries: libicui18n.so.73

testing i386-apple-darwin14-clang++ -stdlib=libc++ -std=c++11 ... failed (ignored)
testing x86_64-apple-darwin14-clang++ -stdlib=libc++ -std=c++11 ... failed (ignored)testing i386-apple-darwin14-clang ... /home/sam/AwesomeWorkSpace/RustStudy/osxcross/target/bin/i386-apple-darwin14-ld: error while loading shared libraries: libicui18n.so.73: cannot open shared object file: No such file or directory
clang: error: unable to execute command: No such file or directory
clang: error: linker command failed due to signal (use -v to see invocation)

关键在于没有 libicui18n.so 这个库,

sudo apt-get install libicu-dev

在这里插入图片描述

系统自带的版本太低了,我们从源码编译安装,

# https://icu.unicode.org/download/73
wget https://github.com/unicode-org/icu/releases/download/release-73-1/icu4c-73_1-src.tgz#编译与安装
tar -xzf icu4c-73_1-src.tgz 
cd icu/source 
./configure 
make 
sudo make install
sudo ldconfig

在这里插入图片描述

重新执行,

UNATTENDED=yes OSX_VERSION_MIN=10.10 ./build.sh

在这里插入图片描述

可以看到已经 build 成功,需要配置系统变量,

sudo vim ~/.bashrc
# 添加路径
export PATH="/home/sam/AwesomeWorkSpace/RustStudy/osxcross/target/bin:$PATH"
# 刷新
source  ~/.bashrc

配置交叉编译工具后,回归主线继续编译 x86_64-apple-darwin,修改 Cargo.toml 指定链接器,

[target.x86_64-apple-darwin]
linker = "o64-clang"
ar = "x86_64-apple-darwin-ar"
# 指定交叉编译的编译器
export CC=o64-clang
export CXX=o64-clang++# 告诉 Cargo 使用正确的链接器和存档工具
export CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER=o64-clang
export CARGO_TARGET_X86_64_APPLE_DARWIN_AR=x86_64-apple-darwin-ar# 设置当前会话环境变量
export CC_x86_64_apple_darwin=o64-clang
export CXX_x86_64_apple_darwin=o64-clang++# 设置 SDKROOT 环境变量
# export SDKROOT=/path/to/osxcross/target/SDK/MacOSX10.10.sdk# 清理项目
# cargo clean# 编译
cargo build -Zbuild-std --release --target x86_64-apple-darwin# 查看输出,确保调用了 o64-clang 而不是系统的 cc
# cargo build --target x86_64-apple-darwin -vv

报错版本过高,

在这里插入图片描述

warning: deployment target in MACOSX_DEPLOYMENT_TARGET was set to 10.10, but the minimum supported by `rustc` is 10.12

在这里插入图片描述

"-mmacosx-version-min=10.12.0"note: osxcross: error: targeted macOS version must be <= 10.10.0 (SDK)osxcross: error: while detecting target

解决:升级 SDK 版本,这里升级到 10.14 版本。

在这里插入图片描述
在这里插入图片描述

注意:如果是使用自己的 Clang 环境,LLVM 编译的时候需要启用 lld,否则期间不能编译安装 TAPI ,举个例子,

TAPI(Text-based Stubs for System Libraries)是 Apple 用于处理动态库符号的一套工具。在构建 cctools-port(osxcross 使用的工具集)时,如果系统中缺少 libtapi,就会报错 ERROR: Failed TAPI checks in XXX。

# Step1、编译安装 llvm# 下载 llvm-project
wget -nc https://github.com/llvm/llvm-project/releases/download/llvmorg-18.1.8/llvm-project-18.1.8.src.tar.xz
# 解压 llvm-project-18.1.8.src.tar.xz
# cd llvm-project-18.1.8.src# 创建文件夹
mkdir build
cd build
# sudo apt install ninja-build
# 注意将默认的Debug模式换成Release模式.
cmake -G Ninja -DLLVM_ENABLE_PROJECTS=lld -DCMAKE_BUILD_TYPE=Release ../llvm
# 编译安装
sudo cmake --build . -j4 --target installcd ../# Step2、编译安装 clang
# 创建文件夹
mkdir clang-build
cd clang-build
# 注意将默认的Debug模式换成Release模式.
cmake -G Ninja -DLLVM_ENABLE_PROJECTS=lld -DCMAKE_BUILD_TYPE=Release ../clang
# 编译安装
sudo cmake --build . -j4 --target install

3、打包 jar

mvn install

在这里插入图片描述
在这里插入图片描述

六、使用 Jar 包

在 Resource 文件夹下新建一个 lib 文件夹,把 jar 包放到文件夹下,

在这里插入图片描述

添加项目依赖,

在这里插入图片描述

现在我们就可以直接使用 jar 包中的方法了,

在这里插入图片描述

或者直接在 pom.xml 添加本地 jar 依赖,这适用于 Maven 项目,

        <dependency><groupId>com.yushanma</groupId><artifactId>BaseCrazyExcel</artifactId><version>1.0.0</version></dependency>

七、测试效果

Windows 下

1、10W行20列

耗时:0 分 20 秒 625 毫秒

在这里插入图片描述

2、50W行20列

耗时:1 分 36 秒 533 毫秒

在这里插入图片描述

3、100W行20列

耗时:3 分 23 秒 413 毫秒

在这里插入图片描述

补充:远程查看 Linux JVM

Linux 配置策略

找到 Java 主目录,

(base) sam@sam-PC:~$ which java
/usr/bin/java
(base) sam@sam-PC:~$ ls -lrt /usr/bin/java
lrwxrwxrwx 1 root root 22 1112 17:38 /usr/bin/java -> /etc/alternatives/java
(base) sam@sam-PC:~$ ls -lrt /etc/alternatives/java
lrwxrwxrwx 1 root root 46 1112 17:38 /etc/alternatives/java -> /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java

在这里插入图片描述

在 usr/bin/ 下创建文件,

sudo vim jstatd.all.policy# 写入以下内容
grant codebase "file:/usr/lib/jvm/java-8-openjdk-amd64/lib/tools.jar" {permission java.security.AllPermission;
};

运行 jstatd,

nohup jstatd -J-Djava.security.policy=jstatd.all.policy -p 8066  >/dev/null 2>&1 &# 查看是否启动成功
# ps -ef |grep jstatd
# lsof -i:8066 

在这里插入图片描述

添加 JVM 启动参数,

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=XXXX
-Dcom.sun.management.jmxremote.rmi.port=XXXX
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false

JVM 调参,

-Xms1024m -Xmx10240m -Xmn3840m -Xss512k
java -Xms1024m -Xmx8192m -Xmn3840m -Xss512k -Djava.io.tmpdir=./tmp -Djava.rmi.server.hostname=0.0.0.0  -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8067 -Dcom.sun.management.jmxremote.rmi.port=8067 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -jar CrazyExcel-0.0.1-SNAPSHOT.jar

在这里插入图片描述

在本地电脑打 jdk下的bin目录启动 jvisualvm.exe,

在这里插入图片描述

添加远程主机,输入 ip,

在这里插入图片描述

添加 JMX 连接,输入端口,

在这里插入图片描述
在这里插入图片描述

这样就可以连接远程主机监控 JVM 状态了。如果是本地虚拟机,则使用内网 ip 连接:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com