专栏首页程序猿杂货铺Spring Boot 实现 SFTP 文件上传下载

Spring Boot 实现 SFTP 文件上传下载

1. 实现背景及意义

近期由于系统迁移到docker容器,采用Spring Boot 框架实现微服务治理,在此基础上晚间批量文件服务器也由ftp改成sftp,由于之前ftp的实现是采用公具类的形式,在此基础之上,未屏蔽开发细节和依赖Spring Boot自动装配的特性,进行组件的自动装配和改造,旨在实现简化开发,提高文件传输的安全性和数据交互的可靠性。

2. 什么是SFTP

sftp是SSH File Transfer Protocol的缩写,安全文件传送协议。可以为传输文件提供一种安全的网络的加密方法。sftp 与 ftp 有着几乎一样的语法和功能。SFTP 为 SSH的其中一部分,是一种传输档案至 Blogger 伺服器的安全方式。

其实在SSH软件包中,已经包含了一个叫作SFTP(Secure File Transfer Protocol)的安全文件信息传输子系统,SFTP本身没有单独的守护进程,它必须使用sshd守护进程(端口号默认是22)来完成相应的连接和答复操作,所以从某种意义上来说,SFTP并不像一个服务器程序,而更像是一个客户端程序。

SFTP同样是使用加密传输认证信息和传输的数据,所以,使用SFTP是非常安全的。但是,由于这种传输方式使用了加密/解密技术,所以传输效率比普通的FTP要低得多。

3. SFTP文件传输在java中的实现一

3.1 Maven依赖

<dependency>
 <groupId>com.jcraft</groupId>
 <artifactId>jsch</artifactId>
 <version>0.1.54</version>
</dependency>

3.2 SFTP相关config封装

/**
 * @ClassName: SftpConfig
 * @Description: sftp配置类
 * @Author: 尚先生
 * @CreateDate: 2019/1/7
 * @Version: 1.0
 */
public class SftpConfig {
 private String hostname;
 private Integer port;
 private String username;
 private String password;
 private Integer timeout;
 private Resource privateKey;
 private String remoteRootPath;
 private String fileSuffix;
 public String getHostname() {
 return hostname;
    }
 public void setHostname(String hostname) {
 this.hostname = hostname;
    }
 public Integer getPort() {
 return port;
    }
 public void setPort(Integer port) {
 this.port = port;
    }
 public String getUsername() {
 return username;
    }
 public void setUsername(String username) {
 this.username = username;
    }
 public String getPassword() {
 return password;
    }
 public void setPassword(String password) {
 this.password = password;
    }
 public Integer getTimeout() {
 return timeout;
    }
 public void setTimeout(Integer timeout) {
 this.timeout = timeout;
    }
 public Resource getPrivateKey() {
 return privateKey;
    }
 public void setPrivateKey(Resource privateKey) {
 this.privateKey = privateKey;
    }
 public String getRemoteRootPath() {
 return remoteRootPath;
    }
 public void setRemoteRootPath(String remoteRootPath) {
 this.remoteRootPath = remoteRootPath;
    }
 public String getFileSuffix() {
 return fileSuffix;
    }
 public void setFileSuffix(String fileSuffix) {
 this.fileSuffix = fileSuffix;
    }
 public SftpConfig(String hostname, Integer port, String username, String password, Integer timeout, Resource privateKey, String remoteRootPath, String fileSuffix) {
 this.hostname = hostname;
 this.port = port;
 this.username = username;
 this.password = password;
 this.timeout = timeout;
 this.privateKey = privateKey;
 this.remoteRootPath = remoteRootPath;
 this.fileSuffix = fileSuffix;
    }
 public SftpConfig(String hostname, Integer port, String username, String password, Integer timeout, String remoteRootPath) {
 this.hostname = hostname;
 this.port = port;
 this.username = username;
 this.password = password;
 this.timeout = timeout;
 this.remoteRootPath = remoteRootPath;
    }
 public SftpConfig() {
    }
}

3.3 SFTP工具类实现

/**
 * @ClassName: SFTP
 * @Description: sftp上传通用类
 * @Author: 尚先生
 * @CreateDate: 2019/1/3
 * @Version: 1.0
 */
public class SFTP {
 private long count;
 /**
     * 已经连接次数
     */
 private long count1 = 0;
 private long sleepTime;
 private static final Logger logger = LoggerFactory.getLogger(SFTP.class);
 /**
     * 连接sftp服务器
     *
     * @return
     */
 public ChannelSftp connect(SftpConfig sftpConfig) {
 ChannelSftp sftp = null;
 try {
 JSch jsch = new JSch();
            jsch.getSession(sftpConfig.getUsername(), sftpConfig.getHostname(), sftpConfig.getPort());
 Session sshSession = jsch.getSession(sftpConfig.getUsername(), sftpConfig.getHostname(), sftpConfig.getPort());
            logger.info("Session created ... UserName=" + sftpConfig.getUsername() + ";host=" + sftpConfig.getHostname() + ";port=" + sftpConfig.getPort());
            sshSession.setPassword(sftpConfig.getPassword());
 Properties sshConfig = new Properties();
            sshConfig.put("StrictHostKeyChecking", "no");
            sshSession.setConfig(sshConfig);
            sshSession.connect();
            logger.info("Session connected ...");
            logger.info("Opening Channel ...");
 Channel channel = sshSession.openChannel("sftp");
            channel.connect();
            sftp = (ChannelSftp) channel;
            logger.info("登录成功");
        } catch (Exception e) {
 try {
                count1 += 1;
 if (count == count1) {
 throw new RuntimeException(e);
                }
 Thread.sleep(sleepTime);
                logger.info("重新连接....");
                connect(sftpConfig);
            } catch (InterruptedException e1) {
 throw new RuntimeException(e1);
            }
        }
 return sftp;
    }
 /**
     * 上传文件
     *
     * @param directory  上传的目录
     * @param uploadFile 要上传的文件
     * @param sftpConfig
     */
 public void upload(String directory, String uploadFile, SftpConfig sftpConfig) {
 ChannelSftp sftp = connect(sftpConfig);
 try {
            sftp.cd(directory);
        } catch (SftpException e) {
 try {
                sftp.mkdir(directory);
                sftp.cd(directory);
            } catch (SftpException e1) {
 throw new RuntimeException("ftp创建文件路径失败" + directory);
            }
        }
 File file = new File(uploadFile);
 InputStream inputStream=null;
 try {
            inputStream = new FileInputStream(file);
            sftp.put(inputStream, file.getName());
        } catch (Exception e) {
 throw new RuntimeException("sftp异常" + e);
        } finally {
            disConnect(sftp);
            closeStream(inputStream,null);
        }
    }
 /**
     * 下载文件
     *
     * @param directory    下载目录
     * @param downloadFile 下载的文件
     * @param saveFile     存在本地的路径
     * @param sftpConfig
     */
 public void download(String directory, String downloadFile, String saveFile, SftpConfig sftpConfig) {
 OutputStream output = null;
 try {
 File localDirFile = new File(saveFile);
 // 判断本地目录是否存在,不存在需要新建各级目录
 if (!localDirFile.exists()) {
                localDirFile.mkdirs();
            }
 if (logger.isInfoEnabled()) {
                logger.info("开始获取远程文件:[{}]---->[{}]", new Object[]{directory, saveFile});
            }
 ChannelSftp sftp = connect(sftpConfig);
            sftp.cd(directory);
 if (logger.isInfoEnabled()) {
                logger.info("打开远程文件:[{}]", new Object[]{directory});
            }
            output = new FileOutputStream(new File(saveFile.concat(File.separator).concat(downloadFile)));
            sftp.get(downloadFile, output);
 if (logger.isInfoEnabled()) {
                logger.info("文件下载成功");
            }
            disConnect(sftp);
        } catch (Exception e) {
 if (logger.isInfoEnabled()) {
                logger.info("文件下载出现异常,[{}]", e);
            }
 throw new RuntimeException("文件下载出现异常,[{}]", e);
        } finally {
            closeStream(null,output);
        }
    }
 /**
     * 下载远程文件夹下的所有文件
     *
     * @param remoteFilePath
     * @param localDirPath
     * @throws Exception
     */
 public void getFileDir(String remoteFilePath, String localDirPath, SftpConfig sftpConfig) throws Exception {
 File localDirFile = new File(localDirPath);
 // 判断本地目录是否存在,不存在需要新建各级目录
 if (!localDirFile.exists()) {
            localDirFile.mkdirs();
        }
 if (logger.isInfoEnabled()) {
            logger.info("sftp文件服务器文件夹[{}],下载到本地目录[{}]", new Object[]{remoteFilePath, localDirFile});
        }
 ChannelSftp channelSftp = connect(sftpConfig);
 Vector<LsEntry> lsEntries = channelSftp.ls(remoteFilePath);
 if (logger.isInfoEnabled()) {
            logger.info("远程目录下的文件为[{}]", lsEntries);
        }
 for (LsEntry entry : lsEntries) {
 String fileName = entry.getFilename();
 if (checkFileName(fileName)) {
 continue;
            }
 String remoteFileName = getRemoteFilePath(remoteFilePath, fileName);
            channelSftp.get(remoteFileName, localDirPath);
        }
        disConnect(channelSftp);
    }
 /**
     * 关闭流
     * @param outputStream
     */
 private void closeStream(InputStream inputStream,OutputStream outputStream) {
 if (outputStream != null) {
 try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
 if(inputStream != null){
 try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
 private boolean checkFileName(String fileName) {
 if (".".equals(fileName) || "..".equals(fileName)) {
 return true;
        }
 return false;
    }
 private String getRemoteFilePath(String remoteFilePath, String fileName) {
 if (remoteFilePath.endsWith("/")) {
 return remoteFilePath.concat(fileName);
        } else {
 return remoteFilePath.concat("/").concat(fileName);
        }
    }
 /**
     * 删除文件
     *
     * @param directory  要删除文件所在目录
     * @param deleteFile 要删除的文件
     * @param sftp
     */
 public void delete(String directory, String deleteFile, ChannelSftp sftp) {
 try {
            sftp.cd(directory);
            sftp.rm(deleteFile);
        } catch (Exception e) {
 throw new RuntimeException(e);
        }
    }
 /**
     * 列出目录下的文件
     *
     * @param directory  要列出的目录
     * @param sftpConfig
     * @return
     * @throws SftpException
     */
 public List<String> listFiles(String directory, SftpConfig sftpConfig) throws SftpException {
 ChannelSftp sftp = connect(sftpConfig);
 List fileNameList = new ArrayList();
 try {
            sftp.cd(directory);
        } catch (SftpException e) {
 return fileNameList;
        }
 Vector vector = sftp.ls(directory);
 for (int i = 0; i < vector.size(); i++) {
 if (vector.get(i) instanceof LsEntry) {
 LsEntry lsEntry = (LsEntry) vector.get(i);
 String fileName = lsEntry.getFilename();
 if (".".equals(fileName) || "..".equals(fileName)) {
 continue;
                }
                fileNameList.add(fileName);
            }
        }
        disConnect(sftp);
 return fileNameList;
    }
 /**
     * 断掉连接
     */
 public void disConnect(ChannelSftp sftp) {
 try {
            sftp.disconnect();
            sftp.getSession().disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 public SFTP(long count, long sleepTime) {
 this.count = count;
 this.sleepTime = sleepTime;
    }
 public SFTP() {
    }
}

3.4 测试类实现

/**
 * @ClassName: TestSFTPUtils
 * @Description: SFTP工具类测试类
 * @Author: 尚先生
 * @CreateDate: 2019/4/25 11:09
 * @Version: 1.0
 */
public class TestSFTPUtils {
 private static final Logger logger = LoggerFactory.getLogger(TestSFTPUtils.class);
 public static void main(String[] args) {
            SFTP ftp = new SFTP(3, 6000);
 SftpConfig sftpConfig = new SftpConfig("10.0.155.55", 22, "test", "test", 1000, "/opt/bdepfile/bdp/tset/20190425");
 try {
 List<String> list = ftp.listFiles("/opt/bdepfile/bdp/pucms/20190108", sftpConfig);
                logger.info("文件上传下载详情"  ,new Object[]{list});
            } catch (SftpException e) {
                logger.error("文件上传下载异常:[{}]" ,new Object[]{e});
            }
        }
}

4. SFTP文件传输在java中的实现二

4.1 Maven依赖

<dependency>
 <groupId>com.jcraft</groupId>
 <artifactId>jsch</artifactId>
 <version>0.1.54</version>
</dependency>

4.2 扩展上述工具类的实现

4.2.1 SFTP.java改造
/**
 * @ClassName: SFTP
 * @Description: sftp上传通用类
 * @Author: 尚先生
 * @CreateDate: 2019/1/3
 * @Version: 1.0
 */
@Component
public class SFTP {
 private ChannelSftp channelSftp;
 @Value("${sftp.remotepath}")
 private String remotepath;
 @Value("${sftp.localpath}")
 private String localpath;
 @Value("${sftp.filenames}")
 private String filenames;
 private static final String COMPLATEG_FILE_FLAG = "over_%s.dat";
 private static final Logger logger = LoggerFactory.getLogger(SFTP.class);
    ...
 public void setChannelSftp(ChannelSftp channelSftp) {
 this.channelSftp = channelSftp;
    }
 /**
     *
     * @param remotePath
     * @param remoteFileName
     * @param localPath
     * @param localFileName
     * @return
     */
 public boolean downloadFile(String remotePath, String remoteFileName, String localPath, String localFileName){
        logger.info("开始下载文件,远程路径:[{}],本地路径:[{}],文件名称:[{}]",new Object[]{remotePath,localPath,remoteFileName});
 FileOutputStream fileOutputStream = null;
 File file = new File(localPath + localFileName);
 try {
            fileOutputStream = new FileOutputStream(file);
            channelSftp.get(remotePath + remoteFileName,fileOutputStream);
 return true;
        } catch (Exception e) {
            logger.error("sftp下载文件失败:[{}]",new Object[]{e});
 return false;
        }
    }
 /**
     * 单个ok文件下载
     * @param trandate
     * @return
     */
 public boolean downloadOKFile(String trandate){
        trandate = trandate.replace("-", "");
 String localDirPath = localpath.concat("/").concat(trandate);
 File localDirFile = new File(localDirPath);
 if (!localDirFile.exists()){
            localDirFile.mkdirs();
        }else {
            logger.info("文件[{}]已存在",new Object[]{localDirPath});
 if (!localDirFile.isDirectory()){
                logger.error("文件[{}]已存在,但不是目录,文件下载失败",new Object[]{localDirPath});
 throw new RuntimeException(String.format("本地文件[{%s}]已存在,但不是目录,不能创建文件",localDirPath));
            }
        }
 String filename = String.format(COMPLATEG_FILE_FLAG, trandate);
 String remoteFilePath = remotepath.concat("/").concat(trandate).concat("/");
 String localFilePath = localDirPath.concat("/");
 boolean flag = downloadFile(remoteFilePath, filename, localFilePath, filename);
 return flag;
    }
 /**
     * 多个文件下载
     * @param trandate
     * @return
     */
 public boolean downloadCoreFilesToLocal(String trandate){
 boolean flag = false;
        trandate = trandate.replace("-", "");
 String localDirPath = localpath.concat("/").concat(trandate).concat("/");
 String remoteDirPath = remotepath.concat("/").concat(trandate).concat("/");
 for (String coreFileName : filenames.split(",")){
 //文件名称截取
 String coreFilaName = String.format(coreFileName.trim(), trandate);
            flag = downloadFile(remoteDirPath, coreFileName, localDirPath, coreFileName);
        }
 return flag;
    }
}
4.2.2 自动装配SFTP连接器
/**
 * @ClassName: SftpClientConfigure
 * @Description: 自动装配Sftp连接器
 * @Author: 尚先生
 * @CreateDate: 2019/4/25
 * @Version: 1.0
 */
@Configuration
@ConfigurationProperties(prefix = "sftp")
public class SftpClientConfigure {
 private String hostname;
 private Integer port;
 private String username;
 private String password;
 private Integer timeout;
 private String privateKey;
 private String remoteRootPath;
 private String fileSuffix;
 // 通道类型
 private static final String CHANNEL_TYPE = "sftp";
 private static final Logger logger = LoggerFactory.getLogger(SftpClientConfigure.class);
 public String getHostname() {
 return hostname;
    }
 public void setHostname(String hostname) {
 this.hostname = hostname;
    }
 public Integer getPort() {
 return port;
    }
 public void setPort(Integer port) {
 this.port = port;
    }
 public String getUsername() {
 return username;
    }
 public void setUsername(String username) {
 this.username = username;
    }
 public String getPassword() {
 return password;
    }
 public void setPassword(String password) {
 this.password = password;
    }
 public String getPrivateKey() {
 return privateKey;
    }
 public void setPrivateKey(String privateKey) {
 this.privateKey = privateKey;
    }
 public String getRemoteRootPath() {
 return remoteRootPath;
    }
 public void setRemoteRootPath(String remoteRootPath) {
 this.remoteRootPath = remoteRootPath;
    }
 public String getFileSuffix() {
 return fileSuffix;
    }
 public void setFileSuffix(String fileSuffix) {
 this.fileSuffix = fileSuffix;
    }
 public Integer getTimeout() {
 return timeout;
    }
 public void setTimeout(Integer timeout) {
 this.timeout = timeout;
    }
 @Bean("sshSession")
 @Lazy
 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
 public Session session() throws JSchException {
 if (logger.isInfoEnabled()) {
            logger.info("获取session,设置的超时时间为[{}]毫秒", timeout);
        }
 JSch jsch = new JSch();
 Session session = jsch.getSession(username, hostname, port);
 // 设置秘钥
//        jsch.addIdentity(privateKey);
        session.setPassword(password);
 Properties config = new Properties();
        config.put("StrictHostKeyChecking", "no");
        session.setConfig(config);  //为Session对象设置properties
        session.setTimeout(timeout);  //设置timeout时间
        session.connect();  //通过Session建立链接
 return session;
    }
 @Bean("coreSftpChannel")
 @Lazy
 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
 public ChannelSftp channel(Session session) throws JSchException {
 if (logger.isInfoEnabled()) {
            logger.info("初始化sftp连接");
        }
 Channel channel = session.openChannel(CHANNEL_TYPE);  //打开SFTP通道
        channel.connect();  //建立SFTP通道的连接
 return (ChannelSftp) channel;
    }
}
4.2.3 配置文件
# sftp配置sftp.hostname=10.0.155.55
sftp.port=22
sftp.username=test
sftp.password=test
sftp.timeout=6000
sftp.privateKey=
sftp.remotepath=/opt/bdepfile/bdp/tset
sftp.localpath=D:/core
sftp.filenames=duebillInfo_%s.dat,repayInfo_%s.dat
4.2.4 测试类实现
/**
 * @ClassName: TestAutoConfigurationSFTP
 * @Description: SftpClientConfigure测试类
 * @Author: 尚先生
 * @CreateDate: 2019/4/25
 * @Version: 1.0
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestAutoConfigurationSFTP {
 private static final Logger logger = LoggerFactory.getLogger(TestAutoConfigurationSFTP.class);
 @Autowired
 @Qualifier("coreSftpChannel")
 private ChannelSftp channelSftp;
 @Autowired
 private SFTP sftp;
 @Test
 public void testAotuDownload(){
 String trandate = "2019-04-25";
 boolean flag = false;
        sftp.setChannelSftp(channelSftp);
        flag = sftp.downloadOKFile(trandate);
        flag = sftp.downloadCoreFilesToLocal(trandate);
        logger.error("下载文件结果:[{}]",new Object[]{flag});
    }
}
4.2.5 测试结果
打开 Git Bash Here
cd D:\core
ll
 20190425
cd 20190425
ll -als
    ./
    ../
    duebillInfo_20190425.dat
    repayInfo_20190425.dat
    over_20190425.dat

4.3 SFTP扩展实现

由于当前 SpringBoot环境实现,采用的是 SpringBoot"自动装配"实现的,自动注入并调用实现从核心拉取文件的功能。

  1. 在项目中取固定文件时,只需动态追加或者替换下面的配置项 sftp.filenames=duebillInfo_%s.dat,repayInfo_%s.dat
  2. 如果是新增sftp连接时可以手动创建 SftpConfigconfig=newSftpConfig(); SFTP sftp=newSFTP(); sftp.download(directory,downloadFile,saveFile,sftpConfig)
  3. 至此,多种方式实现sftp文件传输可以共存,而且在系统中可以实现"自动装配"。

完整代码和相关依赖请见GitHub

https://github.com/dwyanewede/project-learn/tree/master/src/main/java/com/learn/demo/sftp

本文分享自微信公众号 - 程序猿杂货铺(zhoudl_l),作者:尚先生

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-04-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • java8新特性(一):Lambda表达式

    Lambda 是一个匿名函数,我们可以把Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的...

    周三不加班
  • 啥?前后端数据到现在还是明文的?DES与3DES 加解密了解一下

    我们在线上经常使用DES加密用户id,以下简称(encodeId),后端传给前端,前端会使用localStorage保存encodeId,然后调用接口时将enc...

    周三不加班
  • Java 实现日志脱敏处理

    在日常工作中,日志处理是我们每一个程序员必备的素质,但是在有些场景下客户信息敏感,需要进行某些字段,或者某部分字段的脱敏处理。接到需求我们开始操刀!

    周三不加班
  • Aptana的破解

    最近写JS比较多,常常苦恼与没有一个顺手的IDE。Editplus虽然用的熟,不过那个的效率太低而且代码看起来也很不方便,经过一个多月的试用,发现了一款好用的编...

    大江小浪
  • Spring Cloud Gateway-自定义GatewayFilter

    GatewayFilter的作用域是指定的路由配置,路由配置选项里面需要通过filters指定想要使用的GatewayFilter列表。我们可以通过自定义Gat...

    Throwable
  • Java微信公众平台开发_03_消息管理之被动回复消息

    上一节,我们启用服务器配置的时候,填写了一个服务器地址(url),如下图,这个url就是回调url,是开发者用来接收微信消息和事件的接口URL 。也就是说,用户...

    shirayner
  • Java微信公众平台开发(四)--回复消息的分类及实体的创建

    前面有说道对接收到微信服务器消息后对消息的分类,当时主要分为普通消息和事件消息,这里我们要讲述的是我们在给用户回复的消息类型,在这里也可以大致分为两类:一种为不...

    用户2417870
  • Android 天气APP(十九)更换新版API接口(更高、更快、更强)

    近段时间,和风天气上线了新的API版本,并且给所有的和风开发者发送了邮件,好像是7月10号,哪个时候我去看了一下,发现改动还是有的,和风天气V7版开发API文档...

    晨曦_LLW
  • 关于spring中的validate注解后台校验的解析

    在后台开发过程中,对参数的校验成为开发环境不可缺少的一个环节。比如参数不能为null,email那么必须符合email的格式,如果手动进行if判断或者写正则表达...

    Dream城堡
  • 第5章—构建Spring Web应用程序—关于spring中的validate注解后台校验的解析

    在后台开发过程中,对参数的校验成为开发环境不可缺少的一个环节。比如参数不能为null,email那么必须符合email的格式,如果手动进行if判断或者写正则表达...

    Dream城堡

扫码关注云+社区

领取腾讯云代金券