大數據量導出 Excel 時,後端查詢數據庫、組裝對象、生成 Workbook 往往超過 Nginx/網關的 60s/120s 超時限制,導致 504 Gateway Timeout。本文提供一個 **CSV 流式輸出工具類**,利用 ServletOutputStream 分批 flush(),讓瀏覽器持續接收數據,徹底規避超時問題。
public class CSV {
private final OutputStream stream;
private final Map headers; // fieldName -> displayName
private static final Map FIELD_CACHE = new HashMap<>();
public CSV(HttpServletResponse response, Map headers,
String fileName, Class> clazz) throws IOException {
if (response == null || headers == null || fileName == null || clazz == null)
throw new IllegalArgumentException();
initFieldCache(clazz);
this.stream = response.getOutputStream();
this.headers = headers;
response.setContentType("application/octet-stream;charset=GBK");
response.setHeader("Content-Disposition", "attachment;fileName=" + fileName);
// 寫表頭
StringBuilder hb = new StringBuilder();
for (String h : headers.values()) hb.append(h).append(",");
stream.write(hb.substring(0, hb.length()-1).getBytes("GBK"));
stream.flush();
}
private static void initFieldCache(Class> clazz) {
if (!FIELD_CACHE.isEmpty()) return;
for (Field f : clazz.getDeclaredFields()) {
f.setAccessible(true);
FIELD_CACHE.put(f.getName(), f);
}
if (clazz.getSuperclass() != null) initFieldCache(clazz.getSuperclass());
}
public void write(List> data) throws IOException, IllegalAccessException {
for (Object obj : data) {
StringBuilder sb = new StringBuilder("\n");
for (String fieldName : headers.keySet()) {
Field f = FIELD_CACHE.get(fieldName);
Object val = f.get(obj);
if (val == null) { sb.append(" ,"); continue; }
if (f.getType() == Date.class) {
sb.append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(val)).append(",");
} else if (f.getType() == DateTime.class) { // Joda-Time
sb.append(((DateTime)val).toString("yyyy-MM-dd HH:mm:ss")).append(",");
} else {
String s = val.toString();
if (s.contains(",")) s = s.replace(",", "\",\""); // 簡易逗號轉義
sb.append(s).append(",");
}
}
stream.write(sb.substring(0, sb.length()-1).getBytes("GBK"));
stream.flush(); // 關鍵:每行即時推送給瀏覽器
}
}
public void close() throws IOException { stream.close(); }
}
headers = new LinkedHashMap<>();
headers.put("id", "ID");
headers.put("name", "用戶名");
headers.put("createTime", "註冊時間");
CSV csv = new CSV(response, headers, "users.csv", User.class);
int pageSize = 5000;
long offset = 0;
while (true) {
List batch = userService.selectPage(offset, pageSize);
if (batch.isEmpty()) break;
csv.write(batch);
offset += pageSize;
}
csv.close();
}
| 傳統 POI 導出 | CSV 流式導出 |
|---|---|
| 需在記憶體構建整個 Workbook,OOM 風險高 | 流式寫入,記憶體佔用恆定 |
| 耗時長,易觸發網關超時 | 邊查邊寫邊 flush,長連接保活 |
| 輸出 .xlsx,格式豐富但體積大 | 輸出 .csv,純文本,體積小、打開快 |
| 樣式、合併儲存格等高級功能 | 無樣式,純數據導出場景適用 |
此方案已在百萬級數據導出場景驗證穩定,徹底解決 504 超時痛點。