1. 概述
💡 作者:古渡藍按
個人微信公眾號:微信公眾號(深入淺出談java)
感覺本篇對你有幫助可以關注一下,會不定期更新知識和麪試資料、技巧!!!
本技術文檔旨在説明如何通過 SMB(Server Message Block)協議 實現對遠程 Windows 共享服務器或 Samba 服務的文件讀取、寫入與目錄遍歷操作。適用於 Java 應用程序在企業內網環境中安全、高效地訪問遠程共享資源。
主要應用場景包括:
- 自動化從 Jenkins 構建服務器拉取構建產物;
- 定期同步業務系統生成的配置/數據文件;
- 批量處理遠程共享目錄中的特定類型文件(如
.hex、.csv等)。
1.1技術選型
| 組件 | 説明 |
|---|---|
| 協議 | SMBv2 / SMBv3(推薦,安全性更高) |
| Java 庫 | jcifs-ng(JCIFS 的活躍維護分支,支持現代 SMB 協議) |
| 認證方式 | NTLM(Windows 域或本地賬户) |
| 開發語言 | Java 8+ |
1.2前提條件
✅ 前提條件(必須滿足)
在目標服務器 173.16.1.152 上:
- 已共享
D:\jenkins文件夾(這裏改成你需要訪問的共享目錄)- 共享名建議為
jenkins→ 訪問路徑:\\173.16.1.152\jenkins(目錄名稱改成自己相應即可)
- 共享名建議為
- 你有一個有寫權限的 Windows 賬户(如
admin/deploy) - 防火牆允許 445 端口(默認 SMB 端口)
- “密碼保護的共享”已關閉(或你知道正確憑據)
💡 測試:在
winds服務器上按Win+R,輸入
\\173.16.1.152\jenkins
看是否能打開並寫入文件。
2、代碼實現
代碼執行流程示意圖:
2.1、添加依賴
<dependency>
<groupId>eu.agno3.jcifs</groupId>
<artifactId>jcifs-ng</artifactId>
<version>2.1.9</version> <!-- 請使用最新穩定版 -->
</dependency>
2.2 提供接口核心代碼
這部分主要是提供接口,和有些參數校驗
@ApiOperation("只下載目錄下的 .hex 文件並下載")
@PostMapping("/getJenkinsHexData")
public R<String> downloadSmbHexFiles(@RequestBody SmbDownloadRequestVo request) {
// 1. 路徑安全檢查(防止路徑遍歷)
if (request.getLocalBaseDir() != null &&
(request.getLocalBaseDir().contains("..") ||
request.getLocalBaseDir().contains("/"))) {
throw new UserException("無效的本地基礎目錄路徑");
}
// // 2. 從環境變量獲取密碼(生產環境必須)
// String safePassword = System.getenv("SMB_PASSWORD");
// if (safePassword == null) {
// throw new UserException("未設置SMB_PASSWORD環境變量");
// }
// 3. 驗證請求參數
if (request.getSmbHost() == null || request.getShareName() == null ||
request.getUsername() == null) {
throw new UserException("缺少必需參數:smbHost、shareName、username");
}
try {
// 4. 使用安全密碼執行下載
WindowsDownloaderHexFile.downloadHexFiles(
request.getSmbHost(),
request.getShareName(),
request.getRemotePath(),
request.getUsername(),
request.getPassword(),
request.getLocalBaseDir(),
true,
request.getFileExtension()
);
return R.ok("文件下載成功");
} catch (Exception e) {
return R.fail("文件下載失敗");
}
}
2.3邏輯實現核心代碼
具體代碼
@Service
@Slf4j
public class WindowsDownloaderHexFile {
/**
* 從指定的 SMB 遠程路徑遞歸查找並下載所有 .hex 文件到本地目錄。
*
* @param smbHost SMB 服務器地址 (e.g., "172.16.1.85")
* @param shareName SMB 共享名 (e.g., "jenkins")
* @param remoteBasePath 需要開始搜索的遠程基礎路徑 (相對於共享根目錄)。支持多級,使用 \ 或 / 分隔。 (e.g., "1/8-位號文件/圖號導入文件")
* 如果路徑是目錄,建議以分隔符結尾或確保它是目錄。
* @param username 用户名 (e.g., "administrator")
* @param password 密碼 (e.g., "Jn300880")
* @param localDownloadDir 本地下載目錄,找到的 .hex 文件將被下載到這裏,並保持相對結構。(e.g., "D:\\DownloadedHexFiles")
* @param preserveStructure 是否在本地保持遠程的目錄結構。true: 保持結構;false: 所有文件下載到 localDownloadDir 根目錄下。
* @param fileType 文件類型,指定只下載以該後綴的文件。
* @throws RuntimeException 如果發生 IO、SMB 或其他錯誤
*/
public static void downloadHexFiles(
String smbHost,
String shareName,
String remoteBasePath,
String username,
String password,
String localDownloadDir,
boolean preserveStructure,
String fileType) {
CIFSContext context = null;
try {
// 1. 初始化 SMB 上下文和認證
context = SingletonContext.getInstance().withCredentials(new NtlmPasswordAuthenticator(null, username, password));
// 2. 構建基礎 SMB URL
String baseSmbUrl = "smb://" + smbHost + "/" + shareName + "/";
// 3. 處理 remoteBasePath,確保格式正確並構建目標 SmbFile
// 移除路徑開頭和結尾的多餘分隔符
remoteBasePath = remoteBasePath.replaceAll("^[\\\\/]+|[\\\\/]+$", "");
String targetSmbUrl = baseSmbUrl + (remoteBasePath.isEmpty() ? "" : remoteBasePath.replace("\\", "/") + "/");
SmbFile targetRemoteDir = new SmbFile(targetSmbUrl, context);
// 4. 檢查遠程基礎路徑是否存在且為目錄
if (!targetRemoteDir.exists()) {
throw new RuntimeException("遠程路徑不存在: " + targetSmbUrl);
}
if (!targetRemoteDir.isDirectory()) {
throw new RuntimeException("遠程路徑不是目錄: " + targetSmbUrl);
}
Path localBasePath = Paths.get(localDownloadDir);
System.out.println("嘗試創建目錄: " + localBasePath.toAbsolutePath());
try {
Files.createDirectories(localBasePath);
System.out.println("目錄創建成功");
} catch (Exception e) {
e.printStackTrace();
}
// 6. 開始遞歸查找和下載
findAndDownloadHexFiles(targetRemoteDir, localBasePath, context, preserveStructure, targetRemoteDir.getCanonicalPath(),fileType);
} catch (Exception e) {
throw new RuntimeException("初始化 SMB 連接或準備下載時出錯", e);
}
}
/**
* 遞歸查找 .hex 文件並下載的核心方法。
*
* @param currentRemoteDir 當前正在處理的遠程目錄 SmbFile。
* @param localBasePath 本地下載的基礎目錄 Path。
* @param context SMB 上下文。
* @param preserveStructure 是否保持目錄結構。
* @param rootRemotePath 搜索的根遠程路徑,用於計算相對路徑。
* @param fileType 文件類型,指定只下載以該後綴的文件。
* @throws IOException 如果發生 IO 錯誤。
* @throws SmbException 如果發生 SMB 錯誤。
*/
private static void findAndDownloadHexFiles(
SmbFile currentRemoteDir,
Path localBasePath,
CIFSContext context,
boolean preserveStructure,
String rootRemotePath,
String fileType) throws IOException {
log.info("進入遞歸方法開始查詢!!!");
// --- 確保目錄 URL 以 '/' 結尾,這是 listFiles 的關鍵 ---
String dirUrl = currentRemoteDir.getURL().toString();
SmbFile dirToList = currentRemoteDir;
if (!dirUrl.endsWith("/")) {
dirToList = new SmbFile(dirUrl + "/", context);
}
// -------------------------------------------------------------
SmbFile[] children;
try {
children = dirToList.listFiles(); // 列出子項
System.out.println("列出目錄內容: " + dirToList.getCanonicalPath());
} catch (SmbException e) {
System.err.println("❌ SmbException while listing children of: " + dirToList.getCanonicalPath() + " - " + e.getMessage());
// 可以選擇跳過此目錄或拋出異常
// 這裏選擇打印錯誤並跳過
System.err.println(" -> 跳過此目錄。");
return;
}
if (children != null) {
for (SmbFile child : children) {
String childName = child.getName();
// 過濾掉 . 和 ..
if (".".equals(childName) || "..".equals(childName)) {
continue;
}
if (child.isDirectory()) {
// 遞歸進入子目錄
findAndDownloadHexFiles(child, localBasePath, context, preserveStructure, rootRemotePath,fileType);
} else if (child.isFile() && childName.toLowerCase().endsWith(fileType)) {
// 找到 .hex 文件,準備下載
System.out.println("🔍 找到 .hex 文件: " + child.getCanonicalPath());
// 計算本地文件路徑
Path localFilePath;
if (preserveStructure) {
// 計算相對於搜索根目錄的路徑
String relativePath = child.getCanonicalPath().substring(rootRemotePath.length());
// 清理路徑分隔符 (確保使用本地分隔符)
relativePath = relativePath.replace('/', File.separatorChar).replace('\\', File.separatorChar);
localFilePath = localBasePath.resolve(relativePath);
} else {
// 直接放在基礎目錄下
localFilePath = localBasePath.resolve(childName);
}
// 確保本地文件的父目錄存在
Path parentDir = localFilePath.getParent();
if (parentDir != null) {
Files.createDirectories(parentDir);
}
// 下載文件
System.out.println("📥 下載到: " + localFilePath);
try (InputStream in = child.getInputStream();
OutputStream out = new BufferedOutputStream(new FileOutputStream(localFilePath.toFile()))) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
System.out.println("✅ 下載完成: " + childName);
} catch (IOException e) {
System.err.println("❌ 下載文件失敗: " + child.getCanonicalPath() + " - " + e.getMessage());
// 可以選擇繼續下載其他文件或拋出異常
// 這裏選擇打印錯誤並繼續
System.err.println(" -> 繼續下載其他文件。");
}
}
}
} else {
System.out.println("⚠️ 目錄 " + dirToList.getCanonicalPath() + " 列表為空或無法訪問。");
}
}
}
3、關鍵代碼邏輯深度解析
3.1. 路徑標準化處理(核心防錯點)
remoteBasePath = remoteBasePath.replaceAll("^[\\\\/]+|[\\\\/]+$", "");
String targetSmbUrl = baseSmbUrl + (remoteBasePath.isEmpty() ? "" : remoteBasePath.replace("\\", "/") + "/");
-
為什麼必須:SMB 協議要求目錄路徑必須以
/結尾,否則listFiles()會返回SmbException: The system cannot find the file specified -
陷阱規避:處理了 Windows 路徑分隔符(
\)與 URL 標準分隔符(/)的混合問題
3.2. 遞歸遍歷的防禦性設計
if (!dirUrl.endsWith("/")) {
dirToList = new SmbFile(dirUrl + "/", context);
}
-
關鍵作用:確保每次遍歷的目錄 URL 都以
/結尾,避免因路徑格式錯誤導致的遍歷中斷 -
錯誤案例:當遠程路徑為
smb://host/share/dir(缺少結尾/)時,listFiles()會失敗
3.3. 目錄結構保持的精準實現
String relativePath = child.getCanonicalPath().substring(rootRemotePath.length());
relativePath = relativePath.replace('/', File.separatorChar);
localFilePath = localBasePath.resolve(relativePath);
-
邏輯核心:通過
substring精確截取相對路徑(從根路徑開始的後綴) -
平台適配:
replace('/', File.separatorChar)確保在 Windows/Linux 系統都能正確生成本地路徑
3.4. 文件過濾的精準匹配
childName.toLowerCase().endsWith(fileType)
-
設計優勢:大小寫不敏感匹配(
.HEX/.Hex/.hex均被識別) -
安全邊界:避免正則表達式導致的性能問題(
endsWith是 O(1) 操作)
3.5. 錯誤隔離機制(企業級健壯性)
try {
// 下載文件
} catch (IOException e) {
System.err.println("❌ 下載失敗: " + child.getCanonicalPath() + " - " + e.getMessage());
System.err.println(" -> 繼續下載其他文件。");
}
-
關鍵價值:單個文件下載失敗(如文件被鎖定)不會導致整個目錄遍歷中斷
-
對比:若未做此隔離,一個文件失敗將導致整個任務失敗
3.6. 資源安全釋放
try (InputStream in = child.getInputStream();
OutputStream out = new BufferedOutputStream(...)) {
// 傳輸數據
}
- Java 7 try-with-resources:確保
InputStream和OutputStream在作用域結束時自動關閉 - 避免泄漏:防止因未關閉流導致的文件句柄耗盡
3.7 代碼設計決策總結
| 代碼段 | 設計決策 | 為什麼重要 |
|---|---|---|
| `replaceAll("[1]+ | [\/]+$", "")` | 路徑兩端標準化 |
dirUrl.endsWith("/") 檢查 |
目錄 URL 標準化 | 確保 listFiles() 能正確識別目錄 |
child.getCanonicalPath().substring() |
精確計算相對路徑 | 保持原始目錄結構不丟失 |
toLowerCase().endsWith() |
文件類型匹配 | 處理大小寫不敏感的文件名 |
try-with-resources |
流資源自動關閉 | 防止文件句柄泄漏 |
| 獨立文件異常捕獲 | 錯誤隔離 | 保證單個文件失敗不影響整體任務 |
核心工程哲學:在 SMB 傳輸中,路徑格式和錯誤隔離是決定系統是否能穩定運行的兩個關鍵因素。本實現通過精準處理路徑和設計錯誤隔離機制,確保在工業環境中(如測試設備頻繁生成文件)也能可靠運行。
4. 異常處理與最佳實踐
4.1 常見異常
| 異常類型 | 可能原因 | 解決方案 |
|---|---|---|
jcifs.smb.SmbAuthException:unknown user name or bad password |
用户名/密碼錯誤,或無權限 | 檢查賬户權限,確認共享設置 |
jcifs.smb.SmbException: Access is denied |
賬户有登錄權限但無文件訪問權限 | 聯繫管理員授予“讀取”或“完全控制”權限 |
SmbException: The system cannot find the file specified |
1、目錄 URL 未以 / 結尾;
2、這個目錄在遠程並不存在 |
確保 listFiles() 前 URL 以 / 結尾 |
UnknownHostException |
主機名無法解析 | 檢查 IP 或 DNS 配置 |
ConnectException |
網絡不通或防火牆阻斷 | 確認 445/TCP 端口開放 |
The filename, directory name, or volume label syntax is incorrect |
提供的文件或目錄名稱不符合語法要求(包含非法字符)。如 `< > : " | ? * `)、URL 編碼問題。 |
4.2 安全建議
- 禁止硬編碼密碼:使用配置中心、環境變量或密鑰管理服務;
- 最小權限原則:SMB 賬户僅授予必要讀寫權限;
- 啓用 SMB 簽名(如需):在
jcifs-ng中可通過withProperties()配置; - 避免使用 SMBv1:
jcifs-ng默認禁用 SMBv1,符合安全規範。
4.3 性能優化
- 使用緩衝流(
BufferedInputStream)提升大文件傳輸效率; - 對大量小文件,考慮壓縮後傳輸再解壓;
- 控制併發連接數,避免對 SMB 服務器造成壓力。
-
\/ ↩︎