访问Windows共享目录

一、背景

使用 Spring Boot 访问 Windows 共享目录,对文件进行增删改查。

1. Windows 共享目录介绍

Windows 共享目录使用的是 CIFS 协议:

CIFS 的全称是 Common Internet File System ,即通用互联网文件系统,是一种基于 SMB(Server Message Block,服务器消息块)协议 扩展的网络文件共享协议,主要用于在局域网或广域网中实现不同设备间的文件、打印机等资源共享。

二、解决方案

Maven引入 jcifs-ng 包

1. jcifs(Java CIFS/SMB 客户端库)

jcifs 全称 Java CIFS Client Library ,是纯 Java 实现的 SMB/CIFS 协议客户端库,专门用于在 Java 程序中访问 SMB/CIFS 共享资源(如 Windows 共享文件夹、Samba 服务器)。它解决了 Java 原生 API 不支持 SMB 协议的问题,是 Java 开发中操作网络共享文件的核心工具。

2. 引入 jcifs-ng

xml 复制代码
<!-- 添加jcifs-ng库依赖 -->
<dependency>
    <groupId>eu.agno3.jcifs</groupId>
    <artifactId>jcifs-ng</artifactId>
    <version>2.1.3</version> <!-- 推荐使用最新稳定版 -->
</dependency>

3. 编写工具类进行访问

java 复制代码
import jcifs.CIFSContext;
import jcifs.CIFSException;
import jcifs.config.PropertyConfiguration;
import jcifs.context.BaseContext;
import jcifs.smb.NtlmPasswordAuthenticator;
import jcifs.smb.SmbFile;
import jcifs.smb.SmbFileInputStream;
import jcifs.smb.SmbFileOutputStream;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.*;

/**
 * SMB/CIFS文件操作工具类
 * 支持Windows共享目录的文件读写、目录管理等操作
 */
public final class SmbFileUtils {

    // 私有构造器(工具类禁止实例化)
    private SmbFileUtils() {
        throw new UnsupportedOperationException("工具类禁止实例化");
    }


    /**
     * 构建CIFS上下文(核心:封装认证和配置)
     */
    synchronized public static CIFSContext buildCifsContext(DgDeviceSpecificationSmbConfig config) {
        String cacheKey = buildCacheKey(config);
        try {
            Properties props = new Properties();
// ========== 协议版本优化(禁用SMB1,强制SMB2/SMB3)==========
            props.setProperty("jcifs.smb.client.minVersion", "SMB210"); // 最低SMB2.1
            props.setProperty("jcifs.smb.client.maxVersion", "SMB311"); // 最高SMB3.1.1
            props.setProperty("jcifs.smb.client.disableSMB1", "true");    // 禁用低效的SMB1

// ========== 连接池优化(避免重复创建连接)==========
            props.setProperty("jcifs.pool.maxActive", "20");    // 最大活跃连接数
            props.setProperty("jcifs.pool.maxIdle", "10");     // 最大空闲连接数
            props.setProperty("jcifs.pool.minIdle", "5");      // 最小空闲连接数
            props.setProperty("jcifs.pool.idleTimeout", "300000"); // 空闲连接超时(5分钟)
            props.setProperty("jcifs.pool.maxWait", "30000");  // 获取连接超时(30秒)

// ========== 解析优化(解决NetBIOS/DNS耗时)==========
// 1. 优先用IP代替主机名(彻底避免解析耗时)
// 例如:smb://192.168.1.100/share 代替 smb://fileserver/share
// 2. 禁用NetBIOS(传统解析方式极慢)
            props.setProperty("jcifs.netbios.disabled", "true");
// 3. 仅用DNS解析,且启用缓存
            props.setProperty("jcifs.resolveOrder", "DNS");          // 解析顺序:仅DNS
            props.setProperty("jcifs.dns.cache.ttl", "3600");        // DNS缓存1小时
            props.setProperty("jcifs.netbios.wins", "");             // 清空WINS服务器(禁用WINS)

// ========== 超时配置(避免默认超长等待)==========
            props.setProperty("jcifs.smb.client.connectTimeout", "5000");  // 连接超时5秒
            props.setProperty("jcifs.smb.client.soTimeout", "10000");     // 读取超时10秒
            props.setProperty("jcifs.smb.client.negotiateTimeout", "5000"); // 协议协商超时5秒

// ========== 鉴权缓存(避免重复NTLM/Kerberos鉴权)==========
            props.setProperty("jcifs.ntlm.cache.ttl", "3600");    // NTLM鉴权上下文缓存1小时
            props.setProperty("jcifs.ntlm.cache.size", "100");   // 缓存最大条目数

// ========== 禁用不必要功能(减少协商开销)==========
            props.setProperty("jcifs.smb.client.dfs.disabled", "true"); // 禁用Dfs(不用则关)
            props.setProperty("jcifs.smb.client.signingRequired", "false"); // 禁用SMB签名(服务器允许则关)
            props.setProperty("jcifs.smb.client.tcpNoDelay", "true"); // 启用TCP_NODELAY(减少延迟)

            // 构建配置上下文
            PropertyConfiguration propertyConfig = new PropertyConfiguration(props);
            CIFSContext baseContext = new BaseContext(propertyConfig);

            // 添加认证信息
            NtlmPasswordAuthenticator authenticator = new NtlmPasswordAuthenticator(
                    config.getDomain(), config.getUsername(), config.getPassword()
            );
            CIFSContext context = baseContext.withCredentials(authenticator);
            // 放入缓存
            return context;
        } catch (Exception e) {
            throw new IpsException("构建SMB上下文失败"+e.getMessage());
        }
    }

    /**
     * 构建缓存Key(需包含核心配置,避免不同配置复用)
     * @param config
     * @return
     */
    private static String buildCacheKey(DgDeviceSpecificationSmbConfig config) {
        return String.format("%s_%s_%s_%s_%s",
                Optional.ofNullable(config.getDomain()).orElse(""),
                config.getUsername(),
                config.getMinSmbVersion(),
                config.getMaxSmbVersion(),
                config.getEncoding()
        );
    }


    /**
     * 验证SMB目录是否存在且为有效目录
     * @param smbUrl SMB路径(格式:smb://IP/共享名/子路径)
     * @param config 连接配置
     */
    public static boolean isDirectoryExist(String smbUrl, DgDeviceSpecificationSmbConfig config) {
        try {
            CIFSContext context = buildCifsContext(config);
            SmbFile smbFile = new SmbFile(smbUrl, context);
            return smbFile.exists() && smbFile.isDirectory();
        } catch (Exception e) {
            throw new IpsException("验证目录失败:" + smbUrl+e);
        }
    }
    /**
     * 验证SMB文件是否存在
     * @param smbUrl SMB路径(格式:smb://IP/共享名/子路径)
     * @param context SMB连接
     */
    public static boolean isFileExist(String smbUrl, CIFSContext context) {
        try {
            SmbFile smbFile = new SmbFile(smbUrl, context);
            return smbFile.exists() && smbFile.isFile();
        } catch (Exception e) {
            throw new IpsException("验证文件失败:" + smbUrl+e);
        }
    }

    /**
     * 列出目录下的文件/子目录
     * @param smbUrl 目录路径
     * @param config 连接配置
     * @param includeDir 是否包含子目录
     */
    public static List<String> listFiles(String smbUrl, DgDeviceSpecificationSmbConfig config, boolean includeDir) {
        CIFSContext context=null;
        try {
            context = buildCifsContext(config);
            SmbFile smbDir = new SmbFile(smbUrl, context);
            if (!smbDir.exists() || !smbDir.isDirectory()) {
                throw new IpsException("目录不存在或不是目录:" + smbUrl);
            }

            SmbFile[] smbFiles = smbDir.listFiles();
            if (smbFiles == null || smbFiles.length == 0) {
                return Collections.emptyList();
            }

            List<String> fileList = new ArrayList<>();
            for (SmbFile file : smbFiles) {
                if (includeDir || file.isFile()) {
                    fileList.add(file.getPath());
                }
            }
            return fileList;

        } catch (Exception e) {
            throw new IpsException("列出目录文件失败:" + smbUrl+e.getMessage());
        } finally {
            // 5. 释放上下文资源(避免连接泄漏)
            if (context != null) {
                try {
                    context.close();
                } catch (CIFSException e) {
                    // 关闭上下文的异常不抛出(避免覆盖核心异常),仅打印日志
                    System.err.println("关闭SMB上下文失败(删除文件后):" + smbUrl + ",异常信息:" + e.getMessage());
                }
            }
        }
    }

    /**
     * 获取SMB文件的输入流(数据流)
     * 【重要】调用方必须关闭此流(建议用try-with-resources),否则会导致连接泄漏
     * @param smbUrl 文件路径(smb://IP/共享名/文件路径)
     * @param config 连接配置
     * @return 封装后的InputStream(关闭时自动释放SMB上下文)
     */
    public static InputStream getSmbFileInputStream(String smbUrl, DgDeviceSpecificationSmbConfig config) {
        CIFSContext context = null;
        try {
            // 构建上下文(不使用try-with-resources,交给包装流关闭)
            context = buildCifsContext(config);
            SmbFile smbFile = new SmbFile(smbUrl, context);

            // 校验文件有效性
            if (!smbFile.exists()) {
                throw new IpsException("文件不存在:" + smbUrl);
            }
            if (smbFile.isDirectory()) {
                throw new IpsException("路径是目录,不是文件:" + smbUrl);
            }

            // 创建SMB输入流,并包装(关闭流时自动关闭上下文)
            SmbFileInputStream smbIn = new SmbFileInputStream(smbFile);
            return smbIn;

        } catch (Exception e) {
            String cacheKey = buildCacheKey(config);
            throw new IpsException("获取SMB文件流失败:" + smbUrl+e.getMessage());
        } finally {
            // 5. 释放上下文资源(避免连接泄漏)
            if (context != null) {
                try {
                    context.close();
                } catch (CIFSException e) {
                    // 关闭上下文的异常不抛出(避免覆盖核心异常),仅打印日志
                    System.err.println("关闭SMB上下文失败(删除文件后):" + smbUrl + ",异常信息:" + e.getMessage());
                }
            }
        }
    }

    /**
     * 将 MultipartFile 写入 SMB 服务器(文件不存在则新增,存在则覆盖)
     * @param smbUrl 目标SMB文件路径(格式:smb://IP/共享名/子路径/文件名.后缀)
     * @param config SMB连接配置
     * @param multipartFile 待写入的MultipartFile(如Web上传的文件)
     */
    public static void writeMultipartFileToSmb(String smbUrl, DgDeviceSpecificationSmbConfig config, MultipartFile multipartFile) {
        // 1. 校验MultipartFile有效性
        if (multipartFile.isEmpty()) {
            throw new IpsException("待写入的MultipartFile为空,文件名称:" + multipartFile.getOriginalFilename());
        }
        if (multipartFile.getSize() <= 0) {
            throw new IpsException("待写入的文件大小为0,文件名称:" + multipartFile.getOriginalFilename());
        }

        CIFSContext context = null;
        InputStream fileIn = null;
        // 2. 构建SMB上下文并写入文件
        try{ // 获取MultipartFile输入流
            context = buildCifsContext(config);
            fileIn = multipartFile.getInputStream();
            SmbFile smbFile = new SmbFile(smbUrl, context);
            // 判断文件是否存在
            if(isFileExist(smbUrl, context)){
                throw new IpsException("新增失败,已存在同名文件" + smbUrl);
            }
            // 判断父目录是否存在自动创建父目录(关键:解决父目录不存在导致的写入失败)
            SmbFile parentDir = new SmbFile(smbFile.getParent(), context);
            if (!parentDir.exists()) {
                throw new IpsException("父目录:" + smbFile.getParent()+"不存在,请先创建父目录");
            }
            // 3. 写入文件(覆盖模式,若需追加可传true)
            try (SmbFileOutputStream smbOut = new SmbFileOutputStream(smbFile)) {
                byte[] buffer = new byte[4096];
                int len;
                while ((len = fileIn.read(buffer)) != -1) {
                    smbOut.write(buffer, 0, len);
                }
                smbOut.flush(); // 强制刷出缓冲区数据
            }

            System.out.println("MultipartFile写入SMB成功:" + smbUrl + ",文件大小:" + multipartFile.getSize() + "字节");

        } catch (Exception e) {
            throw new IpsException(
                    "写入MultipartFile到SMB失败,文件名称:" + multipartFile.getOriginalFilename() + ",目标路径:" + smbUrl+e.getMessage()
            );
        } finally {
            // 5. 释放上下文资源(避免连接泄漏)
            if (context != null) {
                try {
                    context.close();
                } catch (CIFSException e) {
                    // 关闭上下文的异常不抛出(避免覆盖核心异常),仅打印日志
                    System.err.println("关闭SMB上下文失败(删除文件后):" + smbUrl + ",异常信息:" + e.getMessage());
                }
            }
        }
    }

    /**
     * 将 MultipartFile 写入 SMB 服务器(文件不存在则新增,存在则覆盖)
     * @param smbUrl 目标SMB文件路径(格式:smb://IP/共享名/子路径/文件名.后缀)
     * @param config SMB连接配置
     * @param inputStream 待写入的MultipartFile(如Web上传的文件)
     */
    public static void writeOutputStreamToSmb(String smbUrl, DgDeviceSpecificationSmbConfig config, InputStream inputStream) {

        CIFSContext context = null;
        // 2. 构建SMB上下文并写入文件
        try{ // 获取MultipartFile输入流
            context = buildCifsContext(config);
            SmbFile smbFile = new SmbFile(smbUrl, context);
            // 3. 写入文件(覆盖模式,若需追加可传true)
            try (SmbFileOutputStream smbOut = new SmbFileOutputStream(smbFile)) {
                byte[] buffer = new byte[4096];
                int len;
                while ((len = inputStream.read(buffer)) != -1) {
                    smbOut.write(buffer, 0, len);
                }
                smbOut.flush(); // 强制刷出缓冲区数据
            }


        } catch (Exception e) {
            throw new IpsException(
                    "写入MultipartFile到SMB失败," + ",目标路径:" + smbUrl+e.getMessage()
            );
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                throw new IpsException(
                        e.getMessage()
                );
            }
            // 5. 释放上下文资源(避免连接泄漏)
            if (context != null) {
                try {
                    context.close();
                } catch (CIFSException e) {
                    // 关闭上下文的异常不抛出(避免覆盖核心异常),仅打印日志
                    System.err.println("关闭SMB上下文失败(删除文件后):" + smbUrl + ",异常信息:" + e.getMessage());
                }
            }
        }
    }


    /**
     * 删除SMB服务器上的指定文件(仅删除文件,不支持删除目录)
     * @param smbUrl 要删除的文件SMB路径(格式:smb://IP/共享名/子路径/文件名.后缀)
     * @param config SMB连接配置
     * @throws IpsException 路径为空/文件不存在/路径是目录/删除操作失败时抛出
     */
    public static void deleteSmbFile(String smbUrl, DgDeviceSpecificationSmbConfig config) {
        // 1. 基础参数校验
        if (smbUrl == null || smbUrl.trim().isEmpty()) {
            throw new IpsException("要删除的SMB文件路径不能为空");
        }

        CIFSContext context = null;
        try {
            // 2. 构建SMB上下文
            context = buildCifsContext(config);
            SmbFile smbFile = new SmbFile(smbUrl, context);

            // 3. 校验文件状态:存在且是文件(防止误删目录)
            if (!smbFile.exists()) {
                throw new IpsException("要删除的文件不存在:" + smbUrl);
            }
            if (smbFile.isDirectory()) {
                throw new IpsException("指定路径是目录,不支持删除目录(仅支持文件删除):" + smbUrl);
            }

            // 4. 执行删除操作
            smbFile.delete();

            System.out.println("SMB文件删除成功:" + smbUrl);
        } catch (CIFSException e) {
            // 捕获SMB协议相关异常,精准提示
            throw new IpsException("删除SMB文件失败(CIFS协议异常):" + smbUrl + ",异常信息:" + e.getMessage());
        } catch (Exception e) {
            // 捕获其他通用异常
            throw new IpsException("删除SMB文件失败:" + smbUrl + ",异常信息:" + e.getMessage());
        } finally {
            // 5. 释放上下文资源(避免连接泄漏)
            if (context != null) {
                try {
                    context.close();
                } catch (CIFSException e) {
                    // 关闭上下文的异常不抛出(避免覆盖核心异常),仅打印日志
                    System.err.println("关闭SMB上下文失败(删除文件后):" + smbUrl + ",异常信息:" + e.getMessage());
                }
            }
        }
    }
}

4. 细节注意

4.1 将Windows路径转换为SMB路径

arduino 复制代码
\\domain\home\code\testcode.java
smb://127.0.0.1/home/code/testcode.java
  1. 将\替换为/
  2. 域名domain替换为ip地址

三、问题

1. 文件占用问题,

刚开始我构建CIFS上下文的配置没写好,导致连接时间很长,平均 20s 左右。于是我使用单例模式构建CIFS上下文,将构建CIFS上下文报错下来不释放。虽然访问时间大大缩短 (20ms) ,但出现了文件占用问题,CIFS上下文不释放,文件就不能被修改和删除。

解决方法:采用连接池配置,耗时平均1.5s。并且每次使用完后释放上下文,解决文件占用问题

相关推荐
掘金酱2 小时前
2025年度稀土掘金影响力榜单发布!
前端·人工智能·后端
程序员侠客行2 小时前
Mybatis二级缓存实现详解
java·数据库·后端·架构·mybatis
源码获取_wx:Fegn08952 小时前
基于springboot + vue健康茶饮销售管理系统
java·vue.js·spring boot·后端·spring
雅俗共赏zyyyyyy2 小时前
SpringBoot集成配置文件加解密
java·spring boot·后端
计算机学姐2 小时前
基于SpringBoot的送货上门系统【2026最新】
java·vue.js·spring boot·后端·mysql·spring·tomcat
算法与双吉汉堡3 小时前
【短链接项目笔记】6 短链接跳转
java·开发语言·笔记·后端·springboot
飞浪3 小时前
告别“Hello World”:一个有经验的程序员如何用 FastAPI 打造生产级后端模板
后端
独自破碎E3 小时前
IDEA2023中新建Spring Boot2.X版本的工程的方法
java·spring boot·后端
无限大63 小时前
为什么"微服务"架构流行?——从集中式到分布式
后端