最近工作中用到了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值不一样,这里会有并发问题:

  1. A工作判断bizType/20210101目录不存在;
  2. B工作判断bizType/20210101目录不存在;
  3. A工作创立bizType/20210101目录;
  4. 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();