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

发布于:2024-12-18 ⋅ 阅读:(137) ⋅ 点赞:(0)

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

在上篇我们实现了最基础的导出功能,本篇我们拓展样式 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);
    }

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

#[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

网站公告

今日签到

点亮在社区的每一天
去签到