天天看點

apache-common-pool2對象池的使用

一、概述

大多時候,我們擷取對象的方法都是直接new一個。但是,對于大對象的構造,或者構造耗時比較久的對象,我們每次要使用都去new一個是很不科學的。比如資料庫的連接配接對象、redis的連接配接對象、Http連接配接請求對象等等。在設計模式中有一個專門的模式來解決這種場景下的問題,即享元模式。

享元模式其實很好了解,也就是構造一個對象池,這個對象池中維護一定數量的對象,需要的時候就從這個對象池中擷取對象,使用完後返還給對象池。這樣就避免構造對象所帶來的耗時,提升了系統的性能。

設計這樣的一個對象池看起來好像并不難,甚至覺的隻需要一個List就可以做到。但是,如果考慮到系統的伸縮性,比如在系統忙時可能需要對象池中有足夠的對象可以被拿來使用,以保證系統大多時候應該等待對象而進入阻塞,同時,在系統閑時又不需要太多的對象存放在對象池中,這時候就需要釋放一些對象。另外,還需要考慮對象何時構造,何時銷毀,對象異常的處理等問題。

為了讓大多java程式員不重複造輪子,apache開發了一個庫,專門給需要對象池的程式提供一個底層的支援。這個庫也就是

apche-common-pool2

,使用這個庫,我們隻需要關注對象的生成、銷毀、校驗等操作就可以了。對象池的具體實作細節都交給common-pool2中的具體對象池實作類來完成。

二、maven位址

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.4.2</version>
</dependency>
           

三、相關接口

common-pool2下有幾個很重要的接口,common-pool2也是以這幾個接口為基礎進行開發的。

ObjectPool.java

,抽象了對象池的一個整體的行為。

//從對象池中擷取對象的方法
    T borrowObject() throws Exception, NoSuchElementException,
            IllegalStateException;
    //将對象返還給對象池
    void returnObject(T obj) throws Exception;
    //讓對象失效
    void invalidateObject(T obj) throws Exception;
    //往對象池中新增一個對象
    void addObject() throws Exception, IllegalStateException,
            UnsupportedOperationException;
    //擷取目前閑置在對象池中的對象數量,即沒有被拿走使用的對象數量
    int getNumIdle();
    //擷取已經在使用中的對象數量,即被使用者從對象池中拿走使用的數量
    int getNumActive();
    //清空對象池中閑置的所有對象
    void clear() throws Exception, UnsupportedOperationException;
    //關閉對象池
    void close();
           

PooledObject

,抽象了對象池中對象應該具備的一些屬性。注意,這個對象并不是我們真正要存的對象,而是經過一層封裝的對象。比如我們想往對象池存放String類型的對象,那麼真正存放在對象池的其實都是經過封裝過的對象,即PooledObject對象。

public interface PooledObject<T> extends Comparable<PooledObject<T>> {
    T getObject();
    long getCreateTime();
    long getActiveTimeMillis();
    long getIdleTimeMillis();
    long getLastBorrowTime();
    long getLastReturnTime();
    long getLastUsedTime();
    @Override
    int compareTo(PooledObject<T> other);
    @Override
    boolean equals(Object obj);
    @Override
    int hashCode();
    String toString();
    boolean startEvictionTest();
    boolean endEvictionTest(Deque<PooledObject<T>> idleQueue);
    boolean allocate();
    boolean deallocate();
    void invalidate();
    void setLogAbandoned(boolean logAbandoned);
    void use();
    void printStackTrace(PrintWriter writer);
    PooledObjectState getState();
    void markAbandoned();
    void markReturning();
}
           

PooledObjectFactory.java

,抽象了生成對象的工廠模型

public interface PooledObjectFactory<T> {
  //構造一個封裝對象
  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;
}
           

同時還有

KeyedObjectPool

,

KeyedPooledObjectFactory

接口,主要提供了帶有鍵的對象池模闆。帶有鍵的對象池可以了解為一個 Map

四、正常對象池的使用

下面舉個例子,假設我們要做一個維護資料庫連接配接的對象池。我們隻需要定義兩個類

DbConnection

,用于連接配接資料庫的對象。為了讓代碼不那麼複雜,下面的類并沒有連接配接資料庫的代碼,隻有一個簡單的屬性,表示是否連接配接上資料庫。

public class DbConnection {

    private Boolean isActive;

    public Boolean getActive() {
        return isActive;
    }

    public void setActive(Boolean active) {
        isActive = active;
    }
}
           

再定義一個構造DbConnection的工廠類

DbConnectionFactory

,然後實作一些方法

public class DbConnectionFactory implements PooledObjectFactory<DbConnection> {

    @Override
    public PooledObject<DbConnection> makeObject() throws Exception {
        DbConnection dbConnection = new DbConnection();
        //構造一個新的連接配接對象
        return new DefaultPooledObject<>(dbConnection);
    }

    @Override
    public void destroyObject(PooledObject<DbConnection> p) throws Exception {
        //斷開連接配接
        p.getObject().setActive(false);
    }

    @Override
    public boolean validateObject(PooledObject<DbConnection> p) {
        //判斷這個對象是否是保持連接配接狀态
        return p.getObject().getActive();
    }

    @Override
    public void activateObject(PooledObject<DbConnection> p) throws Exception {
        //激活這個對象,讓它連接配接上資料庫
        p.getObject().setActive(true);
    }

    @Override
    public void passivateObject(PooledObject<DbConnection> p) throws Exception {
        //不處理
    }
}
           

到這裡,我們就可以使用common-pool2自帶的

GenericObjectPool

類了。

public static void main(String[] args) {
        DbConnectionFactory factory = new DbConnectionFactory();
        //設定對象池的相關參數
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setMaxIdle();
        poolConfig.setMaxTotal();
        poolConfig.setMinIdle();
        //建立一個對象池,傳入對象工廠和配置
        GenericObjectPool<DbConnection> objectPool = new GenericObjectPool<>(factory, poolConfig);
        DbConnection dbConnection = null;
        try {
            //從對象池擷取對象,如果
            dbConnection = objectPool.borrowObject();
            System.out.println(dbConnection.getActive());
            //使用改對象
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (dbConnection != null) {
                //返還對象
                objectPool.returnObject(dbConnection);
            }
        }
    }
           

這樣,我們就已經完成的正常對象池的所有開發。

SoftReferenceObjectPool

common-pool2還提供了一個軟引用對象池的實作類,原理和GenericObjectPool差不多,不過對象池中存放的封裝對象是軟引用對象。軟引用對象主要是在堆空間不足會被GC回收,再具體的細節請讀者自行百度。

五、帶有鍵的對象池

目前我們看到的對象池的對象都是屬于一個類型的,也就是說如果要求對象池中存放的對象類型不一樣,正常對象池就做不到了。

比如現在有這樣一個場景,項目中要連接配接的資料庫比較多,每個資料庫的位址賬号密碼都不一樣,但是上面的正常對象池存放的連接配接對象好像沒辦法在取對象時指定擷取哪個資料庫的連接配接對象。這是,就需要帶有鍵的對象池,也就是

GenericKeyedObjectPool

我們再定義個DbConnection2,多一個字段url,來表示屬于哪個資料庫的連接配接

public class DbConnection2 {

    private Boolean isActive;
    private String url;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public Boolean getActive() {
        return isActive;
    }

    public void setActive(Boolean active) {
        isActive = active;
    }
}
           

再實作一個KeyedPooledObjectFactory類

public class DbConnectionKeyFactory implements KeyedPooledObjectFactory<String, DbConnection2> {

    @Override
    public PooledObject<DbConnection2> makeObject(String key) throws Exception {
        DbConnection2 dbConnection2 = new DbConnection2();
        dbConnection2.setUrl(key);
        dbConnection2.setActive(true);
        return new DefaultPooledObject<>(dbConnection2);
    }

    @Override
    public void destroyObject(String key, PooledObject<DbConnection2> p) throws Exception {
        p.getObject().setActive(false);
    }

    @Override
    public boolean validateObject(String key, PooledObject<DbConnection2> p) {
        return p.getObject().getActive();
    }

    @Override
    public void activateObject(String key, PooledObject<DbConnection2> p) throws Exception {
        p.getObject().setActive(true);
    }

    @Override
    public void passivateObject(String key, PooledObject<DbConnection2> p) throws Exception {

    }
}
           

之後就可以使用

GenericKeyedObjectPool

對象池了

public static void main(String[] args) {
        GenericKeyedObjectPoolConfig genericKeyedObjectPoolConfig = new GenericKeyedObjectPoolConfig();
        genericKeyedObjectPoolConfig.setMaxIdlePerKey();
        genericKeyedObjectPoolConfig.setMaxTotalPerKey();
        genericKeyedObjectPoolConfig.setMaxTotal();
        genericKeyedObjectPoolConfig.setMinIdlePerKey();

        DbConnectionKeyFactory dbConnectionKeyFactory = new DbConnectionKeyFactory();
        GenericKeyedObjectPool<String, DbConnection2> genericKeyedObjectPool = new GenericKeyedObjectPool<>
                (dbConnectionKeyFactory, genericKeyedObjectPoolConfig);
        DbConnection2 dbConnection1 = null;
        DbConnection2 dbConnection2 = null;
        try {
            dbConnection1 = genericKeyedObjectPool.borrowObject("192.168.0.1");
            dbConnection2 = genericKeyedObjectPool.borrowObject("192.168.0.2");
            System.out.println(dbConnection1.getUrl());
            System.out.println(dbConnection2.getUrl());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (dbConnection1 != null) {
                genericKeyedObjectPool.returnObject(dbConnection1.getUrl(), dbConnection1);
            }
            if (dbConnection2 != null) {
                genericKeyedObjectPool.returnObject(dbConnection2.getUrl(), dbConnection2);
            }
        }
    }
           

六、存放代理對象的對象池

如果需要在對象池中存放代理對象,common-pool2也提供了兩個現有的對象池實作類

ProxiedObjectPool

ProxiedKeyedObjectPool

,我們下面簡單介紹下ProxiedObjectPool的使用。

我們要用到jdk的對象代理機制,是以需要定義一個代表連接配接對象的接口類

DbConnectionInterface

public interface DbConnectionInterface {
    public Boolean getActive();
    public void setActive(Boolean active);
}
           

之後定義一個實作類

DbConnection

public class DbConnection implements DbConnectionInterface {

    private Boolean isActive;

    public Boolean getActive() {
        return isActive;
    }

    public void setActive(Boolean active) {
        isActive = active;
    }
}
           

為了友善觀察代理的效果,我們需要再定義一下封裝對象

MyDefaultPooledObject

,直接繼承

DefaultPooledObject

類就好了。重寫一下use()方法

public class MyDefaultPooledObject extends DefaultPooledObject<DbConnectionInterface> {
    public MyDefaultPooledObject(DbConnectionInterface object) {
        super(object);
    }

    @Override
    //代理對象被調用時,會回調這個方法
    public void use() {
        super.use();
        DbConnectionInterface object = getObject();
        System.out.println("method get " + object.getActive());
    }
}
           

最後還要定義一個生産DbConnectionInterface對象的工廠類DbConnectionFactory

public class DbConnectionFactory implements PooledObjectFactory<DbConnectionInterface> {

    @Override
    public PooledObject<DbConnectionInterface> makeObject() throws Exception {
        DbConnectionInterface dbConnection = new DbConnection();
        return new MyDefaultPooledObject(dbConnection);
    }

    @Override
    public void destroyObject(PooledObject<DbConnectionInterface> p) throws Exception {
        p.getObject().setActive(false);
    }

    @Override
    public boolean validateObject(PooledObject<DbConnectionInterface> p) {
        return p.getObject().getActive();
    }

    @Override
    public void activateObject(PooledObject<DbConnectionInterface> p) throws Exception {
        p.getObject().setActive(true);
    }

    @Override
    public void passivateObject(PooledObject<DbConnectionInterface> p) throws Exception {
        //不處理
    }
}
           

然後就可以開始使用ProxiedObjectPool對象池了

public static void main(String[] args) {
        DbConnectionFactory factory = new DbConnectionFactory();
        //這裡必須定義一個AbandonedConfig配置,并且設定useUsageTracking屬性為true
        //這樣代理對象的方法被調用時,才會去調用DefaultPooledObject的use()方法
        AbandonedConfig abandonedConfig = new AbandonedConfig();
        abandonedConfig.setUseUsageTracking(true);
        //定義一個正常對象池
        GenericObjectPool<DbConnectionInterface> pool = new GenericObjectPool<>(factory, new GenericObjectPoolConfig()
                , abandonedConfig);
        pool.setMaxTotal(); // 對象池中允許的最大對象數量
        pool.setMaxIdle();
        pool.setMaxWaitMillis();
        pool.setTimeBetweenEvictionRunsMillis();
        pool.setMinIdle();
        // 使用jdk自帶的代理機制
        JdkProxySource<DbConnectionInterface> proxySource = new JdkProxySource<>(DbConnectionInterface.class
                .getClassLoader(), new
                Class[]{DbConnectionInterface.class});
        // 加裝代理新對象池
        ProxiedObjectPool<DbConnectionInterface> proxiedPool = new ProxiedObjectPool<>(pool, proxySource);
        DbConnectionInterface connection = null;
        try {
            connection = proxiedPool.borrowObject();
            //每次代理對象被調用時,都會執行MyDefaultPooledObject裡的use方法
            connection.getActive();
            //這裡輸出class com.sun.proxy.$Proxy0,說明這是一個代理對象
            System.out.println(connection.getClass());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    proxiedPool.returnObject(connection);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

        }
    }
           

這樣,通過代理對象池取出的所有對象在調用方法時,都會經過我們定義的MyDefaultPooledObject#use()方法。

其實代理對象池的原理很簡單,就是通過jdk的代理機制為剛建立的封裝對象再建立一個代理對象,這樣放到對象池中的就是代理對象了。當代理對象的任一方法被調用時,都會觸發JdkProxyHandler#invoke()執行,也就是下面這個方法

@Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        return doInvoke(method, args);
    }

Object doInvoke(Method method, Object[] args) throws Throwable {
        validateProxiedObject();
        T object = getPooledObject();
        if (usageTracking != null) {
          //這裡的usageTracking其實就是我們的正常對象池GenericObjectPool
          //GenericObjectPool實作了UsageTracking接口
            usageTracking.use(object);
        }
        return method.invoke(object, args);
    }
           

是以每個代理對象的方法調用都會觸發GenericObjectPool#use()方法,use()方法裡面會判斷useUsageTracking是否開啟來決定是否調用poolObject#use()方法。

@Override
    public void use(T pooledObject) {
        AbandonedConfig ac = this.abandonedConfig;
        if (ac != null && ac.getUseUsageTracking()) {
            PooledObject<T> wrapper = allObjects.get(new IdentityWrapper<T>(pooledObject));
            wrapper.use();
        }
    }
           

ProxiedKeyedObjectPool

的原理也差不多,就不多做介紹了。

七、總結

總的來說,common-pool2給我們提供了5種不同類型的對象池實作來給我們使用:

1. GenericObjectPool

2. SoftReferenceObjectPool

3. GenericKeyedObjectPool

4. ProxiedObjectPool

5. ProxiedKeyedObjectPool

一般情況下,GenericObjectPool足夠滿足我們大部分的場景,而且用起來也很簡單,我們隻需要實作一個對象工廠類就可以了,可謂是開箱即用。另外

SoftReferenceObjectPool

GenericKeyedObjectPool

也有不少的使用場景,也很好用。

對于ProxiedObjectPool和ProxiedKeyedObjectPool,個人感覺并不太好用。因為代理對象隻會在方法被調用前執行一些自定義方法,而沒有在方法被調用後執行一些方法。另外,要開啟代理對象池還需要專門設定AbandonedConfig,很是麻煩。可能還不如自己基于GenericObjectPool實作一個。當然,也可能是我了解不夠,如果有不同意見的讀者歡迎指出。