同一個 `FTPClient.upload()` 方法,在 `main()` 直接呼叫可完整上傳 ZIP 檔,但經由 Spring `@Autowired` 注入的 Service 呼叫卻僅上傳 10 Bytes 的空殼檔案。這類「同碼異果」問題通常源於執行緒上下文差異、資源管理衝突或Spring 代理導致的事務/連線狀態異常。本文提供系統性排查路徑。
| 維度 | main() 直接呼叫 | Spring 注入呼叫 |
|---|---|---|
| 執行緒 | 主執行緒 | Tomcat/容器工作執行緒 |
| 連線池 | 無(新建連線) | 可能共用 DataSource/連線池 |
| 事務管理 | 無 | 可能被 `@Transactional` 包裹 |
| 異常處理 | 拋出主執行緒 | 可能被全域異常處理器吞沒 |
| 檔案流生命週期 | 同步關閉 | 可能被 Spring 代理提前關閉 |
// 在 upload 方法加入完整 try-catch 並記錄日誌
public boolean upload(String ftpPath, String localFilePath) {
FileInputStream fis = null;
try {
fis = new FileInputStream(localFilePath);
// 關鍵:使用輸入流多載,而非檔案路徑多載
boolean success = ftpClient.storeFile(ftpPath, fis);
return success;
} catch (IOException e) {
log.error("FTP 上傳失敗: {}", e.getMessage(), e); // 必須記錄堆疊追蹤
return false;
} finally {
if (fis != null) try { fis.close(); } catch (IOException ignore) {}
}
}
重點:`storeFile(String, InputStream)` 比 `storeFile(String, String)` 更可控,避免內部二次開啟檔案導致路徑解析差異。
// 錯誤:單例 Bean 共用同一 FTPClient 實例,多執行緒並發會衝突
@Service
public class FtpService {
private FTPClient ftpClient = new FTPClient(); // 危險!
}
// 正確:每次操作建立新實例,或使用 ThreadLocal/ObjectPool
@Service
public class FtpService {
public boolean upload(...) {
FTPClient client = new FTPClient(); // 方法區域變數,執行緒安全
try { client.connect(...); client.login(...);
return client.storeFile(...); }
finally { client.disconnect(); }
}
}
client.enterLocalPassiveMode(); // 必須在 connect/login 之後、storeFile 之前
// 或
client.enterRemotePassiveMode(); // 視伺服器設定
Spring 容器環境(如雲端部署)網路拓撲與本機 `main()` 不同,被動模式設定往往是關鍵。
// log4j2.xml
// 程式碼中
ftpClient.setDebug(true); // 印出原始 FTP 指令/回應
對比 `main()` 與 Spring 環境下的指令流差異(如 `PORT` vs `PASV`、`TYPE I` 等),精準定位問題。
若問題持續,改用 Spring 官方整合模組,宣告式處理重試、暫存、錯誤通道:
Spring 環境下的網路/IO 操作,核心差異在於「容器管理的生命週期」與「共用資源的執行緒安全」。逐項排查上述清單,多數「Main 正常、注入異常」問題可解決。