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

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

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

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

一、写入表头

1、实现逻辑

1、定义一个注解,作用与类字段上,用于标识字段对应的表头名称;

2、定义一个 Excel 导出 VO 类,为每个字段添加注解,如果没有注解,则默认采用字段名;

3、创建 WorkSheet 时,通过反射获取所有字段与注解,整合到一个 TreeMap,然后写入表头

2、Java 部分

新增注解类 ExcelColumn

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

    /**
     * 列名
     */
    String value();

}

调整构造方法

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

java 复制代码
/**
 * @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,这用于控制表头顺序与写入数据的顺序。

java 复制代码
/**
 * @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 顺序。

java 复制代码
/**
 * @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);
    }

    @Override
    public 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 函数,新建参数解析。

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,
) -> 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、常见样式

宽度高度

rust 复制代码
// 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)?;

字体加粗

rust 复制代码
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)?;

日期格式

rust 复制代码
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)?;

小数格式

rust 复制代码
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)?;

颜色相关

rust 复制代码
// 字体颜色
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 超链接

rust 复制代码
// 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 部分

新增注解属性,

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;
}

构造函数解析导出属性,

java 复制代码
/**
 * @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 时使用默认表头样式,

java 复制代码
/**
 * @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 部分

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 部分

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);

    @Override
    public 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 部分

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();
    }

    // 写入 URL
    if 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;
    }
}

效果,

三、泛型抽象

java 复制代码
/**
 * @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 调用,

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("mysheet1", true, response)) {
//            testDao.selectMillionData(handler);
//        }

        BaseExportResultHandler<TestVo> handler = new BaseExportResultHandler<TestVo>("sheetName1", true, response) {
            /**
             * 子类重写 fetchDataByStream ,自定义获取数据的方式
             */
            @Override
            public void fetchDataByStream() {
                // 这里的this 指的就是 BaseExportResultHandler<TestVo> handler 这个对象,在这里写 mapper 调用获取数据的调用
                testDao.selectMillionData(this);
            }
        };
        // startExportExcel 方法中调用 fetchDataByStream 方法,
        // 而 fetchDataByStream 方法中 selectMillionData 方法会调用 handler 中的 handleResult 方法
        handler.startExportExcel();

        // 计算两个时间点之间的 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("数量:100W20行,耗时:%d 分 %d 秒 %d 毫秒\n", minutes, seconds, millis);

    }
}

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


耗时:0 分 17 秒 37 毫秒

四、反射优化

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

java 复制代码
    /**
     * 获取类字段值
     *
     * @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);
    }

使用缓存优化一下,

java 复制代码
    /**
     * 获取类字段值
     *
     * @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提取到一个临时目录,然后再加载它。

java 复制代码
/**
 * @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);
    }

}
java 复制代码
/**
 * @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

bash 复制代码
rustc --print target-list

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

bash 复制代码
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

bash 复制代码
cargo build --release --target x86_64-pc-windows-msvc

可以看到 build 成功。

linux

bash 复制代码
cargo build --release --target x86_64-unknown-linux-gnu

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

bash 复制代码
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`

执行指令安装,

bash 复制代码
# 安装 x86_64-unknown-linux-gnu 目标架构
rustup target add x86_64-unknown-linux-gnu

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

这里在 Deepin 系统下 build 成功。

macos

bash 复制代码
cargo build --release --target x86_64-apple-darwin

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

bash 复制代码
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 直接安装工具链编译即可,

bash 复制代码
# rustup toolchain install stable-x86_64-apple-darwin

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

bash 复制代码
rustup target add x86_64-apple-darwin

直接跑编译会报错,

bash 复制代码
cargo build --release --target x86_64-apple-darwin

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

bash 复制代码
# 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 ,

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


需要 cmake ,

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

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


重新执行,

bash 复制代码
UNATTENDED=yes OSX_VERSION_MIN=10.10 ./build.sh

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

bash 复制代码
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 这个库,

bash 复制代码
sudo apt-get install libicu-dev

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

bash 复制代码
# 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

重新执行,

bash 复制代码
UNATTENDED=yes OSX_VERSION_MIN=10.10 ./build.sh

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

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

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

bash 复制代码
[target.x86_64-apple-darwin]
linker = "o64-clang"
ar = "x86_64-apple-darwin-ar"
bash 复制代码
# 指定交叉编译的编译器
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

报错版本过高,

bash 复制代码
warning: deployment target in MACOSX_DEPLOYMENT_TARGET was set to 10.10, but the minimum supported by `rustc` is 10.12
bash 复制代码
"-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。

bash 复制代码
# 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 install

cd ../

# 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

bash 复制代码
mvn install


六、使用 Jar 包

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

添加项目依赖,

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

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

bash 复制代码
        <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 主目录,

bash 复制代码
(base) sam@sam-PC:~$ which java
/usr/bin/java
(base) sam@sam-PC:~$ ls -lrt /usr/bin/java
lrwxrwxrwx 1 root root 22 11月 12 17:38 /usr/bin/java -> /etc/alternatives/java
(base) sam@sam-PC:~$ ls -lrt /etc/alternatives/java
lrwxrwxrwx 1 root root 46 11月 12 17:38 /etc/alternatives/java -> /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java

在 usr/bin/ 下创建文件,

bash 复制代码
sudo vim jstatd.all.policy

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

运行 jstatd,

bash 复制代码
nohup jstatd -J-Djava.security.policy=jstatd.all.policy -p 8066  >/dev/null 2>&1 &

# 查看是否启动成功
# ps -ef |grep jstatd
# lsof -i:8066 

添加 JVM 启动参数,

bash 复制代码
-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 调参,

bash 复制代码
-Xms1024m -Xmx10240m -Xmn3840m -Xss512k
bash 复制代码
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 连接:



相关推荐
ZPILOTE7 分钟前
日志基础示例python和c++
c++·python·日志·log·logger·glog
nuclear201117 分钟前
Python PPT合并与拆分 – 详解
python·合并ppt·合并幻灯片·拆分ppt·合并演示文稿·拆分演示文稿·ppt合并和拆分
Allen_LVyingbo20 分钟前
聚类及Python下实现 K-means 算法
python·算法
是Winky啊40 分钟前
【实践·专业课】内存管理-存储管理-文件系统
开发语言·python
只会HelloWorld的华娃42 分钟前
【Excel学习记录】01-认识Excel
学习·excel
blues_C1 小时前
Pytest-Bdd-Playwright 系列教程(14):Docstring 参数
自动化测试·python·pytest·测试框架·bdd
ymchuangke1 小时前
python基础:(七)类
开发语言·python
封步宇AIGC1 小时前
量化交易系统开发-实时行情自动化交易-8.25.真格(澎博财经旗下)平台
人工智能·python·机器学习·数据挖掘
工业互联网专业2 小时前
Python毕业设计选题:基于协同过滤的动漫推荐系统设计与实现_django+hive+spider
hive·python·django·源码·课程设计·spider