Files
contract-manager/server/src/main/java/com/ecep/contract/service/SmbFileService.java
songqq 87290f15b0 feat(SMB): 重构SMB文件服务支持多服务器配置和连接池优化
重构SmbFileService以支持多服务器配置,引入连接池和会话池管理机制。主要变更包括:
1. 实现基于主机的多服务器认证配置
2. 新增连接池和会话池管理,提高连接复用率
3. 添加定时清理空闲连接和会话的功能
4. 优化异常处理和重试机制
5. 改进日志记录和资源释放

同时更新相关配置文件和应用属性以支持新功能:
1. 修改application.properties支持多服务器SMB配置
2. 增强SmbConfig类以管理多服务器配置
3. 添加任务映射到tasker_mapper.json
4. 新增客户端和服务端任务规则文档
2025-11-17 12:55:31 +08:00

685 lines
26 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.ecep.contract.service;
import com.ecep.contract.SpringApp;
import com.ecep.contract.config.SmbConfig;
import com.hierynomus.msdtyp.AccessMask;
import com.hierynomus.mserref.NtStatus;
import com.hierynomus.msfscc.FileAttributes;
import com.hierynomus.msfscc.fileinformation.FileAllInformation;
import com.hierynomus.mssmb2.SMB2CreateDisposition;
import com.hierynomus.mssmb2.SMB2CreateOptions;
import com.hierynomus.mssmb2.SMB2ShareAccess;
import com.hierynomus.mssmb2.SMBApiException;
import com.hierynomus.protocol.transport.TransportException;
import com.hierynomus.smbj.SMBClient;
import com.hierynomus.smbj.auth.AuthenticationContext;
import com.hierynomus.smbj.common.SMBRuntimeException;
import com.hierynomus.smbj.common.SmbPath;
import com.hierynomus.smbj.event.SessionLoggedOff;
import com.hierynomus.smbj.share.DiskShare;
import com.hierynomus.smbj.share.File;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.OutputStream;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* SMB文件服务类提供SMB/CIFS协议的文件操作功能
*/
@Slf4j
@Service
public class SmbFileService implements DisposableBean {
private final SMBClient client;
private final SmbConfig smbConfig;
// 连接空闲超时时间3分钟
private static final long CONNECTION_IDLE_TIMEOUT_MS = 3 * 60 * 1000;
// Session信息内部类用于存储session和最后使用时间
private static class SessionInfo implements AutoCloseable {
private final com.hierynomus.smbj.session.Session session;
private final String username; // 添加用户名字段
private volatile long lastUsedTimestamp;
public SessionInfo(com.hierynomus.smbj.session.Session session, String username) {
this.session = session;
this.username = username;
this.lastUsedTimestamp = System.currentTimeMillis();
}
public String getUsername() {
return username;
}
public com.hierynomus.smbj.session.Session getSession() {
return session;
}
public long getLastUsedTimestamp() {
return lastUsedTimestamp;
}
public void updateLastUsedTimestamp() {
this.lastUsedTimestamp = System.currentTimeMillis();
}
public boolean isIdle(long timeoutMs) {
return (System.currentTimeMillis() - lastUsedTimestamp) > timeoutMs;
}
public void close() throws IOException {
session.close();
}
}
// 连接信息内部类用于存储连接和session连接池
private static class ConnectionInfo {
private final com.hierynomus.smbj.connection.Connection connection;
private volatile long lastUsedTimestamp;
// Session连接池使用List存储
private final List<SessionInfo> sessionPool;
public ConnectionInfo(com.hierynomus.smbj.connection.Connection connection) {
this.connection = connection;
this.lastUsedTimestamp = System.currentTimeMillis();
this.sessionPool = Collections.synchronizedList(new ArrayList<>());
}
public com.hierynomus.smbj.connection.Connection getConnection() {
return connection;
}
public long getLastUsedTimestamp() {
return lastUsedTimestamp;
}
public void updateLastUsedTimestamp() {
this.lastUsedTimestamp = System.currentTimeMillis();
}
public boolean isIdle(long timeoutMs) {
return (System.currentTimeMillis() - lastUsedTimestamp) > timeoutMs;
}
// 清理空闲的session
public int cleanupIdleSessions(long timeoutMs) {
List<SessionInfo> idleSessions = new java.util.ArrayList<>();
// 查找所有空闲session
for (SessionInfo sessionInfo : sessionPool) {
if (sessionInfo != null && sessionInfo.isIdle(timeoutMs)) {
idleSessions.add(sessionInfo);
}
}
// 关闭并移除空闲session
synchronized (sessionPool) {
for (SessionInfo sessionInfo : idleSessions) {
if (sessionInfo != null && sessionPool.contains(sessionInfo)) {
try {
sessionInfo.close();
} catch (IOException e) {
log.error("Error closing idle session for username: {}", sessionInfo.getUsername(), e);
}
sessionPool.remove(sessionInfo);
}
}
}
return idleSessions.size();
}
// 从session池中获取任意一个有效的session
public SessionInfo peekSession() {
if (sessionPool.isEmpty()) {
return null;
}
return sessionPool.removeFirst();
}
// 创建新session并添加到池中
public SessionInfo createSession(AuthenticationContext authContext) throws IOException {
String username = authContext.getUsername();
// 创建新session
com.hierynomus.smbj.session.Session session = connection.authenticate(authContext);
SessionInfo newSession = new SessionInfo(session, username);
return newSession;
}
// 更新session的最后使用时间
public void returnSession(SessionInfo sessionInfo) {
// 重新添加到池中
sessionPool.addLast(sessionInfo);
sessionInfo.updateLastUsedTimestamp();
}
// 关闭所有session
public void closeAllSessions() {
// 创建副本以避免并发修改异常
List<SessionInfo> sessionsCopy = new ArrayList<>();
// 先获取副本并清空池
synchronized (sessionPool) {
sessionsCopy.addAll(sessionPool);
sessionPool.clear();
}
// 关闭所有session
for (SessionInfo sessionInfo : sessionsCopy) {
try {
if (sessionInfo != null) {
sessionInfo.close();
}
} catch (IOException e) {
log.error("Error closing session for username: {}", sessionInfo.getUsername(), e);
}
}
}
}
// 连接池使用ConcurrentHashMap确保线程安全
private final Map<String, ConnectionInfo> connectionPool = new ConcurrentHashMap<>();
// 连接池锁,用于同步连接的创建和关闭
private final ReentrantLock connectionPoolLock = new ReentrantLock();
// 定时清理线程池
private final ScheduledExecutorService cleanupScheduler;
/**
* 构造函数注入SMB客户端和配置初始化定时清理任务
*
* @param smbClient SMB客户端实例
*/
public SmbFileService(@Autowired SMBClient smbClient, @Autowired ScheduledExecutorService executor) {
this.client = smbClient;
this.smbConfig = SpringApp.getBean(SmbConfig.class);
this.smbConfig.subscribe(this);
// 初始化定时清理任务每30秒运行一次
this.cleanupScheduler = executor;
// 启动定时清理任务延迟1分钟后开始每30秒执行一次
this.cleanupScheduler.scheduleAtFixedRate(this::cleanupIdleConnections, 1, 30, TimeUnit.SECONDS);
}
//
// @net.engio.mbassy.listener.Handler
// private void onSessionLoggedOff(SessionLoggedOff sessionLoggedOffEvent) {
//
// }
/**
* 获取认证上下文,根据主机名获取对应的认证信息
*
* @param host 主机名
* @return 认证上下文
* @throws IOException 如果找不到对应的服务器配置
*/
private AuthenticationContext getAuthenticationContext(String host) throws IOException {
log.debug("Creating AuthenticationContext for host: {}", host);
// 获取该主机对应的配置信息
SmbConfig.ServerConfig serverConfig = smbConfig.getServerConfig(host);
// 检查是否找到配置
if (serverConfig == null) {
String errorMsg = String.format("No SMB configuration found for host: %s", host);
log.error(errorMsg);
throw new IOException(errorMsg);
}
// 检查配置是否完整
if (serverConfig.getUsername() == null || serverConfig.getPassword() == null) {
String errorMsg = String.format("Incomplete SMB configuration for host: %s, username or password missing",
host);
log.error(errorMsg);
throw new IOException(errorMsg);
}
return new AuthenticationContext(
serverConfig.getUsername(),
serverConfig.getPassword().toCharArray(),
"");
}
/**
* 从连接池获取或创建连接
*
* @param hostname 主机名
* @return SMB连接
* @throws IOException 如果创建连接失败
*/
private ConnectionInfo getConnectionInfo(String hostname) throws IOException {
// 首先检查连接池是否已有该主机的连接
com.hierynomus.smbj.connection.Connection connection = null;
int maxTrys = 3;
while (maxTrys-- > 0) {
ConnectionInfo connectionInfo = connectionPool.get(hostname);
// 如果连接存在且有效,则更新最后使用时间并返回
if (connectionInfo != null) {
connection = connectionInfo.getConnection();
if (connection != null && connection.isConnected()) {
// 更新连接的最后使用时间
connectionInfo.updateLastUsedTimestamp();
log.debug("Reusing SMB connection for host: {}", hostname);
return connectionInfo;
}
log.debug("Closing invalid SMB connection for host: {}", hostname);
connectionInfo.closeAllSessions();
}
// 如果连接不存在或已关闭,则创建新连接
connectionPoolLock.lock();
try {
// 创建新连接
log.debug("Creating new SMB connection for host: {}", hostname);
connection = client.connect(hostname);
connectionInfo = new ConnectionInfo(connection);
connectionPool.put(hostname, connectionInfo);
return connectionInfo;
} finally {
connectionPoolLock.unlock();
}
}
return null;
}
/**
* 从连接池获取或创建连接(兼容旧方法签名)
*
* @param hostname 主机名
* @return SMB连接
* @throws IOException 如果创建连接失败
*/
private com.hierynomus.smbj.connection.Connection getConnection(String hostname) throws IOException {
return getConnectionInfo(hostname).getConnection();
}
// Session空闲超时时间2分钟比连接超时时间短
private static final long SESSION_IDLE_TIMEOUT_MS = 2 * 60 * 1000;
/**
* 清理空闲连接和session的定时任务
* 1. 检查并关闭所有超时的session
* 2. 检查并关闭所有超时的连接
*/
private void cleanupIdleConnections() {
log.debug(
"Running idle connections and sessions cleanup task with session timeout: {}ms, connection timeout: {}ms",
SESSION_IDLE_TIMEOUT_MS, CONNECTION_IDLE_TIMEOUT_MS);
// 创建要移除的连接列表避免在迭代时修改Map
List<String> idleHostnames = new java.util.ArrayList<>();
int totalClosedSessions = 0;
// 首先清理每个连接中的空闲session
for (Map.Entry<String, ConnectionInfo> entry : connectionPool.entrySet()) {
String hostname = entry.getKey();
ConnectionInfo connectionInfo = entry.getValue();
if (connectionInfo != null) {
// 清理该连接下的空闲session - 检查session是否超时超时则关闭
log.debug("Checking for idle sessions on host: {}", hostname);
int closedSessions = connectionInfo.cleanupIdleSessions(SESSION_IDLE_TIMEOUT_MS);
totalClosedSessions += closedSessions;
if (closedSessions > 0) {
log.debug("Closed {} idle/expired sessions for host: {}", closedSessions, hostname);
}
// 然后检查连接是否空闲超时
if (connectionInfo.isIdle(CONNECTION_IDLE_TIMEOUT_MS)) {
idleHostnames.add(hostname);
log.debug("Found idle connection for host: {}, will be closed", hostname);
}
}
}
// 关闭并移除空闲连接
if (!idleHostnames.isEmpty()) {
connectionPoolLock.lock();
try {
for (String hostname : idleHostnames) {
ConnectionInfo connectionInfo = connectionPool.get(hostname);
if (connectionInfo != null) {
try {
// 先关闭所有session
connectionInfo.closeAllSessions();
log.debug("Closed all remaining sessions for host: {}", hostname);
// 再关闭连接
log.debug("Closing idle connection for host: {}", hostname);
connectionInfo.getConnection().close();
} catch (IOException e) {
log.error("Error closing idle connection for host: {}", hostname, e);
} finally {
connectionPool.remove(hostname);
log.debug("Removed connection from pool for host: {}", hostname);
}
}
}
} finally {
connectionPoolLock.unlock();
}
}
log.debug(
"Idle connections and sessions cleanup completed successfully. Results: closed {} connections and {} expired sessions",
idleHostnames.size(), totalClosedSessions);
}
/**
* 执行SMB操作的通用方法使用连接池和session池
*
* @param smbPath SMB路径
* @param operation 要执行的操作
* @param <T> 操作返回类型
* @return 操作结果
* @throws IOException 如果操作失败
*/
private <T> T executeSmbOperation(SmbPath smbPath, SmbOperation<T> operation) throws IOException {
String hostname = smbPath.getHostname();
ConnectionInfo connectionInfo = null;
SessionInfo sessionInfo = null;
T re = null;
// 尝试执行获取连接执行操作,当发生 TransportException 时 尝试重试最多3次
int maxTrys = 3;
while (maxTrys-- > 0) {
try {
// 获取连接
connectionInfo = getConnectionInfo(hostname);
// 从session池获取session
sessionInfo = connectionInfo.peekSession();
// 如果session不存在
if (sessionInfo == null) {
// 获取认证上下文
AuthenticationContext authContext = getAuthenticationContext(hostname);
// 创建新session并添加到池中
sessionInfo = connectionInfo.createSession(authContext);
log.debug("Created new SMB session for host: {}", hostname);
} else {
log.debug("Reusing SMB session for host: {}", hostname);
}
// 连接共享
try (var share = (DiskShare) sessionInfo.getSession().connectShare(smbPath.getShareName())) {
re = operation.execute(share, smbPath.getPath());
// 操作完成后更新session的最后使用时间将session放回池中
connectionInfo.returnSession(sessionInfo);
log.debug("Returned SMB session to pool for host: {}", hostname);
} catch (SMBRuntimeException e) {
sessionInfo.close();
throw e;
} finally {
}
break;
} catch (TransportException e) {
log.warn("TransportException occurred while trying to connect to host: {}. Retrying...", hostname);
// 延迟1秒
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
continue;
} catch (IOException e) {
// 如果操作失败且连接信息存在,检查连接状态
if (connectionInfo != null) {
com.hierynomus.smbj.connection.Connection connection = connectionInfo.getConnection();
if (connection != null && !connection.isConnected()) {
// 从连接池移除失效的连接并关闭所有session
connectionInfo.closeAllSessions();
connectionPool.remove(hostname);
log.debug("Removed disconnected SMB connection from pool for host: {}", hostname);
}
}
throw e;
}
}
return re;
}
/**
* 上传文件到SMB服务器
*
* @param filePath 文件路径
* @param fileContent 文件内容字节数组
* @throws IOException 如果上传失败
*/
public void uploadFile(String filePath, byte[] fileContent) throws IOException {
Objects.requireNonNull(filePath, "File path cannot be null");
Objects.requireNonNull(fileContent, "File content cannot be null");
log.debug("Uploading file: {} with size: {} bytes", filePath, fileContent.length);
var smbPath = SmbPath.parse(filePath);
executeSmbOperation(smbPath, (share, path) -> {
// 创建目录(如果不存在)
String directoryPath = path.substring(0, path.lastIndexOf('/'));
if (!share.folderExists(directoryPath)) {
share.mkdir(directoryPath);
log.debug("Created directory: {}", directoryPath);
}
// 上传文件内容
try (File smbFile = share.openFile(smbPath.getPath(),
EnumSet.of(AccessMask.GENERIC_WRITE),
EnumSet.of(FileAttributes.FILE_ATTRIBUTE_NORMAL),
EnumSet.of(SMB2ShareAccess.FILE_SHARE_WRITE),
SMB2CreateDisposition.FILE_CREATE,
EnumSet.noneOf(SMB2CreateOptions.class))) {
try (OutputStream out = smbFile.getOutputStream()) {
out.write(fileContent);
}
}
log.info("Successfully uploaded file: {}", filePath);
return null;
});
}
/**
* 检查SMB路径是否存在
*
* @param smbPath SMB路径
* @return 如果路径存在则返回true
* @throws IOException 如果检查失败
*/
public boolean exists(SmbPath smbPath) throws IOException {
return executeSmbOperation(smbPath, (share, path) -> {
try {
FileAllInformation info = share.getFileInformation(path);
if (info.getStandardInformation().isDirectory()) {
return share.folderExists(path);
}
return share.fileExists(path);
} catch (SMBApiException e) {
if (e.getStatus().equals(NtStatus.STATUS_OBJECT_NAME_NOT_FOUND)) {
return false;
}
log.error("Error checking if path exists: {}", path, e);
throw e;
}
});
}
/**
* 检查文件是否存在基于File对象
*
* @param file 文件对象
* @return 如果文件存在则返回true
*/
public boolean exists(java.io.File file) {
Objects.requireNonNull(file, "File cannot be null");
try {
var smbPath = SmbPath.parse(file.getAbsolutePath());
return exists(smbPath);
} catch (IOException e) {
log.error("Error checking if file exists: {}", file.getAbsolutePath(), e);
return false;
}
}
/**
* 创建目录
*
* @param path 要创建的目录路径
* @throws IOException 如果创建失败
*/
public void mkdir(java.io.File path) throws IOException {
Objects.requireNonNull(path, "Path cannot be null");
var smbPath = SmbPath.parse(path.getAbsolutePath());
executeSmbOperation(smbPath, (share, smbFilePath) -> {
if (!share.folderExists(smbFilePath)) {
share.mkdir(smbFilePath);
log.debug("Created directory: {}", smbFilePath);
}
return null;
});
}
/**
* 列出目录中的文件(不包括子目录)
*
* @param dir 目录对象
* @return 文件列表
* @throws IOException 如果列出失败
*/
public List<java.io.File> listFiles(java.io.File dir) throws IOException {
Objects.requireNonNull(dir, "Directory cannot be null");
var smbPath = SmbPath.parse(dir.getAbsolutePath());
return executeSmbOperation(smbPath, (share, path) -> {
try {
FileAllInformation info = share.getFileInformation(path);
if (info.getStandardInformation().isDirectory() && share.folderExists(path)) {
var files = share.list(path);
return files.stream()
.filter(f -> !f.getFileName().startsWith(".") && !f.getFileName().equals(".."))
.filter(f -> {
try {
String fullPath = path + "\\" + f.getFileName();
return !share.getFileInformation(fullPath).getStandardInformation().isDirectory();
} catch (SMBApiException e) {
log.warn("Error checking file type for: {}", f.getFileName(), e);
return false;
}
})
.map(f -> new java.io.File(dir, f.getFileName()))
.toList();
}
} catch (SMBApiException e) {
log.error("Error listing files in directory: {}", path, e);
}
return Collections.emptyList();
});
}
/**
* 检查是否为文件(非目录)
* 修复:之前的实现逻辑错误,现在正确返回是否为文件
*
* @param file 文件对象
* @return 如果是文件非目录则返回true
* @throws IOException 如果检查失败
*/
public boolean isFile(java.io.File file) throws IOException {
Objects.requireNonNull(file, "File cannot be null");
var smbPath = SmbPath.parse(file.getAbsolutePath());
return executeSmbOperation(smbPath, (share, path) -> {
FileAllInformation fileInformation = share.getFileInformation(path);
// 修复:返回是否不是目录,即是否为文件
return !fileInformation.getStandardInformation().isDirectory();
});
}
/**
* 检查是否为目录
*
* @param file 文件对象
* @return 如果是目录则返回true
* @throws IOException 如果检查失败
*/
public boolean isDirectory(java.io.File file) throws IOException {
Objects.requireNonNull(file, "File cannot be null");
var smbPath = SmbPath.parse(file.getAbsolutePath());
return executeSmbOperation(smbPath, (share, path) -> {
FileAllInformation fileInformation = share.getFileInformation(path);
return fileInformation.getStandardInformation().isDirectory();
});
}
@Override
public void destroy() throws Exception {
shutdown();
}
/**
* 关闭并清理所有连接和session资源
*/
public void shutdown() {
log.debug("Shutting down SMB connection pool and sessions");
// 关闭定时清理任务
try {
cleanupScheduler.shutdown();
if (!cleanupScheduler.awaitTermination(5, TimeUnit.SECONDS)) {
cleanupScheduler.shutdownNow();
}
} catch (InterruptedException e) {
cleanupScheduler.shutdownNow();
Thread.currentThread().interrupt();
}
// 关闭所有连接和session
connectionPoolLock.lock();
try {
for (Map.Entry<String, ConnectionInfo> entry : connectionPool.entrySet()) {
String hostname = entry.getKey();
ConnectionInfo connectionInfo = entry.getValue();
try {
// 先关闭所有session
connectionInfo.closeAllSessions();
// 再关闭连接
log.debug("Closing connection to host: {}", hostname);
connectionInfo.getConnection().close();
} catch (IOException e) {
log.error("Error closing connection or sessions to host: {}", hostname, e);
}
}
connectionPool.clear();
// 关闭SMB客户端
client.close();
} finally {
connectionPoolLock.unlock();
}
}
/**
* SMB操作接口用于执行具体的SMB操作
*/
@FunctionalInterface
private interface SmbOperation<T> {
T execute(DiskShare share, String path) throws IOException;
}
}