最近工作中用到了JSch去操作SFTP文件的上传和下载,本文记录一下封装的一个工具类,以及理论遇到的两个问题。
SFTP(Secure File Transfer Protocol,平安文件传送协定)个别指SSH文件传输协定(SSH File Transfer Protocol),应用加密传输认证信息和数据,所以绝对于FTP,SFTP会十分平安但传输效率要低得多。
JSch(Java Secure Channel)是一个SSH2的纯Java实现,它容许你连贯到一个SSH服务器,并且能够应用端口转发,X11转发,文件传输等。
SFTP工具类
pom.xml文件增加相干包依赖
<dependency> <groupId>com.jcraft</groupId> <artifactId>jsch</artifactId> <version>0.1.55</version></dependency>
SFTP工具类,提供文件上传和下载性能
public class SftpClient { public boolean downloadFile(SftpConfig sftpConfig, SftpDownloadRequest request) { Session session = null; ChannelSftp channelSftp = null; try { session = getSession(sftpConfig); channelSftp = getChannelSftp(session); String remoteFileDir = getRemoteFileDir(request.getRemoteFilePath()); String remoteFileName = getRemoteFileName(request.getRemoteFilePath()); // 校验SFTP上文件是否存在 if (!isFileExist(channelSftp, remoteFileDir, remoteFileName, request.getEndFlag())) { return false; } // 切换到SFTP文件目录 channelSftp.cd(remoteFileDir); // 下载文件 File localFile = new File(request.getLocalFilePath()); FileUtils.createDirIfNotExist(localFile); FileUtils.deleteQuietly(localFile); channelSftp.get(remoteFileName, request.getLocalFilePath()); return true; } catch (JSchException jSchException) { throw new RuntimeException("sftp connect failed:" + JsonUtils.toJson(sftpConfig), jSchException); } catch (SftpException sftpException) { throw new RuntimeException("sftp download file failed:" + JsonUtils.toJson(request), sftpException); } finally { disconnect(channelSftp, session); } } public void uploadFile(SftpConfig sftpConfig, SftpUploadRequest request) { Session session = null; ChannelSftp channelSftp = null; try { session = getSession(sftpConfig); channelSftp = getChannelSftp(session); String remoteFileDir = getRemoteFileDir(request.getRemoteFilePath()); String remoteFileName = getRemoteFileName(request.getRemoteFilePath()); // 切换到SFTP文件目录 cdOrMkdir(channelSftp, remoteFileDir); // 上传文件 channelSftp.put(request.getLocalFilePath(), remoteFileName); if (StringUtils.isNoneBlank(request.getEndFlag())) { channelSftp.put(request.getLocalFilePath() + request.getEndFlag(), remoteFileName + request.getEndFlag()); } } catch (JSchException jSchException) { throw new RuntimeException("sftp connect failed: " + JsonUtils.toJson(sftpConfig), jSchException); } catch (SftpException sftpException) { throw new RuntimeException("sftp upload file failed: " + JsonUtils.toJson(request), sftpException); } finally { disconnect(channelSftp, session); } } private Session getSession(SftpConfig sftpConfig) throws JSchException { Session session; JSch jsch = new JSch(); if (StringUtils.isNoneBlank(sftpConfig.getIdentity())) { jsch.addIdentity(sftpConfig.getIdentity()); } if (sftpConfig.getPort() <= 0) { // 默认端口 session = jsch.getSession(sftpConfig.getUser(), sftpConfig.getHost()); } else { // 指定端口 session = jsch.getSession(sftpConfig.getUser(), sftpConfig.getHost(), sftpConfig.getPort()); } if (StringUtils.isNoneBlank(sftpConfig.getPassword())) { session.setPassword(sftpConfig.getPassword()); } session.setConfig("StrictHostKeyChecking", "no"); session.setTimeout(10 * 1000); // 设置超时工夫10s session.connect(); return session; } private ChannelSftp getChannelSftp(Session session) throws JSchException { ChannelSftp channelSftp = (ChannelSftp) session.openChannel("sftp"); channelSftp.connect(); return channelSftp; } /** * SFTP文件是否存在 * true:存在;false:不存在 */ private boolean isFileExist(ChannelSftp channelSftp, String fileDir, String fileName, String endFlag) throws SftpException { if (StringUtils.isNoneBlank(endFlag)) { if (!isFileExist(channelSftp, fileDir, fileName + endFlag)) { return false; } } else { if (!isFileExist(channelSftp, fileDir, fileName)) { return false; } } return true; } /** * SFTP文件是否存在 * true:存在;false:不存在 */ private boolean isFileExist(ChannelSftp channelSftp, String fileDir, String fileName) throws SftpException { if (!isDirExist(channelSftp, fileDir)) { return false; } Vector vector = channelSftp.ls(fileDir); for (int i = 0; i < vector.size(); ++i) { ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) vector.get(i); if (fileName.equals(entry.getFilename())) { return true; } } return false; } /** * sftp上目录是否存在 * true:存在;false:不存在 */ private boolean isDirExist(ChannelSftp channelSftp, String fileDir) { try { SftpATTRS sftpATTRS = channelSftp.lstat(fileDir); return sftpATTRS.isDir(); } catch (SftpException e) { return false; } } private void cdOrMkdir(ChannelSftp channelSftp, String fileDir) throws SftpException { if (StringUtils.isBlank(fileDir)) { return; } for (String dirName : fileDir.split(File.separator)) { if (StringUtils.isBlank(dirName)) { dirName = File.separator; } if (!isDirExist(channelSftp, dirName)) { channelSftp.mkdir(dirName); } channelSftp.cd(dirName); } } private String getRemoteFileDir(String remoteFilePath) { int remoteFileNameindex = remoteFilePath.lastIndexOf(File.separator); return remoteFileNameindex == -1 ? "" : remoteFilePath.substring(0, remoteFileNameindex); } private String getRemoteFileName(String remoteFilePath) { int remoteFileNameindex = remoteFilePath.lastIndexOf(File.separator); if (remoteFileNameindex == -1) { return remoteFilePath; } String remoteFileName = remoteFileNameindex == -1 ? remoteFilePath : remoteFilePath.substring(remoteFileNameindex + 1); if (StringUtils.isBlank(remoteFileName)) { throw new RuntimeException("remoteFileName is blank"); } return remoteFileName; } private void disconnect(ChannelSftp channelSftp, Session session) { if (channelSftp != null) { channelSftp.disconnect(); } if (session != null) { session.disconnect(); } }}
SFTP连贯配置
public class SftpConfig { /** * sftp 服务器地址 */ private String host; /** * sftp 服务器端口 */ private int port; /** * sftp服务器登陆用户名 */ private String user; /** * sftp 服务器登陆密码 * 明码和私钥二选一 */ private String password; /** * 私钥文件 * 私钥和明码二选一 */ private String identity;}
文件上传申请
public class SftpUploadRequest { /** * 本地残缺文件名 */ private String localFilePath; /** * sftp上残缺文件名 */ private String remoteFilePath; /** * 文件实现标识 * 非必选 */ private String endFlag;}
文件下载申请
public class SftpDownloadRequest { /** * sftp上残缺文件名 */ private String remoteFilePath; /** * 本地残缺文件名 */ private String localFilePath; /** * 文件实现标识 * 非必选 */ private String endFlag;}
SftpException: Failure
多个工作同时上传文件时,局部工作会上传失败,报错信息如下:
Caused by: com.jcraft.jsch.SftpException: Failure at com.jcraft.jsch.ChannelSftp.throwStatusError(ChannelSftp.java:2873) ~[jsch-0.1.55.jar!/:?] at com.jcraft.jsch.ChannelSftp.mkdir(ChannelSftp.java:2182) ~[jsch-0.1.55.jar!/:?]
网上搜了下( https://winscp.net/eng/docs/s... ),呈现Failure谬误有以下几种可能:
- 重命名文件时存在同名文件;
- 创立了一个曾经存在的文件夹;
- 磁盘满了;
从报错信息的第三行能够看出,应该是命中了第二种可能:创立了一个曾经存在的文件夹。
看一下下面SftpClient类的cdOrMkdir函数的逻辑,当目录存在时,进入到该目录;否则会创立该目录。SFTP上传文件的门路为:bizType/{yyyyMMdd}/{dataLabel}/biz.txt,不同工作的dataLabel值不一样,这里会有并发问题:
- A工作判断bizType/20210101目录不存在;
- B工作判断bizType/20210101目录不存在;
- A工作创立bizType/20210101目录;
- B工作创立bizType/20210101目录时,因该目录已被A工作创立,所以报错;
解决方案:将SFTP上传文件的门路改为 bizType/{dataLabel}/{yyyyMMdd}/biz.txt,使得不同工作的文件门路不再抵触。
JSchException
多个工作同时下载文件时,局部工作会下载失败,报错信息如下:
Caused by: com.jcraft.jsch.JSchException: channel is not opened. at com.jcraft.jsch.Channel.sendChannelOpen(Channel.java:765) ~[jsch-0.1.55.jar!/:?] at com.jcraft.jsch.Channel.connect(Channel.java:151) ~[jsch-0.1.55.jar!/:?]
一开始狐疑还是并发问题,网上搜了下,可能是零碎SSH终端连接数配置过小,该参数在/etc/ssh/sshd_config中配置,因权限问题(须要root权限)去找OP沟通时,OP感觉不应该是这个起因,于是从新看了下报错处代码:
protected void sendChannelOpen() throws Exception { Session _session = getSession(); if (!_session.isConnected()) { throw new JSchException("session is down"); } Packet packet = genChannelOpenPacket(); _session.write(packet); int retry = 2000; long start = System.currentTimeMillis(); long timeout = connectTimeout; if (timeout != 0L) retry = 1; synchronized (this) { while (this.getRecipient() == -1 && _session.isConnected() && retry > 0) { if (timeout > 0L) { if ((System.currentTimeMillis() - start) > timeout) { retry = 0; continue; } } try { long t = timeout == 0L ? 10L : timeout; this.notifyme = 1; wait(t); } catch (java.lang.InterruptedException e) { } finally { this.notifyme = 0; } retry--; } } if (!_session.isConnected()) { throw new JSchException("session is down"); } if (this.getRecipient() == -1) { // timeout throw new JSchException("channel is not opened."); } if (this.open_confirmation == false) { // SSH_MSG_CHANNEL_OPEN_FAILURE throw new JSchException("channel is not opened."); } connected = true; }
从第38~39行可看出谬误起因是超时了,原来是一开始设置的超时工夫太短:
channelSftp.connect(1000); // 设置超时工夫1s
解决方案:将超时工夫改大,或者应用默认值。
channelSftp.connect();