# sftp-spring-boot-starter
**Repository Path**: javacoo/sftp-spring-boot-starter
## Basic Information
- **Project Name**: sftp-spring-boot-starter
- **Description**: 基于jsch简单封装
- **Primary Language**: Unknown
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 2
- **Created**: 2020-05-22
- **Last Updated**: 2024-10-09
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# sftp-spring-boot-starter
#### 背景问题
当时由于需要接入多家渠道SFTP服务器,推送文件,而历史代码中存在以下一些问题:
1. SFTP配置未统一集中管理,配置硬编码。
2. SFTP相关操作代码散落在业务代码中,且实现各异,问题排查,维护困难。
3. 在传送多个文件时效率较低(每个文件传送完成后都要关闭连接),且容易出错(频繁的关闭连接,打开连接,容易造成拿到未完成的连接,导致操作异常)
#### 设计思路
针对上述问题,约定统一的渠道配置规则,即渠道->渠道配置的形式集中管理SFTP配置。
基于JCraft,结合渠道配置封装统一的sftp访问模板接口。
基于commons-pool2提供的池化功能,将sftp访问模板接口池化。
基于springboot starter机制,实现SFTP组件。
#### 知识准备
##### 一:JCraft
jcraft 是一个基于 Java 实现的 SSH 协议库,提供了一系列的类和方法来实现 SSH 客户端和服务器的功能。下面是 jcraft 的一些基本概念:
```
JSch:JSch 是 jcraft 中最重要的一个类,它是 SSH 协议的 Java 实现。使用 JSch 可以创建 SSH 客户端和服务器,进行连接和通信。
Session:Session 是一个远程 SSH 会话,它负责建立和维持与远程 SSH 服务器的连接。
Channel:Channel 是一个数据通道,用于在 SSH 会话中传输数据。可以通过创建不同类型的 Channel 来实现不同的功能,如执行远程命令、传输文件等。
UserInfo:UserInfo 是一个接口,用于处理用户身份验证。可以实现 UserInfo 接口来自定义身份验证逻辑。
```
##### 二:commons-pool2
**Apache Commons Pool2**是一个开源的Java库,用于实现和管理对象池。对象池是一种常见的设计模式,通过复用来分摊昂贵对象的创建和销毁代价,从而优化资源利用和提高应用程序性能。Commons Pool2提供了一套用于实现对象池化的API和灵活的配置选项,内置了多种各具特色的对象池实现,如GenericObjectPool,它实现了一个功能强大的对象池,可以方便地进行配置和扩展。通过使用Commons Pool2,开发者可以更加轻松地实现和管理对象池,提高应用程序的性能和可靠性。
使用Commons Pool2的基本步骤包括:
1. **创建池工厂类**:这涉及到定义PooledObjectFactory接口的实现,用于创建、激活、验证和销毁池中的对象。
2. **配置对象池**:通过设置最大池化对象数、最大空闲时间、最小空闲数等参数,根据不同的应用场景进行灵活配置。
3. **对象的取用和回收**:使用BorrowObject方法从池中借出对象,使用完成后,使用ReturnObject方法将对象返还给池。
4. **对象的激活和钝化**:当对象从池中借出时,它被激活;当对象被返还时,它被钝化。Commons Pool2提供了相应的接口和方法来处理这些状态转换。
此外,Commons Pool2还提供了invalidateObject方法来废弃一个对象,通常在对象发生了异常或其他问题时使用此方法废弃它。
在实际应用中,Commons Pool2被广泛应用于各种数据库连接池、线程池以及请求分发池中,通过复用对象来优化资源利用和提高性能。通过引入相关的依赖并按照上述步骤进行配置和使用,开发者可以轻松地实现和管理自己的对象池
##### 三:springboot starter
SpringBoot 中的 starter 是一种非常重要的机制,能够抛弃以前在 Spring 中的繁杂配置,将其统一集成进 starter,应用者只需要在 maven 中引入 starter 依赖,SpringBoot 就能自动扫描到要加载的信息并启动相应的默认配置。starter 让我们摆脱了各种依赖库的处理以及需要配置各种信息的困扰。SpringBoot 会自动通过 classpath 路径下的类发现需要的 Bean,并注册进 IOC 容器。SpringBoot 提供了针对日常企业应用研发各种场景的 spring-boot-starter 依赖模块。所有这些依赖模块都遵循着约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。
- 自定义 starter 的作用
在我们的日常开发工作中,经常会有一些独立于业务之外的配置模块,比如对 web 请求的日志打印。我们经常将其放到一个特定的包下,然后如果另一个工程需要复用这块功能的时候,需要将代码硬拷贝到另一个工程,重新集成一遍,这样会非常麻烦。如果我们将这些可独立于业务代码之外的功配置模块封装成一个个 starter,复用的时候只需要将其在 maven pom 中引用依赖即可,让 SpringBoot 为我们完成自动装配,提高开发效率。
- 自定义 starter 的命名规则
SpringBoot提供的 starter 以 spring-boot-starter-xxx 的方式命名的。官方建议自定义的 starter 使用 xxx-spring-boot-starter 命名规则。以区分 SpringBoot 生态提供的 starter。如:mybatis-spring-boot-starter。
- 如何自定义starter
步骤
1. 新建两个模块,命名规范: xxx-spring-boot-starter
- xxx-spring-boot-autoconfigure:自动配置核心代码
- xxx-spring-boot-starter:管理依赖
- ps:如果不需要将自动配置代码和依赖项管理分离开来,则可以将它们组合到一个模块中。但SpringBoot 官方建议将两个模块分开。
2. 在 xxx-spring-boot-autoconfigure 项目中
- 引入 spring-boot-autoconfigure 的 maven 依赖
- 创建自定义的 XXXProperties 类: 这个类的属性根据需要是要出现在配置文件中的。
- 创建自定义的类,实现自定义的功能。
- 创建自定义的 XXXAutoConfiguration 类:这个类用于做自动配置时的一些逻辑,需将上方自定义类进行 Bean 对象创建,同时也要让 XXXProperties 类生效。
- 创建自定义的 spring.factories 文件:在 resources/META-INF 创建一个 spring.factories 文件和 spring-configuration-metadata.json,spring-configuration-metadata.json 文件是用于在填写配置文件时的智能提示,可要可不要,有的话提示起来更友好。spring.factories用于导入自动配置类,必须要有。
3. 在 xxx-spring-boot-starter 项目中引入 xxx-spring-boot-autoconfigure 依赖,其他项目使用该 starter 时只需要依赖 xxx-spring-boot-starter 即可。
#### 如何使用
##### 第一步:pom依赖
```
com.jccfc
sftp-spring-boot-starter
1.0.1
```
##### 第二步:参数配置
```properties
#阿里
sftp.clients.alipay.host = 127.0.0.1
sftp.clients.alipay.port = 22
sftp.clients.alipay.username = alipay
sftp.clients.alipay.password = YqK#m6a0
sftp.clients.alipay.path = /alipay/{yyyyMMdd}
sftp.clients.alipay.type = 00
sftp.clients.alipay.desc = 阿里
#小杜
sftp.clients.xiaodu.host = 127.0.0.1
sftp.clients.xiaodu.port = 22
sftp.clients.xiaodu.username = xiaodu
sftp.clients.xiaodu.password = W5eK2#%t
sftp.clients.xiaodu.path = /xiaodu/{yyyyMMdd}
sftp.clients.xiaodu.type = 00
sftp.clients.xiaodu.desc = 小杜
```
**参数说明:**
- sftp.clients.**XXXX**:XXXX 表示SFTP客户端配置key,一个渠道一个
- host:主机(必填)
- port:端口(必填)
- username:用户名(必填)
- password:密码(必填)
- path:路径(必填)
- type:类型(扩展字段,可为空)
- desc:描述(扩展字段,可为空)
##### 第三步:使用SftpHelper实现业务功能,如:
```java
/**
* 委外机构SFTP语音文件传输服务
*
* @author duanyong@jccfc.com
* @date 2024/9/24 18:24
*/
@Service
public class OutSftpService {
private static Logger logger = LoggerFactory.getLogger(OutSftpService.class);
....
/** sftpHelper帮助类*/
@Autowired
private SftpHelper sftpHelper;
/** sftp客户端配置参数*/
@Autowired
private SftpClientProperties sftpClientProperties;
....
private void handle(String businessDate,String channel, SftpClientConfig config){
logger.info("开始执行委外机构SFTP文件传输任务,委外机构渠道ID:{},业务日期:{}",channel,businessDate);
try{
Vector fileList;
String dir = "";
if(StrUtil.isNotBlank(businessDate)){
dir = formatDir(config.getPath(),businessDate);
//获取渠道下,指定目录文件集合
fileList = sftpHelper.list(channel,dir);
}else{
//获取渠道下,当前日期目录文件集合
fileList = sftpHelper.list(channel);
}
List outCallFileList = new ArrayList<>();
for (ChannelSftp.LsEntry lsEntry : fileList) {
if(lsEntry.getAttrs().isDir()){
continue;
}
String fileName = lsEntry.getFilename();
// 初始化文件对象
OutCallFile outCallFile = new OutCallFile();
//下载文件
downLoadFile(outCallFile,channel,dir,fileName);
....
}
}catch (Exception e){
...
}
}
}
...
private void downLoadFile(OutCallFile outCallFile,String channel,String dir, String fileName) {
logger.info("开始下载文件:目录:{},文件名:{}", dir,fileName);
try {
byte[] bytes;
if(StrUtil.isNotBlank(dir)){
// 下载渠道下,指定目录文件
bytes = sftpHelper.downloadBytes(channel,dir, fileName);
}else{
// 下载渠道下,当前日期目录文件
bytes = sftpHelper.downloadNow(channel, fileName);
}
.......
}catch (Exception e){
logger.error("文件下载失败,文件名:{}",fileName);
}
}
```
##### 附:SFTP操作帮助类SftpHelper相关方法定义
```java
/**
* 获取指定渠道下,默认当前日期目录下的文件名集合
* 适用于路径配置如:/dingxiang/{yyyyMMdd}
* @author duanyong@jccfc.com
* @date 2024/9/24 17:47
* @param channel 渠道ID
* @return: Vector 文件名集合
*/
public Vector list(final String channel)
/**
* 获取渠道下,指定目录下的文件名集合
*
* @author duanyong@jccfc.com
* @date 2024/9/24 17:47
* @param channel 渠道ID
* @param dir 全路径目录
* @return: Vector 文件名集合
*/
public Vector list(final String channel,final String dir)
/**
* 下载默认当前日期目录下,指定文件名的文件为byte[]
* 适用于路径配置如:/dingxiang/{yyyyMMdd}
* @author duanyong@jccfc.com
* @date 2024/9/24 17:53
* @param channel 渠道ID
* @param fileName 文件名
* @return: byte
*/
public byte[] downloadNow(final String channel,String fileName)
/**
* 下载指定目录下,指定文件名的文件为byte[]
*
* @author duanyong@jccfc.com
* @date 2024/9/24 17:53
* @param channel 渠道ID
* @param dir 全路径目录
* @param fileName 文件名
* @return: byte
*/
public byte[] downloadBytes(final String channel,final String dir,String fileName)
/**
* 文件下载
*
* @author duanyong@jccfc.com
* @date 2024/9/29 10:13
* @param channel 渠道ID
* @param filePath 文件全路径
* @return: InputStream
*/
public InputStream download(final String channel,final String filePath)
/**
* 判断文件是否存在
* 指定sftp文件路径和文件名,判断改文件是否存在当前指定的sftp目录下,若存在返回true,否则返回false
* @author duanyong@jccfc.com
* @date 2020/5/15 13:51
* @param channel:渠道ID
* @param sftpDir:sftp目录
* @param fileName:文件名
* @return: boolean
*/
public boolean exists(String channel, String sftpDir, String fileName)
/**
* 文件传输
* 上传文件到渠道下昨天日期目录下
* 适用于路径配置如:/dingxiang/{yyyyMMdd}
* @author duanyong@jccfc.com
* @date 2020/5/15 11:14
* @param channel: 渠道ID
* @param fileName: 文件名
* @param inputStream: 待上传的文件输入流
*/
public void upload(final String channel,final String fileName, final InputStream inputStream)
/**
* 文件传输
*
* @author duanyong@jccfc.com
* @date 2024/9/29 10:11
* @param channel 渠道ID
* @param dir 目录
* @param fileName 文件名
* @param inputStream
* @return: void 输入流
*/
public void upload(final String channel,final String dir,final String fileName, final InputStream inputStream)
/**
* 文件传输
* 上传文件到渠道下当前日期目录
* 适用于路径配置如:/dingxiang/{yyyyMMdd}
* @author duanyong@jccfc.com
* @date 2020/5/15 11:14
* @param channel: 渠道ID
* @param fileName: 文件名
* @param inputStream: 待上传的文件输入流
*/
public void uploadNow(final String channel,final String fileName, final InputStream inputStream)
/**
* 递归创建目录
*
* @author duanyong@jccfc.com
* @date 2024/9/29 10:06
* @param channel 渠道ID
* @param dir 全路径目录
* @return: void
*/
public void makeDirs(final String channel,final String dir)
/**
* 删除渠道下,指定目录文件
*
* @author duanyong@jccfc.com
* @date 2024/9/29 10:08
* @param channel 渠道ID
* @param dir 全路径目录
* @param fileName 文件名
* @return: void
*/
public void delete(final String channel,final String dir,final String fileName)
/**
* 判断是否目录
*
* @author duanyong@jccfc.com
* @date 2024/9/29 10:15
* @param channel 渠道ID
* @param path 路径
* @return: boolean
*/
public boolean isdir(String channel, String path)
/**
* 进入指定目录
*
* @author duanyong@jccfc.com
* @date 2024/9/29 10:16
* @param channel 渠道ID
* @param path 路径
* @return: void
*/
public void cd(String channel, String path)
/**
* 删除目录
*
* @author duanyong@jccfc.com
* @date 2024/9/29 10:59
* @param channel 渠道ID
* @param path 路径
* @return: void
*/
public void rmdir(String channel, String path)
```
------
#### 组件实现
##### 一:工程结构
```
sftp-spring-boot-starter
└── src
├── main
│ ├── java
│ │ └── com.javacoo
│ │ ├────── sftp
│ │ │ ├──────client
│ │ │ │ ├── api
│ │ │ │ │ └── client
│ │ │ │ │ └── SftpClient sftp操作接口
│ │ │ │ ├── config 参数配置
│ │ │ │ │ ├── SftpClientConfig sftpClient配置类
│ │ │ │ │ └── SftpPoolConfig sftp连接池配置
│ │ │ │ ├── exception 异常
│ │ │ │ │ ├── BaseException 自定义异常基类
│ │ │ │ │ └── SftpClientException 自定义Sftp异常
│ │ │ │ ├── template 模版
│ │ │ │ │ └── SftpTemplate Sftp操作Template
│ │ │ │ ├── util 工具包
│ │ │ │ │ └── SftpHelper sftp操作帮助类
│ │ │ │ └── internal 接口内部实现
│ │ │ │ ├── DefaultSftpClient sftp客户端接口默认实现
│ │ │ │ ├── SftpTemplateFactory SftpTemplate工厂
│ │ │ │ └── SftpTemplatePool SftpTemplate池子
│ │ │ └──────starter
│ │ │ ├── SftpClientProperties 配置类
│ │ │ └── SftpClientAutoConfiguration 自动配置类
│ └── resource
│ └── META-INF
| └── spring.factories
└── test 测试
```
##### 二:主要依赖包及版本
| 名称 | 版本 |
| ----------------------------------- |---------------|
| spring-boot-starter | 2.1.8.RELEASE |
| spring-boot-configuration-processor | 2.1.8.RELEASE |
| spring-boot-autoconfigure | 2.1.8.RELEASE |
| commons-pool2 | 2.7.0 |
| jsch | 0.1.55 |
| lombok | 1.18.6 |
##### 三:主要类结构关系图

##### 三:主要类及关键代码
组件对外提供SftpClient(Sftp客户端接口),和SftpHelper(sftp操作帮助类,提供Sftp客户端一些常用操作),
###### 1:相关配置类
- SftpClientConfig:Sftp服务器配置信息类,主要包含主机ip,端口号,用户名,密码,路径,描述等信息。
- SftpPoolConfig:sftp池配置类,该类继承GenericKeyedObjectPoolConfig,主要包含获取连接重试次数,是否检测对象有效,对象最大数量,最大空闲对象数量,最小空闲对象数量,在从对象池获取对象时是否检测对象有效,获取连接重试次数等信息。
###### 2:基于jsch实现SFTP相关操作相关类
- SftpTemplate:Sftp操作模版类,此类包装了jsch 的ChannelSftp相关常用SFTP操作方法,结合SftpClientConfig,简化了部分操作,是需要被池化的对象。
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SftpTemplate {
/** sftp通道*/
private ChannelSftp channelSftp;
/** sftp配置*/
private SftpClientConfig sftpClientConfig;
...
/**
* 下载渠道指定目录下远程文件
*
* @param dir 文件目录
* @param name 文件名
* @return 文件字节数组
*/
public byte[] download(String dir, String name) {
if (!isExist(dir)) {
throw new SftpClientException("目录{}不存在!", dir);
}
String absoluteFilePath = dir + "/" + name;
if (!isExist(absoluteFilePath)) {
throw new SftpClientException("文件{}不存在!", absoluteFilePath);
}
try {
channelSftp.cd(dir);
InputStream in = channelSftp.get(name);
return ByteUtil.inputStreamToByteArray(in);
} catch (SftpException e) {
throw new SftpClientException(e.id,"sftp下载文件出错", e);
}
}
...
/**
* 下载渠道当前日期目录下文件
*
* @param name 文件名
* @return 文件字节数组
*/
public byte[] downloadNow(String name) {
String path = StringUtil.formatDateNow(sftpClientConfig.getPath(),SftpClientConfig.REGEX);
try {
if(!isExist(path)){
mkdirs(path);
}
String absoluteFilePath = path + "/" + name;
if (!isExist(absoluteFilePath)) {
throw new SftpClientException("文件{}不存在!", absoluteFilePath);
}
channelSftp.cd(path);
InputStream in = channelSftp.get(name);
return ByteUtil.inputStreamToByteArray(in);
} catch (SftpException e) {
throw new SftpClientException(e.id,"sftp下载文件出错", e);
}
}
...
}
```
###### 3:基于commons-pool2实现池化相关类
- SftpTemplateFactory:SftpTemplate工厂,负责创建SftpTemplate并放入对象池中,该类继承BaseKeyedPooledObjectFactory(带KEY的池子),重写了create,wrap,destroyObject,validateObject方法。
```java
@Slf4j
public class SftpTemplateFactory extends BaseKeyedPooledObjectFactory {
private static final String CHANNEL_TYPE = "sftp";
private static Properties SHH_CONFIG = new Properties();
static {
SHH_CONFIG.put("StrictHostKeyChecking", "no");
}
/**
* 创建一个SftpTemplate实例
* 这个方法必须支持并发多线程调用
* @author duanyong@jccfc.com
* @date 2020/5/14 11:19
* @param config:Sftp配置信息
* @return: com.javacoo.sftp.client.template.SftpTemplate
*/
@Override
public SftpTemplate create(SftpClientConfig config) {
try {
JSch jsch = new JSch();
Session sshSession = jsch.getSession(config.getUsername(), config.getHost(), config.getPort());
sshSession.setPassword(config.getPassword());
sshSession.setConfig(SHH_CONFIG);
sshSession.setTimeout(config.getTimeout());
sshSession.connect();
ChannelSftp channel = (ChannelSftp) sshSession.openChannel(CHANNEL_TYPE);
channel.connect();
return new SftpTemplate(channel,config);
} catch (Exception e) {
log.error("客户端:{},连接sftp失败:{}",config, e.getMessage());
throw new SftpClientException("客户端:{},连接sftp失败",e, config.toString());
}
}
/**
* 用PooledObject的实例包装对象
* @author duanyong@jccfc.com
* @date 2020/5/14 11:50
* @param sftpTemplate 被包装的对象
* @return: org.apache.commons.pool2.PooledObject 对象包装器
*/
@Override
public PooledObject wrap(SftpTemplate sftpTemplate) {
return new DefaultPooledObject(sftpTemplate);
}
/**
* 销毁对象
*
* @author duanyong@jccfc.com
* @date 2020/5/14 11:52
* @param config:Sftp配置信息
* @param p:包装对象
* @return: void
*/
@Override
public void destroyObject(SftpClientConfig config,PooledObject p) throws Exception{
if (p==null) {
return;
}
SftpTemplate sftpTemplate = p.getObject();
if (sftpTemplate == null) {
return;
}
ChannelSftp channelSftp = sftpTemplate.getChannelSftp();
if (channelSftp != null) {
channelSftp.disconnect();
}
sftpTemplate.setChannelSftp(null);
sftpTemplate.setSftpClientConfig(null);
}
/**
* 检查连接是否可用
*
* @author duanyong@jccfc.com
* @date 2020/5/14 11:54
* @param config:Sftp配置信息
* @param p:包装对象
* @return: boolean
*/
@Override
public boolean validateObject(SftpClientConfig config,PooledObject p){
if (p == null) {
return false;
}
SftpTemplate sftpTemplate = p.getObject();
if (sftpTemplate == null) {
return false;
}
try {
sftpTemplate.getChannelSftp().cd("./");
return true;
} catch (SftpException e) {
return false;
}
}
}
```
- SftpTemplatePool:存放SftpTemplate的池子,是组件的核心。该类构造时根据sftp连接池配置和SftpTemplate工厂完成对GenericKeyedObjectPool的构造,提供了根据客户端配置key获取和释放SftpTemplate对象的方法。
```java
@Slf4j
@Data
public class SftpTemplatePool{
/** SftpTemplate 对象池 */
private GenericKeyedObjectPool pool;
/** sftp服务器配置 */
private Map clients;
/** sftp连接池配置 */
private SftpPoolConfig sftpPoolConfig;
public SftpTemplatePool(Map clients,SftpPoolConfig sftpPoolConfig) {
this.clients = clients;
this.sftpPoolConfig = sftpPoolConfig;
this.pool = new GenericKeyedObjectPool<>(new SftpTemplateFactory());
setConfig(sftpPoolConfig);
}
/**
* 设置config
* @author duanyong@jccfc.com
* @date 2020/9/27 16:32
* @param conf:
* @return: void
*/
private void setConfig(SftpPoolConfig conf) {
this.pool.setLifo(conf.isLifo());
....
}
/**
* 获取SftpTemplate
*
* @author duanyong@jccfc.com
* @date 2020/5/14 13:28
* @param key: 客户端配置KEY
* @return: com.javacoo.sftp.client.template.SftpTemplate
*/
public SftpTemplate getClient(String key) throws Exception {
if(!clients.containsKey(key)){
log.error("客户端:{},未配置sftp连接信息",key);
throw new SftpClientException("客户端:{},未配置sftp连接信息",key);
}
log.debug("SftpTemplatePool->getClient->{}",key);
return pool.borrowObject(clients.get(key));
}
/**
* 释放SftpTemplate
*
* @author duanyong@jccfc.com
* @date 2020/5/14 13:28
* @param key: 客户端配置KEY
* @param sftpTemplate:
* @return: void
*/
public void releaseClient(String key,SftpTemplate sftpTemplate) {
try {
pool.returnObject(clients.get(key),sftpTemplate);
log.debug("SftpTemplatePool->releaseClient->{}",key);
} catch (Exception e) {
log.error("释放SftpTemplate异常:",e);
}
}
}
```
###### 4:组件对外提供接口及帮助类
- SftpClient:Sftp客户端接口,该接口定义了包括无返回值和有返回值,根据客户端配置KEY,运行处理器的方法,和Handler,ReturnHandler两个函数式接口。
```java
/**
* 运行处理器
*
* @author duanyong@jccfc.com
* @date 2020/5/14 10:29
* @param key: 客户端配置KEY
* @param handler: 处理器
* @return: void
*/
void run(String key,Handler handler);
/**
* 运行带返回值的处理器
*
* @author duanyong@jccfc.com
* @date 2020/5/14 10:37
* @param key 客户端配置KEY
* @param handler 带返回值的处理器
* @return: T 返回值
*/
T runAndReturn(String key,ReturnHandler handler);
@FunctionalInterface
interface Handler {
/**
*
* @author duanyong@jccfc.com
* @date 2020/5/14 10:23
* @param template:操作模板类型
* @return: void
*/
void doHandle(SftpTemplate template) throws Exception;
}
@FunctionalInterface
interface ReturnHandler {
/**
*
* @author duanyong@jccfc.com
* @date 2020/5/14 10:23
* @param template:操作模板类型
* @return: void
*/
T doHandle(SftpTemplate template) throws Exception;
}
```
- DefaultSftpClient:Sftp客户端接口默认实现类,该类集成了SftpTemplatePool,实现了根据客户端配置KEY,从SftpTemplatePool中获取SftpTemplate对象,并传入函数式接口中,完成具体操作。
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
@Builder
public class DefaultSftpClient implements SftpClient {
/** SftpTemplate连接池*/
private SftpTemplatePool sftpTemplatePool;
/**
* 从sftp连接池获取连接并执行操作
*
* @author duanyong@jccfc.com
* @date 2020/5/14 10:29
* @param key: 客户端配置KEY
* @param handler: 处理器
* @return: void
*/
@Override
public void run(String key,SftpClient.Handler handler) {
SftpTemplate sftpTemplate = getClient(key).orElseThrow(()->new SftpClientException("客户端:{},获取sftp连接出错",key));
try {
handler.doHandle(sftpTemplate);
}catch (Exception e) {
log.error("客户端:{},异常:{}",key, e.getMessage());
throw new SftpClientException("客户端:{},sftp发生异常", e,key);
} finally {
if (sftpTemplate != null) {
sftpTemplatePool.releaseClient(key,sftpTemplate);
}
}
}
/**
* 从sftp连接池获取连接并执行操作
*
* @author duanyong@jccfc.com
* @date 2020/5/14 10:29
* @param key: 客户端配置KEY
* @param handler: 处理器
* @return: T
*/
@Override
public T runAndReturn(String key, ReturnHandler handler) {
SftpTemplate sftpTemplate = getClient(key).orElseThrow(()->new SftpClientException("客户端:{},获取sftp连接出错",key));
try {
return handler.doHandle(sftpTemplate);
} catch (Exception e) {
log.error("客户端:{},异常:{}",key, e.getMessage());
throw new SftpClientException("客户端:{},sftp发生异常", e,key);
} finally {
if (sftpTemplate != null) {
sftpTemplatePool.releaseClient(key,sftpTemplate);
}
}
}
/**
* 获取SftpTemplate
*
* @author duanyong@jccfc.com
* @date 2020/6/9 10:26
* @param key: 渠道key
* @return: com.javacoo.sftp.client.template.SftpTemplate
*/
private Optional getClient(String key){
SftpTemplate client = null;
//重试次数
AtomicInteger retryCount = new AtomicInteger(this.sftpTemplatePool.getSftpPoolConfig().getRetryCount());
while (client == null && retryCount.decrementAndGet() >= 0) {
try {
client = sftpTemplatePool.getClient(key);
} catch (Exception e) {
log.error("线程->{},从对象池中获client异常,还可以重试:{}次",Thread.currentThread().getName(),retryCount.get(),e);
}
}
return Optional.ofNullable(client);
}
}
```
- SftpHelper:sftp操作帮助类,提供了Sftp客户端一些常用操作实现。
```java
@Data
@Slf4j
public class SftpHelper {
/** SFTP 客户端*/
@Autowired
private SftpClient sftpClient;
...
/**
* 下载默认当前日期目录下,指定文件名的文件为byte[]
* 适用于路径配置如:/dingxiang/{yyyyMMdd}
* @author duanyong@jccfc.com
* @date 2024/9/24 17:53
* @param channel 渠道ID
* @param fileName 文件名
* @return: byte
*/
public byte[] downloadNow(final String channel,String fileName){
return sftpClient.runAndReturn(channel,sftpTemplate-> sftpTemplate.downloadNow(fileName));
}
/**
* 下载指定目录下,指定文件名的文件为byte[]
*
* @author duanyong@jccfc.com
* @date 2024/9/24 17:53
* @param channel 渠道ID
* @param dir 全路径目录
* @param fileName 文件名
* @return: byte
*/
public byte[] downloadBytes(final String channel,final String dir,String fileName){
return sftpClient.runAndReturn(channel,sftpTemplate-> sftpTemplate.download(dir,fileName));
}
...
}
```
###### 5:基于springboot starter实现组件相关类
- SftpClientProperties:组件配置参数类,该类集成了commons-pool2池配置和sftp服务器配置。
```java
@Data
@ConfigurationProperties(prefix = SftpClientProperties.PREFIX)
public class SftpClientProperties {
/** 前缀,默认值*/
public static final String PREFIX = "sftp";
/** 是否可用,默认值*/
public static final String ENABLED = "enabled";
/**
* 池配置
*/
@NestedConfigurationProperty
private Pool pool = new Pool();
/**
* sftp服务器配置
*/
private Map clients = new LinkedHashMap<>();
/**
* 池配置参数
*/
@Data
public static class Pool {
...
}
}
```
- SftpClientAutoConfiguration:组件自动配置类,该类集成了组件配置参数类,基于组件配置参数完成对SftpClient,SftpHelper对象的构建。
```java
@Configuration
@EnableConfigurationProperties(value = SftpClientProperties.class)
@ConditionalOnClass(SftpClient.class)
@ConditionalOnProperty(prefix = SftpClientProperties.PREFIX, value = SftpClientProperties.ENABLED, matchIfMissing = true)
public class SftpClientAutoConfiguration {
/** sftp连接池配置参数*/
@Autowired
private SftpClientProperties sftpClientProperties;
@Bean
@ConditionalOnMissingBean(SftpClient.class)
public SftpClient createSftpClient() throws Exception {
SftpPoolConfig sftpPoolConfig = createSftpPoolConfig(sftpClientProperties);
SftpTemplatePool sftpTemplatePool = new SftpTemplatePool(sftpClientProperties.getClients(),sftpPoolConfig);
return DefaultSftpClient.builder().sftpTemplatePool(sftpTemplatePool).build();
}
@Bean
@ConditionalOnClass(SftpClient.class)
public SftpHelper createSftpHelper() {
return new SftpHelper();
}
/**
* 创建SftpPoolConfig
*
* @author duanyong@jccfc.com
* @date 2020/5/14 13:49
* @param properties:sftp连接池配置参数
* @return: com.javacoo.sftp.client.config.SftpPoolConfig
*/
private SftpPoolConfig createSftpPoolConfig(SftpClientProperties properties) {
SftpClientProperties.Pool pool = properties.getPool();
return SftpPoolConfig.builder()
.maxTotal(pool.getMaxTotal())
.maxIdle(pool.getMaxIdle())
.minIdle(pool.getMinIdle())
.lifo(pool.isLifo())
.fairness(pool.isFairness())
.maxWaitMillis(pool.getMaxWaitMillis())
.minEvictableIdleTimeMillis(pool.getMinEvictableIdleTimeMillis())
.softMinEvictableIdleTimeMillis(pool.getSoftMinEvictableIdleTimeMillis())
.numTestsPerEvictionRun(pool.getNumTestsPerEvictionRun())
//.evictionPolicy(null)
.evictionPolicyClassName(DefaultEvictionPolicy.class.getName())
.testOnCreate(pool.isTestOnCreate())
.testOnBorrow(pool.isTestOnBorrow())
.testOnReturn(pool.isTestOnReturn())
.testWhileIdle(pool.isTestWhileIdle())
.timeBetweenEvictionRunsMillis(pool.getTimeBetweenEvictionRunsMillis())
.blockWhenExhausted(pool.isBlockWhenExhausted())
.jmxEnabled(pool.isJmxEnabled())
.jmxNamePrefix(pool.getJmxNamePrefix())
.jmxNameBase(pool.getJmxNameBase())
.retryCount(pool.getRetryCount())
.build();
}
}
```