Apache POI 雖功能強大但體積龐大、API 複雜。對於純導出 .xls (Excel 97-2003) 場景,JXL (Java Excel API) 以輕量級(<500KB)、API 簡潔、內存佔用低著稱。本文展示一個生產就緒的封裝工具類,支持 JDBC ResultSet 直接導出與內存對象列表導出雙模式,內置列寬自適應、標題顯示控制、日期/數值格式化等實用功能。
<dependency>
<groupId>net.sourceforge.jexcelapi</groupId>
<artifactId>jxl</artifactId>
<version>2.6.12</version>
</dependency>
package com.example.excel;
import jxl.Workbook;
import jxl.write.*;
import jxl.format.Alignment;
import jxl.format.Colour;
import jxl.format.VerticalAlignment;
import java.io.File;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
public final class JxlExportUtils {
private JxlExportUtils() {}
/**
* 通用導出入口:接受數據提供者介面,統一處理表頭、樣式、寫入
*/
public static void export(File file, DataProvider provider,
String[] headers, boolean[] showHeader, int[] colWidths)
throws IOException, WriteException {
WritableWorkbook workbook = null;
try {
workbook = Workbook.createWorkbook(file);
WritableSheet sheet = workbook.createSheet("Sheet1", 0);
// 1. 寫入表頭
WritableFont headerFont = new WritableFont(WritableFont.ARIAL, 10, WritableFont.BOLD);
WritableCellFormat headerFormat = new WritableCellFormat(headerFont);
headerFormat.setAlignment(Alignment.CENTRE);
headerFormat.setVerticalAlignment(VerticalAlignment.CENTRE);
headerFormat.setBackground(Colour.GRAY_25);
headerFormat.setBorder(Border.ALL, BorderLineStyle.THIN);
for (int i = 0; i < headers.length; i++) {
if (showHeader == null || showHeader[i]) {
sheet.addCell(new Label(i, 0, headers[i], headerFormat));
}
// 設置列寬 (JXL 寬度單位:1/256 字符寬度)
if (colWidths != null && i < colWidths.length && colWidths[i] > 0) {
sheet.setColumnView(i, colWidths[i]);
}
}
// 2. 寫入數據行
WritableCellFormat dataFormat = new WritableCellFormat();
dataFormat.setAlignment(Alignment.LEFT);
dataFormat.setVerticalAlignment(VerticalAlignment.CENTRE);
dataFormat.setBorder(Border.ALL, BorderLineStyle.THIN);
dataFormat.setWrap(true); // 自動換行
// 日期格式
WritableCellFormat dateFormat = new WritableCellFormat(dataFormat);
dateFormat.setFormat(new jxl.write.DateFormat("yyyy-MM-dd HH:mm:ss"));
int row = 1;
while (provider.hasNext()) {
Object[] rowData = provider.next();
for (int col = 0; col < rowData.length; col++) {
Object val = rowData[col];
if (val == null) {
sheet.addCell(new Label(col, row, "", dataFormat));
} else if (val instanceof Date) {
sheet.addCell(new jxl.write.DateTime(col, row, (Date) val, dateFormat));
} else if (val instanceof Number) {
sheet.addCell(new jxl.write.Number(col, row, ((Number) val).doubleValue(), dataFormat));
} else if (val instanceof Boolean) {
sheet.addCell(new jxl.write.Boolean(col, row, (Boolean) val, dataFormat));
} else {
sheet.addCell(new Label(col, row, val.toString(), dataFormat));
}
}
row++;
}
} finally {
if (workbook != null) {
workbook.write();
workbook.close();
}
}
}
/** 數據提供者介面:解耦數據源 */
public interface DataProvider {
boolean hasNext();
Object[] next();
void close() throws Exception;
}
// ========== 靜態工廠方法:構建常用 DataProvider ==========
/** JDBC ResultSet 模式 */
public static DataProvider fromResultSet(ResultSet rs) {
return new DataProvider() {
@Override public boolean hasNext() {
try { return rs.next(); } catch (SQLException e) { throw new RuntimeException(e); }
}
@Override public Object[] next() {
try {
ResultSetMetaData meta = rs.getMetaData();
int cols = meta.getColumnCount();
Object[] row = new Object[cols];
for (int i = 1; i <= cols; i++) row[i-1] = rs.getObject(i);
return row;
} catch (SQLException e) { throw new RuntimeException(e); }
}
@Override public void close() { try { rs.close(); } catch (SQLException ignored) {} }
};
}
/** 內存 List 模式 (Record 為簡單數組封裝) */
public static DataProvider fromRecords(List records) {
return new DataProvider() {
private int index = 0;
@Override public boolean hasNext() { return index < records.size(); }
@Override public Object[] next() { return records.get(index++).getValues(); }
@Override public void close() {}
};
}
/** 通用 List
public void exportUsersToExcel(File file) throws Exception {
String sql = "SELECT id, username, email, create_time, status FROM sys_user WHERE status=1";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
String[] headers = {"ID", "用戶名", "郵箱", "創建時間", "狀態"};
boolean[] show = {true, true, true, true, true};
int[] widths = {8, 20, 30, 22, 10};
JxlExportUtils.export(file, JxlExportUtils.fromResultSet(rs), headers, show, widths);
}
}
public void exportReport(List records, File file) throws Exception {
List data = records.stream()
.map(r -> new JxlExportUtils.Record(
r.getId(), r.getProduct(), r.getAmount(), r.getSaleDate(), r.getRegion()
)).toList();
String[] headers = {"訂單號", "產品", "金額", "銷售日期", "地區"};
int[] widths = {15, 25, 12, 20, 15};
JxlExportUtils.export(file, JxlExportUtils.fromRecords(data), headers, null, widths);
}
| 特性 | 實現細節 | 注意事項 |
|---|---|---|
| 大數據量導出 | ResultSet 模式流式讀寫,不載入全量內存 | 內存模式受限於 JVM Heap,超 5 萬行建議分批或用 SXSSF (POI) |
| 列寬單位 | JXL: 1/256 字符寬度;POI: 1/256 * 256 = 字符數 | 經驗值:中文字符寬度約 2 倍英文,預留 20-40 |
| 日期格式 | WritableCellFormat + DateFormat("yyyy-MM-dd") | 需與數據庫時區一致,避免時區偏移 |
| 數值精度 | Number 儲存 double,大金額用 BigDecimal 轉 String | 避免科學計數法顯示 |
| 中文文件名 | HttpServletResponse.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(name, "UTF-8")) | 瀏覽器兼容性需測試 |
| .xlsx 支持 | JXL 不支持,需遷移 Apache POI (XSSF/SXSSF) | 新項目建議直接用 POI |
JXL 以極小體積、極簡 API、流式寫入低內存特性,成為 .xls 導出場景的經典選擇。封裝 DataProvider 介面實現數據源解耦,一套代碼同時服務 JDBC 與內存模式。對於現代項目需導出 .xlsx、超大數據量、複雜樣式(合併單元格、圖表、宏),請直接採用 Apache POI SXSSF (流式) 或 EasyExcel (阿里開源)。