使用 Microsoft.Office.Interop.Excel 產生報表時,若未正確釋放 COM 物件,伺服器將殘留大量 EXCEL.EXE 進程,耗盡記憶體與授權。
// 正確關閉與釋放順序
workbook.Close(false, Type.Missing, Type.Missing);
app.Workbooks.Close();
app.Quit();
// 由內而外釋放 COM 物件
Marshal.ReleaseComObject(range);
Marshal.ReleaseComObject(worksheet);
Marshal.ReleaseComObject(workbook);
Marshal.ReleaseComObject(app);
// 明確設為 null
range = worksheet = workbook = app = null;
// 強制 GC 回收 RCW
GC.Collect();
GC.WaitForPendingFinalizers();
原則:每個透過 Interop 取得的 COM 物件(Application、Workbook、Worksheet、Range 等)都必須對應一次 ReleaseComObject,順序由細顆粒度到粗顆粒度。
public void GenerateExcelReport(string storedProc, SqlParameter[] parameters)
{
string folder = Server.MapPath("~/Reports/");
CleanOldFiles(folder, TimeSpan.FromHours(1)); // 清理 1 小時前檔案
Application app = null;
Workbook wb = null;
Worksheet ws = null;
Range rng = null;
try
{
GC.Collect(); // 預先清理
app = new Application { Visible = false, DisplayAlerts = false };
wb = (Workbook)app.Workbooks.Add(Type.Missing);
ws = (Worksheet)wb.ActiveSheet;
// 取得資料
using (var reader = new StoredProcRunner(connStr, storedProc, parameters).ExecuteReader())
{
// 寫入標題列
for (int i = 0; i < reader.FieldCount; i++)
ws.Cells[1, i + 1] = reader.GetName(i);
// 寫入資料列
int row = 2;
while (reader.Read())
{
for (int i = 0; i < reader.FieldCount; i++)
ws.Cells[row, i + 1] = reader.GetValue(i)?.ToString();
row++;
}
}
// 格式化
rng = ws.get_Range("A1", $"{(char)('A' + reader.FieldCount - 1)}1");
rng.Font.Bold = true;
rng.VerticalAlignment = XlVAlign.xlVAlignCenter;
rng.EntireColumn.AutoFit();
// 儲存
string fileName = $"Report_{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid():N}.xlsx";
string fullPath = Path.Combine(folder, fileName);
wb.SaveAs(fullPath, XlFileFormat.xlOpenXMLWorkbook);
// 提供下載連結
string url = $"{Request.Url.GetLeftPart(UriPartial.Authority)}/Reports/{fileName}";
Response.Write($"下載報表");
}
catch (Exception ex)
{
LogError(ex);
Response.Write("報表產生失敗:" + ex.Message);
}
finally
{
// 嚴格釋放資源
ReleaseComObjects(wb, app, ws, rng);
}
}
private void ReleaseComObjects(params object[] comObjects)
{
foreach (var obj in comObjects)
{
if (obj != null && Marshal.IsComObject(obj))
{
try { Marshal.ReleaseComObject(obj); } catch { }
}
}
GC.Collect();
GC.WaitForPendingFinalizers();
}
強烈建議新專案採用純 .NET 程式庫,完全避免 COM 互操作問題:
using (var wb = new XLWorkbook())
{
var ws = wb.Worksheets.Add("Report");
ws.Cell(1, 1).InsertTable(dataTable); // 一行寫入 DataTable
ws.Columns().AdjustToContents();
using (var ms = new MemoryStream())
{
wb.SaveAs(ms);
return File(ms.ToArray(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Report.xlsx");
}
}
無需安裝 Office、無 COM 物件洩漏、支援 Linux/Docker 容器化部署、效能提升 10-100 倍。