最近工作中用到了 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();