衡量一个人是否幸福,不应看他有多少高兴的事,而应看他是否为小事烦扰。只有幸福的人,才会把无关痛痒的小事挂心上。那些真正经历巨大灾难和深重痛苦的人,根本无暇顾及这些小事的。因此人们往往在失去幸福之后,才会发现它们曾经存在。
在上篇我们实现了最基础的导出功能,本篇我们拓展样式 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 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/ 下创建文件,
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 连接: