<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在專案開發中,一般檔案儲存很少再使用SFTP服務,但是也不排除合作伙伴使用SFTP來儲存專案中的檔案或者通過SFTP來實現檔案資料的互動。
我遇到的專案中,就有銀行和保險公司等合作伙伴通過SFTP服務來實現與我們專案的檔案資料的互動。
為了能夠順利地完成與友商的SFTP服務的連通,我們需要在自己的專案中實現一套SFTP使用者端工具。一般我們會採用Jsch來實現SFTP使用者端。
<!--執行遠端操作--> <dependency> <groupId>com.jcraft</groupId> <artifactId>jsch</artifactId> <version>0.1.55</version> </dependency> <!--連結池--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.11.1</version> </dependency>
首先我們一定要引入jsch
依賴,這個是我們實現SFTP使用者端的基石;其次我們引入了連結池工具,為了避免每次執行SFTP命令都要重新建立連結,我們使用池化的方式優化了比較消耗資源的建立操作。
為了更好的使用SFTP工具,我們把jsch
中關於SFTP的相關功能提煉出來,做了一次簡單的封裝,做成了我們可以直接使用的工具類。
裡面只有兩類方法:
1.建立Session與開啟Session;
session建立好後,還不能建立channel,需要開啟session後才能建立channel;
2.建立channel與開啟channel;
channel也是一樣,建立好的channel需要開啟後才能真正地執行命令;
public class JschUtil { /** * 建立session * * @param userName 使用者名稱 * @param password 密碼 * @param host 域名 * @param port 埠 * @param privateKeyFile 金鑰檔案 * @param passphrase 口令 * @return * @throws AwesomeException */ public static Session createSession(String userName, String password, String host, int port, String privateKeyFile, String passphrase) throws AwesomeException { return createSession(new JSch(), userName, password, host, port, privateKeyFile, passphrase); } /** * 建立session * * @param jSch * @param userName 使用者名稱 * @param password 密碼 * @param host 域名 * @param port 埠 * @param privateKeyFile 金鑰 * @param passphrase 口令 * @return * @throws AwesomeException */ public static Session createSession(JSch jSch, String userName, String password, String host, int port, String privateKeyFile, String passphrase) throws AwesomeException { try { if (!StringUtils.isEmpty(privateKeyFile)) { // 使用金鑰驗證方式,金鑰可以是有口令的金鑰,也可以是沒有口令的金鑰 if (!StringUtils.isEmpty(passphrase)) { jSch.addIdentity(privateKeyFile, passphrase); } else { jSch.addIdentity(privateKeyFile); } } // 獲取session Session session = jSch.getSession(userName, host, port); if (!StringUtils.isEmpty(password)) { session.setPassword(password); } // 不校驗域名 session.setConfig("StrictHostKeyChecking", "no"); return session; } catch (Exception e) { throw new AwesomeException(500, "create session fail"); } } /** * 建立session * * @param jSch * @param userName 使用者名稱 * @param password 密碼 * @param host 域名 * @param port 埠 * @return * @throws AwesomeException */ public static Session createSession(JSch jSch, String userName, String password, String host, int port) throws AwesomeException { return createSession(jSch, userName, password, host, port, StringUtils.EMPTY, StringUtils.EMPTY); } /** * 建立session * * @param jSch * @param userName 使用者名稱 * @param host 域名 * @param port 埠 * @return * @throws AwesomeException */ private Session createSession(JSch jSch, String userName, String host, int port) throws AwesomeException { return createSession(jSch, userName, StringUtils.EMPTY, host, port, StringUtils.EMPTY, StringUtils.EMPTY); } /** * 開啟session連結 * * @param jSch * @param userName 使用者名稱 * @param password 密碼 * @param host 域名 * @param port 埠 * @param privateKeyFile 金鑰 * @param passphrase 口令 * @param timeout 連結超時時間 * @return * @throws AwesomeException */ public static Session openSession(JSch jSch, String userName, String password, String host, int port, String privateKeyFile, String passphrase, int timeout) throws AwesomeException { Session session = createSession(jSch, userName, password, host, port, privateKeyFile, passphrase); try { if (timeout >= 0) { session.connect(timeout); } else { session.connect(); } return session; } catch (Exception e) { throw new AwesomeException(500, "session connect fail"); } } /** * 開啟session連結 * * @param userName 使用者名稱 * @param password 密碼 * @param host 域名 * @param port 埠 * @param privateKeyFile 金鑰 * @param passphrase 口令 * @param timeout 連結超時時間 * @return * @throws AwesomeException */ public static Session openSession(String userName, String password, String host, int port, String privateKeyFile, String passphrase, int timeout) throws AwesomeException { Session session = createSession(userName, password, host, port, privateKeyFile, passphrase); try { if (timeout >= 0) { session.connect(timeout); } else { session.connect(); } return session; } catch (Exception e) { throw new AwesomeException(500, "session connect fail"); } } /** * 開啟session連結 * * @param jSch * @param userName 使用者名稱 * @param password 密碼 * @param host 域名 * @param port 埠 * @param timeout 連結超時時間 * @return * @throws AwesomeException */ public static Session openSession(JSch jSch, String userName, String password, String host, int port, int timeout) throws AwesomeException { return openSession(jSch, userName, password, host, port, StringUtils.EMPTY, StringUtils.EMPTY, timeout); } /** * 開啟session連結 * * @param userName 使用者名稱 * @param password 密碼 * @param host 域名 * @param port 埠 * @param timeout 連結超時時間 * @return * @throws AwesomeException */ public static Session openSession(String userName, String password, String host, int port, int timeout) throws AwesomeException { return openSession(userName, password, host, port, StringUtils.EMPTY, StringUtils.EMPTY, timeout); } /** * 開啟session連結 * * @param jSch * @param userName 使用者名稱 * @param host 域名 * @param port 埠 * @param timeout 連結超時時間 * @return * @throws AwesomeException */ public static Session openSession(JSch jSch, String userName, String host, int port, int timeout) throws AwesomeException { return openSession(jSch, userName, StringUtils.EMPTY, host, port, StringUtils.EMPTY, StringUtils.EMPTY, timeout); } /** * 開啟session連結 * * @param userName 使用者名稱 * @param host 域名 * @param port 埠 * @param timeout 連結超時時間 * @return * @throws AwesomeException */ public static Session openSession(String userName, String host, int port, int timeout) throws AwesomeException { return openSession(userName, StringUtils.EMPTY, host, port, StringUtils.EMPTY, StringUtils.EMPTY, timeout); } /** * 建立指定通道 * * @param session * @param channelType * @return * @throws AwesomeException */ public static Channel createChannel(Session session, ChannelType channelType) throws AwesomeException { try { if (!session.isConnected()) { session.connect(); } return session.openChannel(channelType.getValue()); } catch (Exception e) { throw new AwesomeException(500, "open channel fail"); } } /** * 建立sftp通道 * * @param session * @return * @throws AwesomeException */ public static ChannelSftp createSftp(Session session) throws AwesomeException { return (ChannelSftp) createChannel(session, ChannelType.SFTP); } /** * 建立shell通道 * * @param session * @return * @throws AwesomeException */ public static ChannelShell createShell(Session session) throws AwesomeException { return (ChannelShell) createChannel(session, ChannelType.SHELL); } /** * 開啟通道 * * @param session * @param channelType * @param timeout * @return * @throws AwesomeException */ public static Channel openChannel(Session session, ChannelType channelType, int timeout) throws AwesomeException { Channel channel = createChannel(session, channelType); try { if (timeout >= 0) { channel.connect(timeout); } else { channel.connect(); } return channel; } catch (Exception e) { throw new AwesomeException(500, "connect channel fail"); } } /** * 開啟sftp通道 * * @param session * @param timeout * @return * @throws AwesomeException */ public static ChannelSftp openSftpChannel(Session session, int timeout) throws AwesomeException { return (ChannelSftp) openChannel(session, ChannelType.SFTP, timeout); } /** * 開啟shell通道 * * @param session * @param timeout * @return * @throws AwesomeException */ public static ChannelShell openShellChannel(Session session, int timeout) throws AwesomeException { return (ChannelShell) openChannel(session, ChannelType.SHELL, timeout); } enum ChannelType { SESSION("session"), SHELL("shell"), EXEC("exec"), X11("x11"), AGENT_FORWARDING("auth-agent@openssh.com"), DIRECT_TCPIP("direct-tcpip"), FORWARDED_TCPIP("forwarded-tcpip"), SFTP("sftp"), SUBSYSTEM("subsystem"); private final String value; ChannelType(String value) { this.value = value; } public String getValue() { return this.value; } } }
我們通過實現BasePooledObjectFactory
類來池化通道ChannelSftp
。這並不是真正池化的程式碼,下面的程式碼只是告知池化管理器如何建立物件和銷燬物件。
static class SftpFactory extends BasePooledObjectFactory<ChannelSftp> implements AutoCloseable { private Session session; private SftpProperties properties; // 初始化SftpFactory // 裡面主要是建立目標session,後續可用通過這個session不斷地建立ChannelSftp。 SftpFactory(SftpProperties properties) throws AwesomeException { this.properties = properties; String username = properties.getUsername(); String password = properties.getPassword(); String host = properties.getHost(); int port = properties.getPort(); String privateKeyFile = properties.getPrivateKeyFile(); String passphrase = properties.getPassphrase(); session = JschUtil.createSession(username, password, host, port, privateKeyFile, passphrase); } // 銷燬物件,主要是銷燬ChannelSftp @Override public void destroyObject(PooledObject<ChannelSftp> p) throws Exception { p.getObject().disconnect(); } // 建立物件ChannelSftp @Override public ChannelSftp create() throws Exception { int timeout = properties.getTimeout(); return JschUtil.openSftpChannel(this.session, timeout); } // 包裝建立出來的物件 @Override public PooledObject<ChannelSftp> wrap(ChannelSftp channelSftp) { return new DefaultPooledObject<>(channelSftp); } // 驗證物件是否可用 @Override public boolean validateObject(PooledObject<ChannelSftp> p) { return p.getObject().isConnected(); } // 銷燬資源,關閉session @Override public void close() throws Exception { if (Objects.nonNull(session)) { if (session.isConnected()) { session.disconnect(); } session = null; } } }
為了實現真正的池化操作,我們還需要以下程式碼:
1.我們需要在SftpClient物件中建立一個GenericObjectPool
物件池,這個才是真正的池子,它負責建立和儲存所有的物件。
2.我們還需要提供資源銷燬的功能,也就是實現AutoCloseable
,在服務停止時,需要把相關的資源銷燬。
public class SftpClient implements AutoCloseable { private SftpFactory sftpFactory; GenericObjectPool<ChannelSftp> objectPool; // 構造方法1 public SftpClient(SftpProperties properties, GenericObjectPoolConfig<ChannelSftp> poolConfig) throws AwesomeException { this.sftpFactory = new SftpFactory(properties); objectPool = new GenericObjectPool<>(this.sftpFactory, poolConfig); } // 構造方法2 public SftpClient(SftpProperties properties) throws AwesomeException { this.sftpFactory = new SftpFactory(properties); SftpProperties.PoolConfig config = properties.getPool(); // 預設池化設定 if (Objects.isNull(config)) { objectPool = new GenericObjectPool<>(this.sftpFactory); } else { // 自定義池化設定 GenericObjectPoolConfig<ChannelSftp> poolConfig = new GenericObjectPoolConfig<>(); poolConfig.setMaxIdle(config.getMaxIdle()); poolConfig.setMaxTotal(config.getMaxTotal()); poolConfig.setMinIdle(config.getMinIdle()); poolConfig.setTestOnBorrow(config.isTestOnBorrow()); poolConfig.setTestOnCreate(config.isTestOnCreate()); poolConfig.setTestOnReturn(config.isTestOnReturn()); poolConfig.setTestWhileIdle(config.isTestWhileIdle()); poolConfig.setBlockWhenExhausted(config.isBlockWhenExhausted()); poolConfig.setMaxWait(Duration.ofMillis(config.getMaxWaitMillis())); poolConfig.setTimeBetweenEvictionRuns(Duration.ofMillis(config.getTimeBetweenEvictionRunsMillis())); objectPool = new GenericObjectPool<>(this.sftpFactory, poolConfig); } } // 銷燬資源 @Override public void close() throws Exception { // 銷燬連結池 if (Objects.nonNull(this.objectPool)) { if (!this.objectPool.isClosed()) { this.objectPool.close(); } } this.objectPool = null; // 銷燬sftpFactory if (Objects.nonNull(this.sftpFactory)) { this.sftpFactory.close(); } } }
我們已經對連結池進行了初始化,下面我們就可以從連結池中獲取我們需要的ChannelSftp
來實現檔案的上傳下載了。
下面實現了多種檔案上傳和下載的方式:
1.直接把本地檔案上傳到SFTP伺服器的指定路徑;
2.把InputStream輸入流提交到SFTP伺服器指定路徑中;
3.可以針對以上兩種上傳方式進行進度的監測;
4.把SFTP伺服器中的指定檔案下載到本地機器上;
5.把SFTP伺服器˙中的檔案寫入指定的輸出流;
6.針對以上兩種下載方式,監測下載進度;
/** * 上傳檔案 * * @param srcFilePath * @param targetDir * @param targetFileName * @return * @throws AwesomeException */ public boolean uploadFile(String srcFilePath, String targetDir, String targetFileName) throws AwesomeException { return uploadFile(srcFilePath, targetDir, targetFileName, null); } /** * 上傳檔案 * * @param srcFilePath * @param targetDir * @param targetFileName * @param monitor * @return * @throws AwesomeException */ public boolean uploadFile(String srcFilePath, String targetDir, String targetFileName, SftpProgressMonitor monitor) throws AwesomeException { ChannelSftp channelSftp = null; try { // 從連結池獲取物件 channelSftp = this.objectPool.borrowObject(); // 如果不存在目標資料夾 if (!exist(channelSftp, targetDir)) { mkdirs(channelSftp, targetDir); } channelSftp.cd(targetDir); // 上傳檔案 if (Objects.nonNull(monitor)) { channelSftp.put(srcFilePath, targetFileName, monitor); } else { channelSftp.put(srcFilePath, targetFileName); } return true; } catch (Exception e) { throw new AwesomeException(500, "upload file fail"); } finally { if (Objects.nonNull(channelSftp)) { // 返還物件給連結池 this.objectPool.returnObject(channelSftp); } } } /** * 上傳檔案到目標資料夾 * * @param in * @param targetDir * @param targetFileName * @return * @throws AwesomeException */ public boolean uploadFile(InputStream in, String targetDir, String targetFileName) throws AwesomeException { return uploadFile(in, targetDir, targetFileName, null); } /** * 上傳檔案,新增進度監視器 * * @param in * @param targetDir * @param targetFileName * @param monitor * @return * @throws AwesomeException */ public boolean uploadFile(InputStream in, String targetDir, String targetFileName, SftpProgressMonitor monitor) throws AwesomeException { ChannelSftp channelSftp = null; try { channelSftp = this.objectPool.borrowObject(); // 如果不存在目標資料夾 if (!exist(channelSftp, targetDir)) { mkdirs(channelSftp, targetDir); } channelSftp.cd(targetDir); if (Objects.nonNull(monitor)) { channelSftp.put(in, targetFileName, monitor); } else { channelSftp.put(in, targetFileName); } return true; } catch (Exception e) { throw new AwesomeException(500, "upload file fail"); } finally { if (Objects.nonNull(channelSftp)) { this.objectPool.returnObject(channelSftp); } } } /** * 下載檔案 * * @param remoteFile * @param targetFilePath * @return * @throws AwesomeException */ public boolean downloadFile(String remoteFile, String targetFilePath) throws AwesomeException { return downloadFile(remoteFile, targetFilePath, null); } /** * 下載目標檔案到本地 * * @param remoteFile * @param targetFilePath * @return * @throws AwesomeException */ public boolean downloadFile(String remoteFile, String targetFilePath, SftpProgressMonitor monitor) throws AwesomeException { ChannelSftp channelSftp = null; try { channelSftp = this.objectPool.borrowObject(); // 如果不存在目標資料夾 if (!exist(channelSftp, remoteFile)) { // 不用下載了 return false; } File targetFile = new File(targetFilePath); try (FileOutputStream outputStream = new FileOutputStream(targetFile)) { if (Objects.nonNull(monitor)) { channelSftp.get(remoteFile, outputStream, monitor); } else { channelSftp.get(remoteFile, outputStream); } } return true; } catch (Exception e) { throw new AwesomeException(500, "upload file fail"); } finally { if (Objects.nonNull(channelSftp)) { this.objectPool.returnObject(channelSftp); } } } /** * 下載檔案 * * @param remoteFile * @param outputStream * @return * @throws AwesomeException */ public boolean downloadFile(String remoteFile, OutputStream outputStream) throws AwesomeException { return downloadFile(remoteFile, outputStream, null); } /** * 下載檔案 * * @param remoteFile * @param outputStream * @param monitor * @return * @throws AwesomeException */ public boolean downloadFile(String remoteFile, OutputStream outputStream, SftpProgressMonitor monitor) throws AwesomeException { ChannelSftp channelSftp = null; try { channelSftp = this.objectPool.borrowObject(); // 如果不存在目標資料夾 if (!exist(channelSftp, remoteFile)) { // 不用下載了 return false; } if (Objects.nonNull(monitor)) { channelSftp.get(remoteFile, outputStream, monitor); } else { channelSftp.get(remoteFile, outputStream); } return true; } catch (Exception e) { throw new AwesomeException(500, "upload file fail"); } finally { if (Objects.nonNull(channelSftp)) { this.objectPool.returnObject(channelSftp); } } } /** * 建立資料夾 * * @param channelSftp * @param dir * @return */ protected boolean mkdirs(ChannelSftp channelSftp, String dir) { try { String pwd = channelSftp.pwd(); if (StringUtils.contains(pwd, dir)) { return true; } String relativePath = StringUtils.substringAfter(dir, pwd); String[] dirs = StringUtils.splitByWholeSeparatorPreserveAllTokens(relativePath, "/"); for (String path : dirs) { if (StringUtils.isBlank(path)) { continue; } try { channelSftp.cd(path); } catch (SftpException e) { channelSftp.mkdir(path); channelSftp.cd(path); } } return true; } catch (Exception e) { return false; } } /** * 判斷資料夾是否存在 * * @param channelSftp * @param dir * @return */ protected boolean exist(ChannelSftp channelSftp, String dir) { try { channelSftp.lstat(dir); return true; } catch (Exception e) { return false; } }
我們可以通過java config
的方式,把我們已經實現好的SftpClient
類範例化到Spring IOC
容器中來管理,以便讓開發人員在整個專案中通過@Autowired
的方式就可以直接使用。
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; /** * @author zouwei * @className SftpProperties * @date: 2022/8/19 下午12:12 * @description: */ @Data @Configuration @ConfigurationProperties(prefix = "sftp.config") public class SftpProperties { // 使用者名稱 private String username; // 密碼 private String password; // 主機名 private String host; // 埠 private int port; // 金鑰 private String privateKeyFile; // 口令 private String passphrase; // 通道連結超時時間 private int timeout; // 連結池設定 private PoolConfig pool; @Data public static class PoolConfig { //最大空閒範例數,空閒超過此值將會被銷燬淘汰 private int maxIdle; // 最小空閒範例數,物件池將至少保留2個空閒物件 private int minIdle; //最大物件數量,包含借出去的和空閒的 private int maxTotal; //物件池滿了,是否阻塞獲取(false則借不到直接拋異常) private boolean blockWhenExhausted; // BlockWhenExhausted為true時生效,物件池滿了阻塞獲取超時,不設定則阻塞獲取不超時,也可在borrowObject方法傳遞第二個引數指定本次的超時時間 private long maxWaitMillis; // 建立物件後是否驗證物件,呼叫objectFactory#validateObject private boolean testOnCreate; // 借用物件後是否驗證物件 validateObject private boolean testOnBorrow; // 歸還物件後是否驗證物件 validateObject private boolean testOnReturn; // 定時檢查期間是否驗證物件 validateObject private boolean testWhileIdle; //定時檢查淘汰多餘的物件, 啟用單獨的執行緒處理 private long timeBetweenEvictionRunsMillis; //jmx監控,和springboot自帶的jmx衝突,可以選擇關閉此設定或關閉springboot的jmx設定 private boolean jmxEnabled; } }
import com.example.awesomespring.exception.AwesomeException; import com.example.awesomespring.sftp.SftpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author zouwei * @className SftpConfig * @date: 2022/8/19 下午12:12 * @description: */ @Configuration public class SftpConfig { @Autowired private SftpProperties properties; // 建立SftpClient物件 @Bean(destroyMethod = "close") @ConditionalOnProperty(prefix = "sftp.config") public SftpClient sftpClient() throws AwesomeException { return new SftpClient(properties); } }
通過以上程式碼,我們就可以在專案的任何地方直接使用SFTP使用者端來上傳和下載檔案了。
更多關於SpringBoot SFTP檔案上傳下載的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45