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