背景
开发一个基于jsch的sftp工具类,方便在以后的项目中使用。写代码的过程记录下来,作为备忘录。。。
Maven依赖
- springboot依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
</parent>
- jsch依赖
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
- 完整依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>jsch-sftp</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
</parent>
<dependencies>
<!-- web应用相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<!-- jsch SFTP -->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
项目结构
Sftp连接工具类
- JschSftpRun: 项目启动类
package cn.com.soulfox;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
/**
* 启动类
*
* @author xxxx
* @create 2024/7/5 11:50
*/
@SpringBootApplication
public class JschSftpRun {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(JschSftpRun.class, args);
}
}
- JschSftpConneciton: 创建sftp连接工具类
用于创建sftp连接,类中提供了创建sftp连接的方法,另外还提供了在创建sftp连接失败后,重新尝试创建连接的方法
package cn.com.soulfox.jsch;
import com.jcraft.jsch.*;
import lombok.extern.slf4j.Slf4j;
/**
* @author xxxx
* @create 2024/7/4 12:15
*/
@Slf4j
public class JschSftpConneciton {
private static final String SFTP_PROTOCOL = "sftp";
private static final Integer DEFAULT_RETRY_COUNT_5 = 5;
private static final Long SLEEP_ITME_1000 = 1000L;//每次重试的时间间隔
private String host;
private Integer port;
private String username;
private String password;
private Integer sessionTimeout;
private Integer connectionTimeout;
public JschSftpConneciton setHost(String host) {
this.host = host;
return this;
}
public JschSftpConneciton setPort(Integer port) {
this.port = port;
return this;
}
public JschSftpConneciton setUsername(String username) {
this.username = username;
return this;
}
public JschSftpConneciton setPassword(String password) {
this.password = password;
return this;
}
public JschSftpConneciton setSessionTimeout(Integer sessionTimeout) {
this.sessionTimeout = sessionTimeout;
return this;
}
public JschSftpConneciton setConnectionTimeout(Integer connectionTimeout) {
this.connectionTimeout = connectionTimeout;
return this;
}
/**
* 返回SftpWrapper对象,是为了方便释放Session,Channel,ChannelSftp资源
* SFTP连接服务器
*/
public SftpWrapper connect() throws Exception {
JSch jsch = new JSch();
Session session = null;
Channel channel = null;
ChannelSftp sftp = null;
try {
session = jsch.getSession(username, host, port);
if (session == null) {
throw new JSchException("create session error");
}
//设置登陆主机的密码
session.setPassword(password);
//设置第一次登陆的时候提示,可选值:(ask | yes | no)
session.setConfig("StrictHostKeyChecking", "no");
//设置登陆超时时间
session.connect(sessionTimeout);
session.sendKeepAliveMsg();
//创建sftp通信通道
channel = session.openChannel(SFTP_PROTOCOL);
channel.connect(connectionTimeout);
sftp = (ChannelSftp) channel;
return new SftpWrapper(session, channel, sftp);
}catch (JSchException e){
log.error("SFTP连接异常:", e);
if (sftp != null ) {
sftp.disconnect();
}
if (channel != null ) {
channel.disconnect();
}
if (session != null ) {
session.disconnect();
}
throw e;
} catch (Exception e1) {
log.error("SFTP连接异常:", e1);
if (sftp != null ) {
sftp.disconnect();
}
if (channel != null ) {
channel.disconnect();
}
if (session != null ) {
session.disconnect();
}
throw e1;
}
}
/**
* 获取sftp连接,获取连接失败时会重试,
* 默认重试次数:5次
* @return
* @throws Exception
*/
public SftpWrapper connectWithRetry() throws Exception {
return connectWithRetry(DEFAULT_RETRY_COUNT_5);
}
public SftpWrapper connectWithRetry(int retryCount) throws Exception {
//最大重试次数
int maxRetryCount = DEFAULT_RETRY_COUNT_5;
if(retryCount > 0){
maxRetryCount = retryCount;
}
int retry = 0;
Throwable t = null;
do {
try {
SftpWrapper channelSftpWrapper = this.connect();
if(channelSftpWrapper != null){
t = null;
return channelSftpWrapper;
}
} catch (Exception e) {
t = e;
}
try {
Thread.sleep(SLEEP_ITME_1000);//休息1秒
} catch (InterruptedException e) {
}
}while (retry++ < maxRetryCount);
if(t != null){
throw new Exception(t);
}
return null;
}
}
- SftpWrapper: sftp连接对象包装类,属性包括Session,Channel,ChannelSftp
执行sftp操作有ChannelSftp就可以了,使用Sftp包装类,是为了方便关闭资源
package cn.com.soulfox.jsch;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.Session;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* SftpWrapper类简单包装ChannelSftl对象,方便关闭资源
* @author xxxx
* @create 2024/7/4 14:26
*/
@AllArgsConstructor//生成全字段,构造方法
@Data //生成 getter,setter方法
public class SftpWrapper {
private Session session = null;
private Channel channel = null;
private ChannelSftp sftp = null;
/**
* 关闭资源
*/
public void disconnect() {
if (sftp != null && sftp.isConnected()) {
sftp.disconnect();
}
if (channel != null && channel.isConnected()) {
channel.disconnect();
}
if (session != null && session.isConnected()) {
session.disconnect();
}
}
}
- JschSftpConfig: jsch sftp配置类
初始化 jsch sftp
package cn.com.soulfox.config;
import cn.com.soulfox.jsch.JschSftpConneciton;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
/**
* @author xxxx
* @create 2024/7/4 12:12
*/
@Configuration
public class JschSftpConfig {
@Value("${jsch.sftp.host}")
private String host;
@Value("${jsch.sftp.port:22}")
private Integer port;
@Value("${jsch.sftp.username}")
private String username;
@Value("${jsch.sftp.password}")
private String password;
@Value("${jsch.sftp.session-timeout:60000}")
private Integer sessionTimeout;//单位毫秒
@Value("${jsch.sftp.connect-timeout:5000}")
private Integer connectTimeout;//单位毫秒
@Bean
@Lazy
public JschSftpConneciton jschSftpConneciton(){
return new JschSftpConneciton()
.setHost(host)
.setPort(port)
.setUsername(username)
.setPassword(password)
.setSessionTimeout(sessionTimeout)
.setConnectionTimeout(connectTimeout);
}
}
sftp连接信息配置在文件application.yml里
jsch:
sftp:
host: 172.168.xxx.xx
port: 1221 #默认22,1221公司自己定义的,这里要配置正确
username: xxxx #远程服务器用户名
password: xxxx #远程服务器密码
SftpUitl 工具类,提供下载,上传文件功能
package cn.com.soulfox.jsch;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.SftpException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.*;
/**
* @author xxxx
* @create 2024/7/4 14:28
*/
@Component
@Slf4j
public class JschSftpUtil {
private static JschSftpConneciton jschSftpConneciton;
@Autowired
public void setJschSftpConneciton(JschSftpConneciton jschSftpConneciton) {
JschSftpUtil.jschSftpConneciton = jschSftpConneciton;
}
/**
* 下载文件
* @param remotePath 远程目录
* @param downloadFileName 待下载的远程文件名称
* @param localSavePath 下载文件保存的本地目录
*/
public static void downloadFile(String remotePath, String downloadFileName, String localSavePath) {
SftpWrapper sftpWrapper = null;
try {
//sftp连接对象
sftpWrapper = jschSftpConneciton.connectWithRetry();
//进入远程服务器指定的目录
sftpWrapper.getSftp().cd(remotePath);
if (!checkLocalPath(localSavePath)) {
log.info("本地目录[{}]不存在,且新建失败+++++", localSavePath);
return;
}
String localFile = checkPathEnd(localSavePath) + downloadFileName;
File outFile = new File(localFile);
sftpWrapper.getSftp().get(downloadFileName, new FileOutputStream(outFile));
log.info("从远程目录[{}]下载文件[{}]到本地目录[{}]成功", remotePath, downloadFileName, localSavePath);
} catch (SftpException e) {
log.info("从远程目录[{}]下载文件[{}]到本地目录[{}]失败", remotePath, downloadFileName, localSavePath);
log.error("下载文件失败: ", e);
} catch (Exception e) {
log.info("从远程目录[{}]下载文件[{}]到本地目录[{}]失败", remotePath, downloadFileName, localSavePath);
log.error("下载文件失败: ", e);
} finally {
if (sftpWrapper != null) {
sftpWrapper.disconnect();
}
}
}
/**
*
* @param localDir 保存上传文件的本地目录
* @param uploadFileName 上传文件名称
* @param remoteSaveDir 保存上传文件的远程目录, 建议使用绝对路径
* 如果使用相对路径,建议基准目录使用sfpt登录后所在的目录
* 这个目录,使用channelSftp.goHome()可以获取
*/
public static void uploadFile(String localDir, String uploadFileName, String remoteSaveDir) {
String uploadFilePath = checkPathEnd(localDir) + uploadFileName;
File uploadFile = new File(uploadFilePath);
uploadFile(uploadFile, remoteSaveDir);
}
/**
* 上传文件
* @param uploadFilePath 本地文件的路径
* @param remoteSaveDir 保存上传文件的远程目录, 建议使用绝对路径
* 如果使用相对路径,建议基准目录使用sfpt登录后所在的目录
* 这个目录,使用channelSftp.goHome()可以获取
*/
public static void uploadFile(String uploadFilePath, String remoteSaveDir) {
File uploadFile = new File(uploadFilePath);
uploadFile(uploadFile, remoteSaveDir);
}
/**
* 上传文件
* @param uploadFile 上传文件的File对象
* @param remoteSavePath 保存上传文件的远程目录, 建议使用绝对路径
* 如果使用相对路径,建议基准目录使用sfpt登录后所在的目录
* 这个目录,使用channelSftp.goHome()可以获取
*/
public static void uploadFile(File uploadFile, String remoteSavePath) {
if(uploadFile == null ){
log.info("本地文件对象不存在++++");
return;
}
if(!uploadFile.exists()){
log.info("本地文件[{}]不存在", uploadFile.getAbsoluteFile());
return;
}
InputStream is = null;
try {
is = new FileInputStream(uploadFile);
} catch (FileNotFoundException e) {
log.info("获取本地文件[{}]的文件流失败", uploadFile.getAbsoluteFile());
log.error("获取文件流失败: ", e);
if(is != null){
try {
is.close();
} catch (IOException ioException) {
}
}
return;
}
SftpWrapper sftpWrapper = null;
try {
//sftp连接对象
sftpWrapper = jschSftpConneciton.connectWithRetry();
//检查远程目录,不存在则创建
createLinuxRemoteDirs(sftpWrapper.getSftp(), remoteSavePath);
//进入用户home目录,
sftpWrapper.getSftp().cd(sftpWrapper.getSftp().getHome());
//保证当前目录在上传文件保存的目录
sftpWrapper.getSftp().cd(remoteSavePath);
//上传文件
sftpWrapper.getSftp().put(is, uploadFile.getName());
} catch (SftpException e) {
log.info("上传本地文件[{}]到远程目录[{}]失败", uploadFile.getAbsoluteFile(), remoteSavePath);
log.error("上传本地文件失败: ", e);
} catch (Exception e) {
log.info("上传本地文件[{}]到远程目录[{}]失败", uploadFile.getAbsoluteFile(), remoteSavePath);
log.error("上传本地文件失败: ", e);
} finally {
if (sftpWrapper != null) {
sftpWrapper.disconnect();
}
if(is != null){
try {
is.close();
} catch (IOException e) {
}
}
}
}
/**
* 检查目录结是否以目录分隔符结尾
* 如果不是,则加上目录分隔符结尾
*
* @param localPath 本地目录
* @return
*/
private static String checkPathEnd(String localPath) {
if (localPath.endsWith("/") || localPath.endsWith("\\")) {
return localPath;
}
return localPath + File.separator;
}
/**
* 检查本地目录是否存在,不存在就创建。
* 为了防止其他线程已创建目录,加上了重试代码
*
* @param localPath 本地目录
* @return
*/
private static boolean checkLocalPath(String localPath) {
int maxRetryCount = 5;
int retry = 0;
do {
File localDir = new File(localPath);
if (localDir.exists()) {
return true;
}
boolean result = localDir.mkdirs();
if (result) {
return true;
}
try {
Thread.sleep(1000L);//休息1秒
} catch (InterruptedException e) {
}
} while (retry++ < maxRetryCount);
return false;
}
/**
* 检查和创建[ linux系统 ]的远程多级目录,
* 外部调用, 单纯的创建远程目录,不作其他操作
* @param remoteDir 远程目录
* @throws SftpException
*/
public static void createLinuxRemoteDirs( String remoteDir) throws SftpException {
SftpWrapper sftpWrapper = null;
try {
sftpWrapper = jschSftpConneciton.connectWithRetry();
createLinuxRemoteDirs(sftpWrapper.getSftp(), remoteDir);
} catch (SftpException e) {
log.info("创建Linux远程目录[{}]失败", remoteDir);
log.error("创建Linux远程目录失败: ", e);
} catch (Exception e) {
log.info("创建Linux远程目录[{}]失败", remoteDir);
log.error("创建Linux远程目录失败: ", e);
} finally {
if (sftpWrapper != null) {
sftpWrapper.disconnect();
}
}
}
/**
* 检查和创建[ linux系统 ]的远程多级目录,
* linux系统目录分隔符是 “/”
* 内部上传文件的方法调用
*
* @param sftpChannel
* @param remoteDir 远程目录
* @throws SftpException
*/
public static void createLinuxRemoteDirs(ChannelSftp sftpChannel, String remoteDir) throws SftpException {
if(remoteDir == null || "".equals(remoteDir)){
log.info("待创建的远程目录为空++++++++");
return;
}
String[] dirs = remoteDir.split("/");;
if(dirs == null || dirs.length == 0){
log.info("拆分目录[{}]失败,没有获取到目录数组", remoteDir);
return;
}
//进入用户home目录,保证初始目录正确
sftpChannel.cd(sftpChannel.getHome());
if( dirs.length == 1){
//只有一层目录,直接处理
try {
sftpChannel.cd(dirs[0]);
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
log.info("开始创建远程目录[{}]", dirs[0]);
sftpChannel.mkdir(dirs[0]);
} else {
throw e;
}
}
return;
}
StringBuilder sb = new StringBuilder();
//处理第一个元素
if(remoteDir.startsWith(".")){
//相对路径,把缺少的路径补上
sb.append(sftpChannel.getHome()).append("/");
}else if(remoteDir.startsWith("/")){
//绝对路径,把"/"放到目录开头
sb.append("/");
}else {
//既不是”/“开头,也不是”.“开头
//属于相对路径的一种情况
try {
//先处理第一层目录
sftpChannel.cd(dirs[0]);
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
log.info("开始创建远程目录[{}]", dirs[0]);
sftpChannel.mkdir(dirs[0]);
} else {
throw e;
}
}
//把已处理的目录加上
sb.append(sftpChannel.getHome()).append("/").append(dirs[0]).append("/");
}
//从第二个元素开始创建不存在的目录
for (int i = 1; i < dirs.length; i++) {
String dir = dirs[i];
if (dir.isEmpty() ) {
//跳过空字符串
continue;
}
sb.append(dir + "/");
try {
sftpChannel.cd(sb.toString());
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
log.info("开始创建远程目录[{}]", sb.toString());
sftpChannel.mkdir(sb.toString());
} else {
throw e;
}
}
}
// for (String dir : dirs) {
// if (dir.isEmpty() || dir.contains(".")) {
// //跳过空字符串,和"."字符串
// continue;
// }
// sb.append(dir + "/");
// try {
// sftpChannel.cd(sb.toString());
// } catch (SftpException e) {
// if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
// sftpChannel.mkdir(sb.toString());
// } else {
// throw e;
// }
// }
// }
}
}
单元测试
- JschSftpConfigTest: 测试类
package cn.com.soulfox.config;
import cn.com.soulfox.JschSftpRun;
import cn.com.soulfox.jsch.JschSftpConneciton;
import cn.com.soulfox.jsch.JschSftpUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @author xxxx
* @create 2024/7/5 12:57
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = JschSftpRun.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class JschSftpConfigTest {
@Autowired
private JschSftpConneciton sftpConneciton;
/**
* 下载文件
*/
@Test
public void testDownload(){
//从远程服务器上的下载 /home/jiyh/ 目录下的 testabc.txt
//下载文件保存到 d:\jiyh
String remotePath = "/home/jiyh/";
String downloadFileName = "testabc.txt";
String localSavePath = "d:\\jiyh";
JschSftpUtil.downloadFile(remotePath, downloadFileName, localSavePath);
}
/**
* 上传文件
*/
@Test
public void testUpload(){
//上传传本地 d:\jiyh 目录下的 test123.txt文件
//到远程 /home/jiyh/test/test 目录
//目录不存在,会自动创建
JschSftpUtil.uploadFile("d:\\jiyh", "test123.txt", "/home/jiyh/test/test");
}
}
-
在远程服务器准备测试文件 文件内容随便,这里我准备的内容为“112dadfdefee”
-
测试下载功能
测试结果:
-
测试上传功能 :在本地 d:/jiyh 目录准备测试文件“test123.txt”,内容为“dfdfdfdfdaerwrt”
测试结果