
最近用到了JSch去操作SFTP文件的上传和下载,本文记录一下封装的一个工具类,以及实际遇到的两个问题。
SFTP(Secure File Transfer Protocol,安全文件传送协议)一般指SSH文件传输协议(SSH File Transfer Protocol),使用加密传输认证信息和数据,所以相对于FTP,SFTP会非常安全但传输效率要低得多。
JSch(Java Secure Channel,http://www.jcraft.com/jsch/)是一个SSH2的纯Java实现,它允许你连接到一个SSH服务器,并且可以使用端口转发,X11转发,文件传输等。

1. 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); // 设置超时时间10ssession.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;}

2. SftpException: Failure
多个任务同时上传文件时,部分任务会上传失败,报错信息如下:
Caused by: com.jcraft.jsch.SftpException: Failureat 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/sftp_codes#code_4),出现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,使得不同任务的文件路径不再冲突。

3. 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) { // timeoutthrow new JSchException("channel is not opened.");}if (this.open_confirmation == false) { // SSH_MSG_CHANNEL_OPEN_FAILUREthrow new JSchException("channel is not opened.");}connected = true;}
从第38~39行可看出错误原因是超时了,原来是一开始设置的超时时间太短:
channelSftp.connect(1000); // 设置超时时间1s
解决方案:将超时时间改大,或者使用默认值。
channelSftp.connect();

End





