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、添加依赖
xml
<dependency>
<groupId>eu.agno3.jcifs</groupId>
<artifactId>jcifs-ng</artifactId>
<version>2.1.9</version> <!-- 请使用最新稳定版 -->
</dependency>
2.2 提供接口核心代码
这部分主要是提供接口,和有些参数校验
java
@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逻辑实现核心代码
具体代码
java
@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. 路径标准化处理(核心防错点)
java
remoteBasePath = remoteBasePath.replaceAll("^[\\\\/]+|[\\\\/]+$", "");
String targetSmbUrl = baseSmbUrl + (remoteBasePath.isEmpty() ? "" : remoteBasePath.replace("\\", "/") + "/");
-
为什么必须 :SMB 协议要求目录路径必须以
/结尾,否则listFiles()会返回SmbException: The system cannot find the file specified -
陷阱规避 :处理了 Windows 路径分隔符(
\)与 URL 标准分隔符(/)的混合问题
3.2. 递归遍历的防御性设计
java
if (!dirUrl.endsWith("/")) {
dirToList = new SmbFile(dirUrl + "/", context);
}
-
关键作用 :确保每次遍历的目录 URL 都以
/结尾,避免因路径格式错误导致的遍历中断 -
错误案例 :当远程路径为
smb://host/share/dir(缺少结尾/)时,listFiles()会失败
3.3. 目录结构保持的精准实现
java
String relativePath = child.getCanonicalPath().substring(rootRemotePath.length());
relativePath = relativePath.replace('/', File.separatorChar);
localFilePath = localBasePath.resolve(relativePath);
-
逻辑核心 :通过
substring精确截取相对路径(从根路径开始的后缀) -
平台适配 :
replace('/', File.separatorChar)确保在 Windows/Linux 系统都能正确生成本地路径
3.4. 文件过滤的精准匹配
java
childName.toLowerCase().endsWith(fileType)
-
设计优势 :大小写不敏感匹配(
.HEX/.Hex/.hex均被识别) -
安全边界 :避免正则表达式导致的性能问题(
endsWith是 O(1) 操作)
3.5. 错误隔离机制(企业级健壮性)
java
try {
// 下载文件
} catch (IOException e) {
System.err.println("❌ 下载失败: " + child.getCanonicalPath() + " - " + e.getMessage());
System.err.println(" -> 继续下载其他文件。");
}
-
关键价值:单个文件下载失败(如文件被锁定)不会导致整个目录遍历中断
-
对比:若未做此隔离,一个文件失败将导致整个任务失败
3.6. 资源安全释放
java
try (InputStream in = child.getInputStream();
OutputStream out = new BufferedOutputStream(...)) {
// 传输数据
}
- Java 7 try-with-resources :确保
InputStream和OutputStream在作用域结束时自动关闭 - 避免泄漏:防止因未关闭流导致的文件句柄耗尽
3.7 代码设计决策总结
| 代码段 | 设计决策 | 为什么重要 |
|---|---|---|
`replaceAll("^[[1]](#代码段 设计决策 为什么重要 replaceAll("[1]+ [\/]+$", "") 路径两端标准化 dirUrl.endsWith("/") 检查 目录 URL 标准化 确保 listFiles() 能正确识别目录 child.getCanonicalPath().substring() 精确计算相对路径 保持原始目录结构不丢失 toLowerCase().endsWith() 文件类型匹配 处理大小写不敏感的文件名 try-with-resources 流资源自动关闭 防止文件句柄泄漏 独立文件异常捕获 错误隔离 保证单个文件失败不影响整体任务)^+ |
[\/]+$", "")` | 路径两端标准化 |
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 服务器造成压力。
- \/ ↩︎