巧用GenericObjectPool建立自定義物件池

京東雲開發者發表於2023-03-17

作者:京東物流 高圓慶

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介面中往池中新增一個物件,就需要使用物件工廠來建立一個物件。該介面說明如下:

  1. public interface PooledObjectFactory<T> {
  2. /**
  3. * 建立一個可由池提供服務的例項,並將其封裝在由池管理的PooledObject中。
  4. */
  5. PooledObject<T> makeObject() throws Exception;
  6. /**
  7. * 銷燬池不再需要的例項
  8. */
  9. void destroyObject(PooledObject<T> p) throws Exception;
  10. /**
  11. * 確保例項可以安全地由池返回
  12. */
  13. boolean validateObject(PooledObject<T> p);
  14. /**
  15. * 重新初始化池返回的例項
  16. */
  17. void activateObject(PooledObject<T> p) throws Exception;
  18. /**
  19. * 取消初始化要返回到空閒物件池的例項
  20. */
  21. void passivateObject(PooledObject<T> p) throws Exception;
  22. }

2.3 配置類GenericObjectPoolConfig

GenericObjectPoolConfig是封裝GenericObject池配置的簡單“結構”,此類不是執行緒安全的;它僅用於提供建立池時使用的屬性。大多數情況,可以使用GenericObjectPoolConfig提供的預設引數就可以滿足日常的需求,GenericObjectPoolConfig是一個抽象類,實際應用中需要新建配置類,然後繼承它。

2.4 工作原理流程

  1. 構造方法
    當我們執行構造方法時,主要工作就是建立了一個儲存物件的LinkedList型別容器,也就是概念意義上的“池”
  2. 從物件池中獲取物件
    獲取池中的物件是透過borrowObject()命令,原始碼比較複雜,簡單而言就是去LinkedList中獲取一個物件,如果不存在的話,要呼叫構造方法中第一個引數Factory工廠類的makeObject()方法去建立一個物件再獲取,獲取到物件後要呼叫validateObject方法判斷該物件是否是可用的,如果是可用的才拿去使用。LinkedList容器減一
  3. 歸還物件到執行緒池
    簡單而言就是先呼叫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方法即可,原始碼如下:

  1. class JedisFactory implements PooledObjectFactory<Jedis> {
  2. private final String host;
  3. private final int port;
  4. private final int timeout;
  5. private final int newTimeout;
  6. private final String password;
  7. private final int database;
  8. private final String clientName;
  9. public JedisFactory(String host, int port, int timeout, String password, int database) {
  10. this(host, port, timeout, password, database, (String)null);
  11. }
  12. public JedisFactory(String host, int port, int timeout, String password, int database, String clientName) {
  13. this(host, port, timeout, timeout, password, database, clientName);
  14. }
  15. public JedisFactory(String host, int port, int timeout, int newTimeout, String password, int database, String clientName) {
  16. this.host = host;
  17. this.port = port;
  18. this.timeout = timeout;
  19. this.newTimeout = newTimeout;
  20. this.password = password;
  21. this.database = database;
  22. this.clientName = clientName;
  23. }
  24. public void activateObject(PooledObject<Jedis> pooledJedis) throws Exception {
  25. BinaryJedis jedis = (BinaryJedis)pooledJedis.getObject();
  26. if (jedis.getDB() != (long)this.database) {
  27. jedis.select(this.database);
  28. }
  29. }
  30. public void destroyObject(PooledObject<Jedis> pooledJedis) throws Exception {
  31. BinaryJedis jedis = (BinaryJedis)pooledJedis.getObject();
  32. if (jedis.isConnected()) {
  33. try {
  34. try {
  35. jedis.quit();
  36. } catch (Exception var4) {
  37. }
  38. jedis.disconnect();
  39. } catch (Exception var5) {
  40. }
  41. }
  42. }
  43. public PooledObject<Jedis> makeObject() throws Exception {
  44. Jedis jedis = new Jedis(this.host, this.port, this.timeout, this.newTimeout);
  45. jedis.connect();
  46. if (null != this.password) {
  47. jedis.auth(this.password);
  48. }
  49. if (this.database != 0) {
  50. jedis.select(this.database);
  51. }
  52. if (this.clientName != null) {
  53. jedis.clientSetname(this.clientName);
  54. }
  55. return new DefaultPooledObject(jedis);
  56. }
  57. public void passivateObject(PooledObject<Jedis> pooledJedis) throws Exception {
  58. }
  59. public boolean validateObject(PooledObject<Jedis> pooledJedis) {
  60. BinaryJedis jedis = (BinaryJedis)pooledJedis.getObject();
  61. try {
  62. return jedis.isConnected() && jedis.ping().equals("PONG");
  63. } catch (Exception var4) {
  64. return false;
  65. }
  66. }
  67. }

3.2 配置類JedisPoolConfig

  1. public class JedisPoolConfig extends GenericObjectPoolConfig {
  2. public JedisPoolConfig() {
  3. this.setTestWhileIdle(true);
  4. this.setMinEvictableIdleTimeMillis(60000L);
  5. this.setTimeBetweenEvictionRunsMillis(30000L);
  6. this.setNumTestsPerEvictionRun(-1);
  7. }
  8. }

4 國際物流履約系統中的應用

在國際物流履約系統中,我們和客戶互動檔案經常使用sftp伺服器,因為建立sftp伺服器的連線比較耗時,所以基於Apache Commons Pool物件池的框架來實現的我們自己的sftp連結池。

4.1 sftp物件池

SftpPool比較簡單,直接繼承GenericObjectPool。

  1. public class SftpPool extends GenericObjectPool<Sftp> {
  2. public SftpPool(SftpFactory factory, SftpPoolConfig config, SftpAbandonedConfig abandonedConfig) {
  3. super(factory, config, abandonedConfig);
  4. }
  5. }

4.2 物件工廠SftpFactory

這是基於Apache Commons Pool框架實現自定義物件池的核心類,程式碼如下:

  1. public class SftpFactory extends BasePooledObjectFactory<Sftp> {
  2. private static final String CHANNEL_TYPE = "sftp";
  3. private static Properties sshConfig = new Properties();
  4. private String host;
  5. private int port;
  6. private String username;
  7. private String password;
  8. static {
  9. sshConfig.put("StrictHostKeyChecking", "no");
  10. }
  11. @Override
  12. public Sftp create() {
  13. try {
  14. JSch jsch = new JSch();
  15. Session sshSession = jsch.getSession(username, host, port);
  16. sshSession.setPassword(password);
  17. sshSession.setConfig(sshConfig);
  18. sshSession.connect();
  19. ChannelSftp channel = (ChannelSftp) sshSession.openChannel(CHANNEL_TYPE);
  20. channel.connect();
  21. log.info("sftpFactory建立sftp");
  22. return new Sftp(channel);
  23. } catch (JSchException e) {
  24. log.error("連線sftp失敗:", e);
  25. throw new BizException(ResultCodeEnum.SFTP_EXCEPTION);
  26. }
  27. }
  28. /**
  29. * @param sftp 被包裝的物件
  30. * @return 物件包裝器
  31. */
  32. @Override
  33. public PooledObject<Sftp> wrap(Sftp sftp) {
  34. return new DefaultPooledObject<>(sftp);
  35. }
  36. /**
  37. * 銷燬物件
  38. * @param p 物件包裝器
  39. */
  40. @Override
  41. public void destroyObject(PooledObject<Sftp> p) {
  42. log.info("開始銷燬channelSftp");
  43. if (p!=null) {
  44. Sftp sftp = p.getObject();
  45. if (sftp!=null) {
  46. ChannelSftp channelSftp = sftp.getChannelSftp();
  47. if (channelSftp!=null) {
  48. channelSftp.disconnect();
  49. log.info("銷燬channelSftp成功");
  50. }
  51. }
  52. }
  53. }
  54. /**
  55. * 檢查連線是否可用
  56. *
  57. * @param p 物件包裝器
  58. * @return {@code true} 可用,{@code false} 不可用
  59. */
  60. @Override
  61. public boolean validateObject(PooledObject<Sftp> p) {
  62. if (p!=null) {
  63. Sftp sftp = p.getObject();
  64. if (sftp!=null) {
  65. try {
  66. sftp.getChannelSftp().cd("./");
  67. log.info("驗證連線是否可用,結果為true");
  68. return true;
  69. } catch (SftpException e) {
  70. log.info("驗證連線是否可用,結果為false",e);
  71. return false;
  72. }
  73. }
  74. }
  75. log.info("驗證連線是否可用,結果為false");
  76. return false;
  77. }
  78. public static class Builder {
  79. private String host;
  80. private int port;
  81. private String username;
  82. private String password;
  83. public SftpFactory build() {
  84. return new SftpFactory(host, port, username, password);
  85. }
  86. public Builder host(String host) {
  87. this.host = host;
  88. return this;
  89. }
  90. public Builder port(int port) {
  91. this.port = port;
  92. return this;
  93. }
  94. public Builder username(String username) {
  95. this.username = username;
  96. return this;
  97. }
  98. public Builder password(String password) {
  99. this.password = password;
  100. return this;
  101. }
  102. }
  103. }

4.3 配置類SftpPoolConfig

配置類繼承了GenericObjectPoolConfig,可繼承該類的預設屬性,也可自定義配置引數。

  1. public class SftpPoolConfig extends GenericObjectPoolConfig<Sftp> {
  2. public static class Builder {
  3. private int maxTotal;
  4. private int maxIdle;
  5. private int minIdle;
  6. private boolean lifo;
  7. private boolean fairness;
  8. private long maxWaitMillis;
  9. private long minEvictableIdleTimeMillis;
  10. private long evictorShutdownTimeoutMillis;
  11. private long softMinEvictableIdleTimeMillis;
  12. private int numTestsPerEvictionRun;
  13. private EvictionPolicy<Sftp> evictionPolicy; // 僅2.6.0版本commons-pool2需要設定
  14. private String evictionPolicyClassName;
  15. private boolean testOnCreate;
  16. private boolean testOnBorrow;
  17. private boolean testOnReturn;
  18. private boolean testWhileIdle;
  19. private long timeBetweenEvictionRunsMillis;
  20. private boolean blockWhenExhausted;
  21. private boolean jmxEnabled;
  22. private String jmxNamePrefix;
  23. private String jmxNameBase;
  24. public SftpPoolConfig build() {
  25. SftpPoolConfig config = new SftpPoolConfig();
  26. config.setMaxTotal(maxTotal);
  27. config.setMaxIdle(maxIdle);
  28. config.setMinIdle(minIdle);
  29. config.setLifo(lifo);
  30. config.setFairness(fairness);
  31. config.setMaxWaitMillis(maxWaitMillis);
  32. config.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
  33. config.setEvictorShutdownTimeoutMillis(evictorShutdownTimeoutMillis);
  34. config.setSoftMinEvictableIdleTimeMillis(softMinEvictableIdleTimeMillis);
  35. config.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
  36. config.setEvictionPolicy(evictionPolicy);
  37. config.setEvictionPolicyClassName(evictionPolicyClassName);
  38. config.setTestOnCreate(testOnCreate);
  39. config.setTestOnBorrow(testOnBorrow);
  40. config.setTestOnReturn(testOnReturn);
  41. config.setTestWhileIdle(testWhileIdle);
  42. config.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
  43. config.setBlockWhenExhausted(blockWhenExhausted);
  44. config.setJmxEnabled(jmxEnabled);
  45. config.setJmxNamePrefix(jmxNamePrefix);
  46. config.setJmxNameBase(jmxNameBase);
  47. return config;
  48. }
  49. }

4.4 SftpClient配置類

讀取配置檔案,建立SftpFactory、SftpPoolConfig、SftpPool,程式碼如下:

  1. @Configuration
  2. @ConditionalOnClass(SftpPool.class)
  3. @EnableConfigurationProperties(SftpClientProperties.class)
  4. public class SftpClientAutoConfiguration {
  5. @Bean
  6. @ConditionalOnMissingBean
  7. public ISftpClient sftpClient(SftpClientProperties sftpClientProperties) {
  8. if (sftpClientProperties.isMultiple()) {
  9. MultipleSftpClient multipleSftpClient = new MultipleSftpClient();
  10. sftpClientProperties.getClients().forEach((name, properties) -> {
  11. SftpFactory sftpFactory = createSftpFactory(properties);
  12. SftpPoolConfig sftpPoolConfig = createSftpPoolConfig(properties);
  13. SftpAbandonedConfig sftpAbandonedConfig = createSftpAbandonedConfig(properties);
  14. SftpPool sftpPool = new SftpPool(sftpFactory, sftpPoolConfig, sftpAbandonedConfig);
  15. ISftpClient sftpClient = new SftpClient(sftpPool);
  16. multipleSftpClient.put(name, sftpClient);
  17. });
  18. return multipleSftpClient;
  19. }
  20. SftpFactory sftpFactory = createSftpFactory(sftpClientProperties);
  21. SftpPoolConfig sftpPoolConfig = createSftpPoolConfig(sftpClientProperties);
  22. SftpAbandonedConfig sftpAbandonedConfig = createSftpAbandonedConfig(sftpClientProperties);
  23. SftpPool sftpPool = new SftpPool(sftpFactory, sftpPoolConfig, sftpAbandonedConfig);
  24. return new SftpClient(sftpPool);
  25. }
  26. public SftpFactory createSftpFactory(SftpClientProperties properties) {
  27. return new SftpFactory.Builder()
  28. .host(properties.getHost())
  29. .port(properties.getPort())
  30. .username(properties.getUsername())
  31. .password(properties.getPassword())
  32. .build();
  33. }
  34. public SftpPoolConfig createSftpPoolConfig(SftpClientProperties properties) {
  35. SftpClientProperties.Pool pool = properties.getPool();
  36. return new SftpPoolConfig.Builder()
  37. .maxTotal(pool.getMaxTotal())
  38. .maxIdle(pool.getMaxIdle())
  39. .minIdle(pool.getMinIdle())
  40. .lifo(pool.isLifo())
  41. .fairness(pool.isFairness())
  42. .maxWaitMillis(pool.getMaxWaitMillis())
  43. .minEvictableIdleTimeMillis(pool.getMinEvictableIdleTimeMillis())
  44. .evictorShutdownTimeoutMillis(pool.getEvictorShutdownTimeoutMillis())
  45. .softMinEvictableIdleTimeMillis(pool.getSoftMinEvictableIdleTimeMillis())
  46. .numTestsPerEvictionRun(pool.getNumTestsPerEvictionRun())
  47. .evictionPolicy(null)
  48. .evictionPolicyClassName(DefaultEvictionPolicy.class.getName())
  49. .testOnCreate(pool.isTestOnCreate())
  50. .testOnBorrow(pool.isTestOnBorrow())
  51. .testOnReturn(pool.isTestOnReturn())
  52. .testWhileIdle(pool.isTestWhileIdle())
  53. .timeBetweenEvictionRunsMillis(pool.getTimeBetweenEvictionRunsMillis())
  54. .blockWhenExhausted(pool.isBlockWhenExhausted())
  55. .jmxEnabled(pool.isJmxEnabled())
  56. .jmxNamePrefix(pool.getJmxNamePrefix())
  57. .jmxNameBase(pool.getJmxNameBase())
  58. .build();
  59. }
  60. public SftpAbandonedConfig createSftpAbandonedConfig(SftpClientProperties properties) {
  61. SftpClientProperties.Abandoned abandoned = properties.getAbandoned();
  62. return new SftpAbandonedConfig.Builder()
  63. .removeAbandonedOnBorrow(abandoned.isRemoveAbandonedOnBorrow())
  64. .removeAbandonedOnMaintenance(abandoned.isRemoveAbandonedOnMaintenance())
  65. .removeAbandonedTimeout(abandoned.getRemoveAbandonedTimeout())
  66. .logAbandoned(abandoned.isLogAbandoned())
  67. .requireFullStackTrace(abandoned.isRequireFullStackTrace())
  68. .logWriter(new PrintWriter(System.out))
  69. .useUsageTracking(abandoned.isUseUsageTracking())
  70. .build();
  71. }
  72. }

4.5 物件SftpClient

SftpClient是實際工作的類,從SftpClient 中可獲取到一個sftp連結,使用完成後,歸還給sftpPool。SftpClient程式碼如下:

  1. public class SftpClient implements ISftpClient {
  2. private SftpPool sftpPool;
  3. /**
  4. * 從sftp連線池獲取連線並執行操作
  5. *
  6. * @param handler sftp操作
  7. */
  8. @Override
  9. public void open(ISftpClient.Handler handler) {
  10. Sftp sftp = null;
  11. try {
  12. sftp = sftpPool.borrowObject();
  13. ISftpClient.Handler policyHandler = new DelegateHandler(handler);
  14. policyHandler.doHandle(sftp);
  15. } catch (Exception e) {
  16. log.error("sftp異常:", e);
  17. throw new BizException(ResultCodeEnum.SFTP_EXCEPTION);
  18. } finally {
  19. if (sftp != null) {
  20. sftpPool.returnObject(sftp);
  21. }
  22. }
  23. }
  24. @AllArgsConstructor
  25. static class DelegateHandler implements ISftpClient.Handler {
  26. private ISftpClient.Handler target;
  27. @Override
  28. public void doHandle(Sftp sftp) {
  29. try {
  30. target.doHandle(sftp);
  31. } catch (Exception e) {
  32. log.error("sftp異常:", e);
  33. throw new BizException(ResultCodeEnum.SFTP_EXCEPTION);
  34. }
  35. }
  36. }
  37. }

4.6 實戰程式碼示例

透過sftp上傳檔案到XX伺服器

  1. //透過SFTP上傳到XX
  2. ((MultipleSftpClient) sftpClient).choose("XX");
  3. sftpClient.open(sftp -> {
  4. boolean exist = sftp.isExist(inventoryPath);
  5. if(!exist){
  6. sftp.mkdirs(inventoryPath);
  7. }
  8. // 執行sftp操作
  9. InputStream is = new FileInputStream(oneColumnCSVFile);
  10. sftp.upload(inventoryPath, titleName, is);
  11. log.info("inventory upload over");
  12. });

5 總結

透過本文的介紹可以知道,Apache Commons Pool定義了一個物件池的行為,提供了可擴充套件的配置類和物件工廠,封裝了物件建立、從池中獲取物件、歸還物件的核心流程。還介紹了開源框架Jedis是如何基於GenericObjectPool來實現的連線池。最後介紹了國際物流履約系統中是如何基於GenericObjectPool來管理Sftp連線的。
掌握了GenericObjectPool的核心原理,我們就可以透過實現幾個關鍵的介面,建立一個物件池管理工具,在專案中避免了物件的頻繁建立和銷燬,從而顯著提升程式的效能。

相關文章