背景
项目使用sftp作为存储介质,需要大量的上传下载,单例的sftp不能满足场景,使用ThreadLocal存储sftp连接,会频繁创建销毁连接,性能太差,于是想到将连接对象池化。
common-pool2介绍
Apache Common Pool2 是Apache提供的一个通用对象池技术实现,可以方便定制化自己需要的对象池,大名鼎鼎的 Redis 客户端 Jedis 内部连接池就是基于它来实现的。
- 依赖
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.0</version>
</dependency>
- sftpClient,用于创建连接,验证连接,销毁连接使用
package com.zhanghao.base.util;
import com.jcraft.jsch.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author zh
*/
@Data
public class SftpClient {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final AtomicLong CLIENT_NUMBER = new AtomicLong(1L);
private ChannelSftp channelSftp;
private Session session;
/**
*
*/
private String clientInfo = "sftpclient";
/**
* ssh 根目录。
* 用于判断是否成功返回连接到连接池的条件之一
*/
private String originalDir;
//创建连接
public SftpClient(SftpProperties sftpProperties) throws SftpException, JSchException {
try {
JSch jsch = new JSch();
session = jsch.getSession(sftpProperties.getUsername(), sftpProperties.getHost(), sftpProperties.getPort());
session.setPassword(sftpProperties.getPassword());
Properties config = new Properties();
if (sftpProperties.getSession() != null) {
sftpProperties.getSession().forEach(config::put);
}
session.setConfig(config);
session.connect();
channelSftp = (ChannelSftp) session.openChannel("sftp");
channelSftp.connect();
clientInfo += CLIENT_NUMBER.getAndIncrement() + ",createTime:" + DATE_TIME_FORMATTER.format(LocalDateTime.now());
originalDir = channelSftp.pwd();
} catch (Exception e) {
disconnect();
throw e;
}
}
//断开连接
public void disconnect() {
if (channelSftp != null) {
try {
channelSftp.disconnect();
} catch (Exception ignored) {
}
}
if (session != null) {
try {
session.disconnect();
} catch (Exception ignored) {
}
}
}
//验证连接
public boolean validateConnect() {
try {
return session.isConnected() && channelSftp.isConnected() && originalDir.equals(channelSftp.pwd());
} catch (Exception e) {
return false;
}
}
}
- 配置
package com.zhanghao.base.util;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author zh
*/
@Component
@ConfigurationProperties("sftp")
@Getter
@Setter
public class SftpProperties {
/**
* 地址
*/
private String host;
/**
* 端口号
*/
private int port = 22;
/**
* 用户名
*/
private String usernamel;
/**
* 密码
*/
private String password;
/**
* Session 参数配置
*/
private Map<String, String> session;
/**
* 连接池配置
*/
private Pool pool;
/**
* 连接池配置类
*/
@Data
public static class Pool {
/**
* 池中最小的连接数,只有当 timeBetweenEvictionRuns 为正时才有效
*/
private int minIdle = 0;
/**
* 池中最大的空闲连接数,为负值时表示无限
*/
private int maxIdle = 8;
/**
* 池可以产生的最大对象数,为负值时表示无限
*/
private int maxActive = 16;
/**
* 当池耗尽时,阻塞的最长时间,为负值时无限等待
*/
private long maxWait = -1;
/**
* 从池中取出对象是是否检测可用
*/
private boolean testOnBorrow = true;
/**
* 将对象返还给池时检测是否可用
*/
private boolean testOnReturn = false;
/**
* 检查连接池对象是否可用
*/
private boolean testWhileIdle = true;
/**
* 距离上次空闲线程检测完成多久后再次执行
*/
private long timeBetweenEvictionRuns = 300000L;
}
}
- 连接池(核心)
package com.zhanghao.base.util;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
/**
* @author zh
*/
public class SftpPool implements ObjectPool<SftpClient> {
private final GenericObjectPool<SftpClient> internalPool;
public SftpPool(SftpProperties sftpProperties) {
this.internalPool = new GenericObjectPool<SftpClient>(new SftpFactory(sftpProperties), getPoolConfig(sftpProperties.getPool())){
@Override
public void returnObject(SftpClient sftpClient) {
try {
sftpClient.getChannelSftp().cd(sftpClient.getOriginalDir());
} catch (Exception ignored) {
}
super.returnObject(sftpClient);
}
};
}
@Override
public void addObject() throws Exception {
internalPool.addObject();
}
@Override
public SftpClient borrowObject() throws Exception {
return internalPool.borrowObject();
}
@Override
public void clear() {
internalPool.clear();
}
@Override
public void close() {
internalPool.close();
}
@Override
public int getNumActive() {
return internalPool.getNumActive();
}
@Override
public int getNumIdle() {
return internalPool.getNumIdle();
}
@Override
public void invalidateObject(SftpClient obj) throws Exception {
internalPool.invalidateObject(obj);
}
@Override
public void returnObject(SftpClient obj) {
internalPool.returnObject(obj);
}
private static class SftpFactory extends BasePooledObjectFactory<SftpClient> {
private final SftpProperties sftpProperties;
public SftpFactory(SftpProperties sftpProperties) {
this.sftpProperties = sftpProperties;
}
@Override
public SftpClient create() throws Exception {
return new SftpClient(sftpProperties);
}
@Override
public PooledObject<SftpClient> wrap(SftpClient sftpClient) {
return new DefaultPooledObject<>(sftpClient);
}
@Override
public boolean validateObject(PooledObject<SftpClient> p) {
return p.getObject().validateConnect();
}
@Override
public void destroyObject(PooledObject<SftpClient> p) {
p.getObject().disconnect();
}
}
private GenericObjectPoolConfig<SftpClient> getPoolConfig(SftpProperties.Pool properties) {
if (properties == null) {
properties = new SftpProperties.Pool();
}
GenericObjectPoolConfig<SftpClient> config = new GenericObjectPoolConfig<>();
config.setMinIdle(properties.getMinIdle());
config.setMaxIdle(properties.getMaxIdle());
config.setMaxTotal(properties.getMaxActive());
config.setMaxWaitMillis(properties.getMaxWait());
config.setTestOnBorrow(properties.isTestOnBorrow());
config.setTestOnReturn(properties.isTestOnReturn());
config.setTestWhileIdle(properties.isTestWhileIdle());
config.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRuns());
return config;
}
}
- 实例化对象池
package com.zhanghao.base.util;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
*
* @author zh
*/
@Configuration
public class SftpPoolConfig {
@Bean
public SftpPool getSftpPool(SftpProperties sftpProperties){
return new SftpPool(sftpProperties);
}
}
- demo
@Resource
private SftpPool sftpPool;
/**
* demo
*
* @return void
* @author zh
*/
@GetMapping("/file/download")
public Integer fileDownload() {
SftpClient sftpClient = null;
try {
log.info("sftp连接池大小{}",sftpPool.getNumIdle());
sftpClient = sftpPool.borrowObject();
InputStream inputStream = sftpClient.getChannelSftp().get("/upload/233.jpg");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
IOUtils.copy(inputStream,byteArrayOutputStream);
return byteArrayOutputStream.size();
} catch (Exception e1) {
log.info("下载异常!",e1);
try {
sftpPool.invalidateObject(sftpClient);
} catch (Exception e2) {
log.info("销毁sftp连接异常!",e2);
}
sftpClient = null;
} finally {
if (null != sftpClient) {
sftpPool.returnObject(sftpClient);
}
}
return null;
}
- 压测数据解析
2022-07-26 18:26:06.467 INFO 6424 --- [io-8090-exec-77] c.zhanghao.base.biz.BizCommonController : sftp连接池大小8
2022-07-26 18:26:06.471 INFO 6424 --- [o-8090-exec-113] c.zhanghao.base.biz.BizCommonController : sftp连接池大小7
总结:
根据上文的配置,200线程下,最大创建了8个连接对象,根据根据自己的需要调整最大连接对象数和空闲数,正常情况下保持几个连接就够用了。