需求背景
需要将本地的文件上传到目标服务器,并且这个操作是比较频繁,而且可能后续作为业务支撑的一部分,介于此站在巨人的肩膀上,借助jsch 实现远程文件传输。
一、依赖配置
这里我们选用jsch,引入依赖
<!-- SSH连接依赖 -->
<dependency>
<groupId>com.github.mwiede</groupId>
<artifactId>jsch</artifactId>
<version>0.2.16</version>
</dependency>
二、配置对象
这里使用了lombok 注解 和 swagger注解,如果项目中不支持请去掉,不影响
@Component
@Data
@ConfigurationProperties(prefix = "file-transfer.server")
@AllArgsConstructor
@NoArgsConstructor
public class FileTransferProperties {
@Schema(description = "文件传输服务器地址")
private String host;
@Schema(description = "文件传输服务器端口")
private int port = 22;
@Schema(description = "文件传输服务器用户名")
private String username;
@Schema(description = "文件传输服务器密码")
private String password;
@Schema(description = "文件传输服务器私钥路径")
private String privateKeyPath;
@Schema(description = "文件传输服务器目标目录")
private String targetDirectory;
@Schema(description = "文件传输服务器连接超时时间")
private int connectionTimeout = 30000;
@Schema(description = "文件传输服务器会话超时时间")
private int sessionTimeout = 60000;
}
三、配置文件配置
我们在pom.yaml文件中加上下面配置,扩展:下面的密码可以使用加密后的字符串然后在代码中做解密处理
# 文件传输服务器配置
file-transfer:
server:
host: 192.168.125.4
port: 22
username: root
password: root
# 或者使用私钥认证
# private-key-path: /path/to/private/key
target-directory: /usr/local/services/app
connection-timeout: 30000
session-timeout: 60000
四、文件传输工具类
注意:这里面的文件路径分隔符使用”/“,而不是使用 File.separator, 为什么呢?因为如果我们本地是windows则路径分隔符为”\“与linux路径分隔符不对应,文件会创建失败,这个分隔符”/“不管是linux和windows都支持
package cn.bdmcom.utils;
import cn.bdmcom.config.FileTransferProperties;
import com.jcraft.jsch.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileInputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
@Component
public class FileTransferUtil {
private static final Logger logger = LoggerFactory.getLogger(FileTransferUtil.class);
private final FileTransferProperties fileTransferProperties;
public FileTransferUtil(FileTransferProperties fileTransferProperties) {
this.fileTransferProperties = fileTransferProperties;
}
/**
* 将指定文件夹下的所有文件传输到远程服务器
*
* @param localFolderPath 本地文件夹路径
* @return 传输结果,true表示成功,false表示失败
*/
public boolean transferFolder(String localFolderPath) {
return transferFolder(localFolderPath, fileTransferProperties.getTargetDirectory());
}
/**
* 将指定文件夹下的所有文件传输到远程服务器的指定目录
*
* @param localFolderPath 本地文件夹路径
* @param remoteTargetPath 远程目标目录
* @return 传输结果,true表示成功,false表示失败
*/
public boolean transferFolder(String localFolderPath, String remoteTargetPath) {
File localFolder = new File(localFolderPath);
if (!localFolder.exists() || !localFolder.isDirectory()) {
logger.error("本地文件夹不存在或不是目录: {}", localFolderPath);
return false;
}
List<File> allFiles = getAllFiles(localFolder);
if (allFiles.isEmpty()) {
logger.warn("文件夹为空: {}", localFolderPath);
return true;
}
Session session = null;
ChannelSftp sftpChannel = null;
try {
session = createSession();
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
// 确保远程目录存在
createRemoteDirectoryIfNotExists(sftpChannel, remoteTargetPath);
// 传输所有文件
int successCount = 0;
for (File file : allFiles) {
if (transferSingleFile(sftpChannel, file, localFolder, remoteTargetPath)) {
successCount++;
} else {
logger.error("传输文件失败: {}", file.getAbsolutePath());
}
}
logger.info("文件传输完成,成功: {}, 总数: {}", successCount, allFiles.size());
return successCount == allFiles.size();
} catch (Exception e) {
logger.error("文件传输过程中发生错误", e);
return false;
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 传输单个文件
*
* @param filePath 文件路径
* @return 传输结果
*/
public boolean transferSingleFile(String filePath) {
return transferSingleFile(filePath, fileTransferProperties.getTargetDirectory());
}
/**
* 传输单个文件到指定远程目录
*
* @param filePath 文件路径
* @param remoteTargetPath 远程目标目录
* @return 传输结果
*/
public boolean transferSingleFile(String filePath, String remoteTargetPath) {
File file = new File(filePath);
if (!file.exists() || !file.isFile()) {
logger.error("文件不存在或不是文件: {}", filePath);
return false;
}
Session session = null;
ChannelSftp sftpChannel = null;
try {
session = createSession();
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
createRemoteDirectoryIfNotExists(sftpChannel, remoteTargetPath);
String remoteFilePath = remoteTargetPath + "/" + file.getName();
sftpChannel.put(new FileInputStream(file), remoteFilePath);
logger.info("文件传输成功: {} -> {}", filePath, remoteFilePath);
return true;
} catch (Exception e) {
logger.error("传输文件失败: {}", filePath, e);
return false;
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 获取文件夹下所有文件(递归)
*/
private List<File> getAllFiles(File directory) {
List<File> allFiles = new ArrayList<>();
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
allFiles.add(file);
} else if (file.isDirectory()) {
allFiles.addAll(getAllFiles(file));
}
}
}
return allFiles;
}
/**
* 传输单个文件(内部方法)
*/
private boolean transferSingleFile(ChannelSftp sftpChannel, File file, File baseFolder, String remoteBasePath) {
try {
// 计算相对路径
String relativePath = getRelativePath(baseFolder, file);
String remoteFilePath = remoteBasePath + "/" + relativePath.replace(File.separator, "/");
// 确保远程目录存在
String remoteDir = remoteFilePath.substring(0, remoteFilePath.lastIndexOf("/"));
createRemoteDirectoryIfNotExists(sftpChannel, remoteDir);
// 上传文件
sftpChannel.put(new FileInputStream(file), remoteFilePath);
logger.debug("文件上传成功: {} -> {}", file.getAbsolutePath(), remoteFilePath);
return true;
} catch (Exception e) {
logger.error("上传文件失败: {}", file.getAbsolutePath(), e);
return false;
}
}
/**
* 获取文件相对于基础文件夹的路径
*/
private String getRelativePath(File baseFolder, File file) {
Path basePath = Paths.get(baseFolder.getAbsolutePath());
Path filePath = Paths.get(file.getAbsolutePath());
return basePath.relativize(filePath).toString();
}
/**
* 创建SSH会话
*/
private Session createSession() throws JSchException {
JSch jsch = new JSch();
// 如果配置了私钥路径,使用私钥认证
if (fileTransferProperties.getPrivateKeyPath() != null && !fileTransferProperties.getPrivateKeyPath().trim().isEmpty()) {
jsch.addIdentity(fileTransferProperties.getPrivateKeyPath());
}
Session session = jsch.getSession(
fileTransferProperties.getUsername(),
fileTransferProperties.getHost(),
fileTransferProperties.getPort()
);
// 如果配置了密码,设置密码
if (fileTransferProperties.getPassword() != null && !fileTransferProperties.getPassword().trim().isEmpty()) {
session.setPassword(fileTransferProperties.getPassword());
}
// SSH配置
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
session.setTimeout(fileTransferProperties.getConnectionTimeout());
session.connect(fileTransferProperties.getSessionTimeout());
return session;
}
/**
* 创建远程目录(如果不存在)
*/
private void createRemoteDirectoryIfNotExists(ChannelSftp sftpChannel, String remotePath) throws SftpException {
String[] dirs = remotePath.split("/");
String currentPath = "";
for (String dir : dirs) {
if (dir.isEmpty()) continue;
currentPath += "/" + dir;
try {
sftpChannel.stat(currentPath);
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
sftpChannel.mkdir(currentPath);
logger.debug("创建远程目录: {}", currentPath);
} else {
throw e;
}
}
}
}
/**
* 关闭连接
*/
private void closeConnections(ChannelSftp sftpChannel, Session session) {
if (sftpChannel != null && sftpChannel.isConnected()) {
sftpChannel.disconnect();
}
if (session != null && session.isConnected()) {
session.disconnect();
}
}
/**
* 测试连接
*/
public boolean testConnection() {
Session session = null;
try {
session = createSession();
logger.info("SSH连接测试成功,服务器: {}:{}", fileTransferProperties.getHost(), fileTransferProperties.getPort());
return true;
} catch (Exception e) {
logger.error("SSH连接测试失败", e);
return false;
} finally {
if (session != null && session.isConnected()) {
session.disconnect();
}
}
}
}
五、控制器
可以通过swagger进行测试,经过测试,功能都是正常的,可以方便食用 注意:这里的swagger使用的是 v3 版本,可以自行替换注解
package cn.bdmcom.controller;
import cn.bdmcom.utils.FileTransferUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/file-transfer")
@Tag(name = "FileTransferUtil", description = "文件传输相关接口")
public class FileTransferController {
private final FileTransferUtil fileTransferUtil;
public FileTransferController(FileTransferUtil fileTransferUtil) {
this.fileTransferUtil = fileTransferUtil;
}
@PostMapping("/test-connection")
@Operation(summary = "测试服务器连接", description = "测试SSH服务器连接是否正常")
public Boolean testConnection() {
return fileTransferUtil.testConnection();
}
@PostMapping("/transfer-file")
@Operation(summary = "传输单个文件", description = "将指定文件传输到远程服务器")
public Boolean transferFile(
@Parameter(description = "本地文件路径", required = true)
@RequestParam String filePath) {
return fileTransferUtil.transferSingleFile(filePath);
}
@PostMapping("/transfer-file-to-path")
@Operation(summary = "传输文件到指定路径", description = "将指定文件传输到远程服务器的指定目录")
public Boolean transferFileToPath(
@Parameter(description = "本地文件路径", required = true)
@RequestParam String filePath,
@Parameter(description = "远程目标目录", required = true)
@RequestParam String remotePath) {
return fileTransferUtil.transferSingleFile(filePath, remotePath);
}
@PostMapping("/transfer-folder")
@Operation(summary = "传输文件夹", description = "将指定文件夹下的所有文件传输到远程服务器")
public Boolean transferFolder(
@Parameter(description = "本地文件夹路径", required = true)
@RequestParam String folderPath) {
return fileTransferUtil.transferFolder(folderPath);
}
@PostMapping("/transfer-folder-to-path")
@Operation(summary = "传输文件夹到指定路径", description = "将指定文件夹下的所有文件传输到远程服务器的指定目录")
public Boolean transferFolderToPath(
@Parameter(description = "本地文件夹路径", required = true)
@RequestParam String folderPath,
@Parameter(description = "远程目标目录", required = true)
@RequestParam String remotePath) {
return fileTransferUtil.transferFolder(folderPath, remotePath);
}
}