衡量一个人是否幸福,不应看他有多少高兴的事,而应看他是否为小事烦扰。只有幸福的人,才会把无关痛痒的小事挂心上。那些真正经历巨大灾难和深重痛苦的人,根本无暇顾及这些小事的。因此人们往往在失去幸福之后,才会发现它们曾经存在。
在上篇我们实现了最基础的导出功能,本篇我们拓展样式 API,封装为通用 Jar 包,提升开发者的使用友好度,并且优化性能以及基本的测试。
一、写入表头
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