作者:京東物流 高圓慶
1 前言
通常一個物件建立、銷燬非常耗時的時候,我們不會頻繁的建立和銷燬它,而是考慮複用。複用物件的一種做法就是物件池,將建立好的物件放入池中維護起來,下次再用的時候直接拿池中已經建立好的物件繼續用,這就是池化的思想。在java中,有很多池管理的概念,典型的如執行緒池,資料庫連線池,socket連線池。本文章講介紹apache提供的通用物件池框架GenericObjectPool,以及基於GenericObjectPool實現的sftp連線池在國際物流排程履約系統中的應用。
2 GenericObjectPool剖析
Apache Commons Pool是一個物件池的框架,他提供了一整套用於實現物件池化的API。它提供了三種物件池:GenericKeyedObjectPool,SoftReferenceObjectPool和GenericObjectPool,其中GenericObjectPool是我們最常用的物件池,內部實現也最複雜。GenericObjectPool的UML圖如下所示:
2.1 核心介面ObjectPool
從圖中可以看出,GenericObjectPool實現了ObjectPool介面,而ObjectPool就是物件池的核心介面,它定義了一個物件池應該實現的行為。
- addObject方法:往池中新增一個物件
- borrowObject方法:從池中借走到一個物件
- returnObject方法:把物件歸還給物件池
- invalidateObject:驗證物件的有效性
- getNumIdle:返回物件池中有多少物件是空閒的,也就是能夠被借走的物件的數量。
- getNumActive:返回物件池中有物件物件是活躍的,也就是已經被借走的,在使用中的物件的數量。
- clear:清理物件池。注意是清理不是清空,該方法要求的是,清理所有空閒物件,釋放相關資源。
- close:關閉物件池。這個方法可以達到清空的效果,清理所有物件以及相關資源。
2.2 物件工廠BasePooledObjectFactory
物件的建立需要透過物件工廠來建立,物件工廠需要實現BasePooledObjectFactory介面。ObjectPool介面中往池中新增一個物件,就需要使用物件工廠來建立一個物件。該介面說明如下:
public interface PooledObjectFactory<T> {
/**
* 建立一個可由池提供服務的例項,並將其封裝在由池管理的PooledObject中。
*/
PooledObject<T> makeObject() throws Exception;
/**
* 銷燬池不再需要的例項
*/
void destroyObject(PooledObject<T> p) throws Exception;
/**
* 確保例項可以安全地由池返回
*/
boolean validateObject(PooledObject<T> p);
/**
* 重新初始化池返回的例項
*/
void activateObject(PooledObject<T> p) throws Exception;
/**
* 取消初始化要返回到空閒物件池的例項
*/
void passivateObject(PooledObject<T> p) throws Exception;
}
2.3 配置類GenericObjectPoolConfig
GenericObjectPoolConfig是封裝GenericObject池配置的簡單“結構”,此類不是執行緒安全的;它僅用於提供建立池時使用的屬性。大多數情況,可以使用GenericObjectPoolConfig提供的預設引數就可以滿足日常的需求,GenericObjectPoolConfig是一個抽象類,實際應用中需要新建配置類,然後繼承它。
2.4 工作原理流程
- 構造方法
當我們執行構造方法時,主要工作就是建立了一個儲存物件的LinkedList型別容器,也就是概念意義上的“池” - 從物件池中獲取物件
獲取池中的物件是透過borrowObject()命令,原始碼比較複雜,簡單而言就是去LinkedList中獲取一個物件,如果不存在的話,要呼叫構造方法中第一個引數Factory工廠類的makeObject()方法去建立一個物件再獲取,獲取到物件後要呼叫validateObject方法判斷該物件是否是可用的,如果是可用的才拿去使用。LinkedList容器減一 - 歸還物件到執行緒池
簡單而言就是先呼叫validateObject方法判斷該物件是否是可用的,如果可用則歸還到池中,LinkedList容器加一,如果是不可以的則則呼叫destroyObject方法進行銷燬
上面三步就是最簡單的流程,由於取和還的流程步驟都在borrowObject和returnObject方法中固定的,所以我們只要重寫Factory工廠類的makeObject()和validateObject以及destroyObject方法即可實現最簡單的池的管理控制,透過構造方法傳入該Factory工廠類物件則可以建立最簡單的物件池管理類。這算是比較好的解耦設計模式,借和還的流程如下圖所示:
3 開源框架如何使用GenericObjectPool
redis的java客戶端jedis就是基於Apache Commons Pool物件池的框架來實現的。
3.1 物件工廠類JedisFactory
物件工廠類只需實現activateObject、destroyObject、makeObject、validateObject方法即可,原始碼如下:
class JedisFactory implements PooledObjectFactory<Jedis> {
private final String host;
private final int port;
private final int timeout;
private final int newTimeout;
private final String password;
private final int database;
private final String clientName;
public JedisFactory(String host, int port, int timeout, String password, int database) {
this(host, port, timeout, password, database, (String)null);
}
public JedisFactory(String host, int port, int timeout, String password, int database, String clientName) {
this(host, port, timeout, timeout, password, database, clientName);
}
public JedisFactory(String host, int port, int timeout, int newTimeout, String password, int database, String clientName) {
this.host = host;
this.port = port;
this.timeout = timeout;
this.newTimeout = newTimeout;
this.password = password;
this.database = database;
this.clientName = clientName;
}
public void activateObject(PooledObject<Jedis> pooledJedis) throws Exception {
BinaryJedis jedis = (BinaryJedis)pooledJedis.getObject();
if (jedis.getDB() != (long)this.database) {
jedis.select(this.database);
}
}
public void destroyObject(PooledObject<Jedis> pooledJedis) throws Exception {
BinaryJedis jedis = (BinaryJedis)pooledJedis.getObject();
if (jedis.isConnected()) {
try {
try {
jedis.quit();
} catch (Exception var4) {
}
jedis.disconnect();
} catch (Exception var5) {
}
}
}
public PooledObject<Jedis> makeObject() throws Exception {
Jedis jedis = new Jedis(this.host, this.port, this.timeout, this.newTimeout);
jedis.connect();
if (null != this.password) {
jedis.auth(this.password);
}
if (this.database != 0) {
jedis.select(this.database);
}
if (this.clientName != null) {
jedis.clientSetname(this.clientName);
}
return new DefaultPooledObject(jedis);
}
public void passivateObject(PooledObject<Jedis> pooledJedis) throws Exception {
}
public boolean validateObject(PooledObject<Jedis> pooledJedis) {
BinaryJedis jedis = (BinaryJedis)pooledJedis.getObject();
try {
return jedis.isConnected() && jedis.ping().equals("PONG");
} catch (Exception var4) {
return false;
}
}
}
3.2 配置類JedisPoolConfig
public class JedisPoolConfig extends GenericObjectPoolConfig {
public JedisPoolConfig() {
this.setTestWhileIdle(true);
this.setMinEvictableIdleTimeMillis(60000L);
this.setTimeBetweenEvictionRunsMillis(30000L);
this.setNumTestsPerEvictionRun(-1);
}
}
4 國際物流履約系統中的應用
在國際物流履約系統中,我們和客戶互動檔案經常使用sftp伺服器,因為建立sftp伺服器的連線比較耗時,所以基於Apache Commons Pool物件池的框架來實現的我們自己的sftp連結池。
4.1 sftp物件池
SftpPool比較簡單,直接繼承GenericObjectPool。
public class SftpPool extends GenericObjectPool<Sftp> {
public SftpPool(SftpFactory factory, SftpPoolConfig config, SftpAbandonedConfig abandonedConfig) {
super(factory, config, abandonedConfig);
}
}
4.2 物件工廠SftpFactory
這是基於Apache Commons Pool框架實現自定義物件池的核心類,程式碼如下:
public class SftpFactory extends BasePooledObjectFactory<Sftp> {
private static final String CHANNEL_TYPE = "sftp";
private static Properties sshConfig = new Properties();
private String host;
private int port;
private String username;
private String password;
static {
sshConfig.put("StrictHostKeyChecking", "no");
}
@Override
public Sftp create() {
try {
JSch jsch = new JSch();
Session sshSession = jsch.getSession(username, host, port);
sshSession.setPassword(password);
sshSession.setConfig(sshConfig);
sshSession.connect();
ChannelSftp channel = (ChannelSftp) sshSession.openChannel(CHANNEL_TYPE);
channel.connect();
log.info("sftpFactory建立sftp");
return new Sftp(channel);
} catch (JSchException e) {
log.error("連線sftp失敗:", e);
throw new BizException(ResultCodeEnum.SFTP_EXCEPTION);
}
}
/**
* @param sftp 被包裝的物件
* @return 物件包裝器
*/
@Override
public PooledObject<Sftp> wrap(Sftp sftp) {
return new DefaultPooledObject<>(sftp);
}
/**
* 銷燬物件
* @param p 物件包裝器
*/
@Override
public void destroyObject(PooledObject<Sftp> p) {
log.info("開始銷燬channelSftp");
if (p!=null) {
Sftp sftp = p.getObject();
if (sftp!=null) {
ChannelSftp channelSftp = sftp.getChannelSftp();
if (channelSftp!=null) {
channelSftp.disconnect();
log.info("銷燬channelSftp成功");
}
}
}
}
/**
* 檢查連線是否可用
*
* @param p 物件包裝器
* @return {@code true} 可用,{@code false} 不可用
*/
@Override
public boolean validateObject(PooledObject<Sftp> p) {
if (p!=null) {
Sftp sftp = p.getObject();
if (sftp!=null) {
try {
sftp.getChannelSftp().cd("./");
log.info("驗證連線是否可用,結果為true");
return true;
} catch (SftpException e) {
log.info("驗證連線是否可用,結果為false",e);
return false;
}
}
}
log.info("驗證連線是否可用,結果為false");
return false;
}
public static class Builder {
private String host;
private int port;
private String username;
private String password;
public SftpFactory build() {
return new SftpFactory(host, port, username, password);
}
public Builder host(String host) {
this.host = host;
return this;
}
public Builder port(int port) {
this.port = port;
return this;
}
public Builder username(String username) {
this.username = username;
return this;
}
public Builder password(String password) {
this.password = password;
return this;
}
}
}
4.3 配置類SftpPoolConfig
配置類繼承了GenericObjectPoolConfig,可繼承該類的預設屬性,也可自定義配置引數。
public class SftpPoolConfig extends GenericObjectPoolConfig<Sftp> {
public static class Builder {
private int maxTotal;
private int maxIdle;
private int minIdle;
private boolean lifo;
private boolean fairness;
private long maxWaitMillis;
private long minEvictableIdleTimeMillis;
private long evictorShutdownTimeoutMillis;
private long softMinEvictableIdleTimeMillis;
private int numTestsPerEvictionRun;
private EvictionPolicy<Sftp> evictionPolicy; // 僅2.6.0版本commons-pool2需要設定
private String evictionPolicyClassName;
private boolean testOnCreate;
private boolean testOnBorrow;
private boolean testOnReturn;
private boolean testWhileIdle;
private long timeBetweenEvictionRunsMillis;
private boolean blockWhenExhausted;
private boolean jmxEnabled;
private String jmxNamePrefix;
private String jmxNameBase;
public SftpPoolConfig build() {
SftpPoolConfig config = new SftpPoolConfig();
config.setMaxTotal(maxTotal);
config.setMaxIdle(maxIdle);
config.setMinIdle(minIdle);
config.setLifo(lifo);
config.setFairness(fairness);
config.setMaxWaitMillis(maxWaitMillis);
config.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
config.setEvictorShutdownTimeoutMillis(evictorShutdownTimeoutMillis);
config.setSoftMinEvictableIdleTimeMillis(softMinEvictableIdleTimeMillis);
config.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
config.setEvictionPolicy(evictionPolicy);
config.setEvictionPolicyClassName(evictionPolicyClassName);
config.setTestOnCreate(testOnCreate);
config.setTestOnBorrow(testOnBorrow);
config.setTestOnReturn(testOnReturn);
config.setTestWhileIdle(testWhileIdle);
config.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
config.setBlockWhenExhausted(blockWhenExhausted);
config.setJmxEnabled(jmxEnabled);
config.setJmxNamePrefix(jmxNamePrefix);
config.setJmxNameBase(jmxNameBase);
return config;
}
}
4.4 SftpClient配置類
讀取配置檔案,建立SftpFactory、SftpPoolConfig、SftpPool,程式碼如下:
@Configuration
@ConditionalOnClass(SftpPool.class)
@EnableConfigurationProperties(SftpClientProperties.class)
public class SftpClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ISftpClient sftpClient(SftpClientProperties sftpClientProperties) {
if (sftpClientProperties.isMultiple()) {
MultipleSftpClient multipleSftpClient = new MultipleSftpClient();
sftpClientProperties.getClients().forEach((name, properties) -> {
SftpFactory sftpFactory = createSftpFactory(properties);
SftpPoolConfig sftpPoolConfig = createSftpPoolConfig(properties);
SftpAbandonedConfig sftpAbandonedConfig = createSftpAbandonedConfig(properties);
SftpPool sftpPool = new SftpPool(sftpFactory, sftpPoolConfig, sftpAbandonedConfig);
ISftpClient sftpClient = new SftpClient(sftpPool);
multipleSftpClient.put(name, sftpClient);
});
return multipleSftpClient;
}
SftpFactory sftpFactory = createSftpFactory(sftpClientProperties);
SftpPoolConfig sftpPoolConfig = createSftpPoolConfig(sftpClientProperties);
SftpAbandonedConfig sftpAbandonedConfig = createSftpAbandonedConfig(sftpClientProperties);
SftpPool sftpPool = new SftpPool(sftpFactory, sftpPoolConfig, sftpAbandonedConfig);
return new SftpClient(sftpPool);
}
public SftpFactory createSftpFactory(SftpClientProperties properties) {
return new SftpFactory.Builder()
.host(properties.getHost())
.port(properties.getPort())
.username(properties.getUsername())
.password(properties.getPassword())
.build();
}
public SftpPoolConfig createSftpPoolConfig(SftpClientProperties properties) {
SftpClientProperties.Pool pool = properties.getPool();
return new SftpPoolConfig.Builder()
.maxTotal(pool.getMaxTotal())
.maxIdle(pool.getMaxIdle())
.minIdle(pool.getMinIdle())
.lifo(pool.isLifo())
.fairness(pool.isFairness())
.maxWaitMillis(pool.getMaxWaitMillis())
.minEvictableIdleTimeMillis(pool.getMinEvictableIdleTimeMillis())
.evictorShutdownTimeoutMillis(pool.getEvictorShutdownTimeoutMillis())
.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())
.build();
}
public SftpAbandonedConfig createSftpAbandonedConfig(SftpClientProperties properties) {
SftpClientProperties.Abandoned abandoned = properties.getAbandoned();
return new SftpAbandonedConfig.Builder()
.removeAbandonedOnBorrow(abandoned.isRemoveAbandonedOnBorrow())
.removeAbandonedOnMaintenance(abandoned.isRemoveAbandonedOnMaintenance())
.removeAbandonedTimeout(abandoned.getRemoveAbandonedTimeout())
.logAbandoned(abandoned.isLogAbandoned())
.requireFullStackTrace(abandoned.isRequireFullStackTrace())
.logWriter(new PrintWriter(System.out))
.useUsageTracking(abandoned.isUseUsageTracking())
.build();
}
}
4.5 物件SftpClient
SftpClient是實際工作的類,從SftpClient 中可獲取到一個sftp連結,使用完成後,歸還給sftpPool。SftpClient程式碼如下:
public class SftpClient implements ISftpClient {
private SftpPool sftpPool;
/**
* 從sftp連線池獲取連線並執行操作
*
* @param handler sftp操作
*/
@Override
public void open(ISftpClient.Handler handler) {
Sftp sftp = null;
try {
sftp = sftpPool.borrowObject();
ISftpClient.Handler policyHandler = new DelegateHandler(handler);
policyHandler.doHandle(sftp);
} catch (Exception e) {
log.error("sftp異常:", e);
throw new BizException(ResultCodeEnum.SFTP_EXCEPTION);
} finally {
if (sftp != null) {
sftpPool.returnObject(sftp);
}
}
}
@AllArgsConstructor
static class DelegateHandler implements ISftpClient.Handler {
private ISftpClient.Handler target;
@Override
public void doHandle(Sftp sftp) {
try {
target.doHandle(sftp);
} catch (Exception e) {
log.error("sftp異常:", e);
throw new BizException(ResultCodeEnum.SFTP_EXCEPTION);
}
}
}
}
4.6 實戰程式碼示例
透過sftp上傳檔案到XX伺服器
//透過SFTP上傳到XX
((MultipleSftpClient) sftpClient).choose("XX");
sftpClient.open(sftp -> {
boolean exist = sftp.isExist(inventoryPath);
if(!exist){
sftp.mkdirs(inventoryPath);
}
// 執行sftp操作
InputStream is = new FileInputStream(oneColumnCSVFile);
sftp.upload(inventoryPath, titleName, is);
log.info("inventory upload over");
});
5 總結
透過本文的介紹可以知道,Apache Commons Pool定義了一個物件池的行為,提供了可擴充套件的配置類和物件工廠,封裝了物件建立、從池中獲取物件、歸還物件的核心流程。還介紹了開源框架Jedis是如何基於GenericObjectPool來實現的連線池。最後介紹了國際物流履約系統中是如何基於GenericObjectPool來管理Sftp連線的。
掌握了GenericObjectPool的核心原理,我們就可以透過實現幾個關鍵的介面,建立一個物件池管理工具,在專案中避免了物件的頻繁建立和銷燬,從而顯著提升程式的效能。