2025年SpringBoot实现远程文件传输

240 阅读5分钟

需求背景

需要将本地的文件上传到目标服务器,并且这个操作是比较频繁,而且可能后续作为业务支撑的一部分,介于此站在巨人的肩膀上,借助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);
    }
}