Mybatis源碼學習(13)-數據源模塊

一、概述

  數據源是系統中數據持久層最重要的概念之一,甚至沒有之一。數據源組件提供了系統訪問數據庫的能力。在Java中定義了數據源接口javax.sql.DataSource,常見的數據源組件一般都會實現該接口。

二、工廠模式簡介

  因爲在Mybatis的數據源模塊的代碼中,採用了工廠設計模式,工廠模式有幾種變化形式,這裏不做過多分析,只是簡單回顧一下工廠模式的概念。UML圖如下:
在這裏插入圖片描述
  通過上面的UML圖可以看出,工廠模式主要包括了四類角色,分別是:

  • 工廠接口(Creator)
    工廠接口是工廠方法模式的核心接口,調用者會直接與工廠接口交互用於獲取具體的產品實現類。
  • 具體工廠類(ConcreteCreator)
    具體工廠類是工廠接口的實現類,用於實例化產品對象,不同的具體工廠類會根據需求實例化不同的產品實現類。
  • 產品接口(Product)
    產品接口用於定義產品類的功能,具體工廠類產生的所有產品對象都必須實現該接口。調用者一般會面向產品接口進行編程,所以產品接口會與調用者直接交互,也是調用者最爲關心的接口。
  • 具體產品類(ConcreteProduct)
    實現產品接口的實現類,具體產品類中定義了具體的業務邏輯。
三、類結構

  Mybatis的數據源模塊的包目錄:org.apache.ibatis.datasource。具體包結構如下圖所示:
在這裏插入圖片描述
  其中,Mybatis數據源模塊就是採用了工廠模式。各個類在工廠模式中扮演的角色如下:

  • DataSourceFactory 是工廠接口;
  • 然後有三類數據源分別對應的目錄是unpooled、pooled、jndi,其中具體工廠實現類就在三個目錄中實現,分別是UnpooledDataSourceFactory、PooledDataSourceFactory、JndiDataSourceFactory;
  • 對應的產品接口即javax.sql.DataSource
  • 具體的產品類也分別在這三個目錄中實現,分別是UnpooledDataSource、PooledDataSource。其中,Jndi中沒有具體的產品類。
四、UnpooledDataSource數據源

下面分析三類數據源的用法和三者之間的關係。

  1. 工廠接口 DataSourceFactory
    DataSourceFactory接口扮演着工廠模式中工廠接口的角色。其中定義了兩個方法,代碼如下所示:
public interface DataSourceFactory {

  /**
   * 設置屬性,被XMLConfigBuilder所調用,是爲了將配置文件中配置的數據源屬性信息填充到DataSource中
   * @param props
   */
  void setProperties(Properties props);
  /**
   * 生產數據源,直接得到javax.sql.DataSource
   * @return
   */
  DataSource getDataSource();

}
  1. 具體工廠實現類 UnpooledDataSourceFactory
    在Mybatis中,數據源模塊一共有三個雷具體工廠實現類,分別是UnpooledDataSourceFactory、PooledDataSourceFactory、JndiDataSourceFactory。

這裏首先介紹最簡單的UnpooledDataSourceFactory。

  • 字段
  //屬性前綴
  private static final String DRIVER_PROPERTY_PREFIX = "driver.";
  //屬性前綴的長度
  private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length();
  //數據源,需要由該工廠生產的數據源
  protected DataSource dataSource;
  //構造器,實例化一個非池型數據源UnpooledDataSource,這將用於供getDataSource()方法獲取數據源實例
  public UnpooledDataSourceFactory() {
    this.dataSource = new UnpooledDataSource();
  }
  • 設置數據源驅動器屬性 setProperties()方法
    在這個方法中主要處理<dataSource>元素下對應的<property>元素,其中<property>元素又分爲兩類,一類是name的命名以driver.爲前綴命名的參數,一類是不以driver.前綴命名的參數。其中以driver.爲前綴的參數,主要是爲UnpooledDataSource對象的driverProperties字段賦值,最後通 過 DriverManager.getConnection(url,driverProperties)方法傳遞給數據庫驅動。不以driver.爲前綴的參數,主要是爲UnpooledDataSource對象的其他字段賦值,比如:driver、url、username、password等。
<dataSource type="POOLED">
 <property name="driver" value="${driver}" />
    <property name="url" value="${url}" />
    <property name="username" value="${username}" />
    <property name="password" value="${password?:123456}" />
    <property name="driver.encoding" value="UTF8"/> 
</dataSource>
@Override
  public void setProperties(Properties properties) {
    Properties driverProperties = new Properties();
    MetaObject metaDataSource = SystemMetaObject.forObject(dataSource);
    for (Object key : properties.keySet()) {
      String propertyName = (String) key;
      if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) {//處理以driver.開頭的屬性名
        String value = properties.getProperty(propertyName);
        driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value);
      } else if (metaDataSource.hasSetter(propertyName)) {//元對象中擁有針對屬性名的set設置方法
        String value = (String) properties.get(propertyName);
        Object convertedValue = convertValue(metaDataSource, propertyName, value);
        metaDataSource.setValue(propertyName, convertedValue);
      } else {
        throw new DataSourceException("Unknown DataSource property: " + propertyName);
      }
    }
    if (driverProperties.size() > 0) {
      metaDataSource.setValue("driverProperties", driverProperties);
    }
  }
  • getDataSource()方法
    該方法就實現了工廠接口中生成對象接口的方法,這裏直接返回了由構造函數生成的UnpooledDataSource的實例對象dataSource。且該實例對象可以通過setProperties()設置其中的一些參數配置。
@Override
  public DataSource getDataSource() {
    return dataSource;
  }
  • convertValue()方法
    主要實現參數類型轉換,供setProperties()方法使用。
private Object convertValue(MetaObject metaDataSource, String propertyName, String value) {
    Object convertedValue = value;
    Class<?> targetType = metaDataSource.getSetterType(propertyName);
    if (targetType == Integer.class || targetType == int.class) {
      convertedValue = Integer.valueOf(value);
    } else if (targetType == Long.class || targetType == long.class) {
      convertedValue = Long.valueOf(value);
    } else if (targetType == Boolean.class || targetType == boolean.class) {
      convertedValue = Boolean.valueOf(value);
    }
    return convertedValue;
  }
  1. 具體產品類 UnpooledDataSource
    在Mybatis的數據源模塊中,工廠模式中對應的產品接口是javax.sql.DataSource,而UnpooledDataSource是具體的產品實現類。
public class UnpooledDataSource implements DataSource {
 //省略
}
  • 字段
    在UnpooledDataSource的字段中,分爲了兩類,其中一類是driverProperties,即這類屬性在設置的時候鍵一般都會是以“driver.”爲前綴,即在前面提到的,主要用來配置驅動器屬性。剩下的就是另一類,每個字段的含有,在註解中已經表明就不在重複描述。
//這個是數據庫的驅動類加載器,目的是從磁盤中加載數據庫驅動類到內存
  private ClassLoader driverClassLoader;
  //驅動器屬性,這個用於保存我們手動設置的數據庫驅動器屬性,如果我們不進行設置,則會使用默認值。這個屬性設置的鍵一般都會是以“driver.”爲前綴。
  private Properties driverProperties;
  //表示數據庫驅動註冊器,其內部保存着所有已註冊的數據庫驅動類實例,這個字段是static修飾的,表示在數據源類加載的時候就會創建,這個時候創建的其實是個空集合。再者使用ConcurrentHashMap集合,這是一個線程安全的鍵值對型集合,
  private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<String, Driver>();
  //數據庫驅動類名
  private String driver;
  //數據庫服務器URL地址
  private String url;
  //數據庫服務器連接用戶名
  private String username;
  //數據庫服務器連接密碼
  private String password;
  //是否自動提交,這是一個邏輯值,真表示使用自動提交,假表示關閉自動提交
  private Boolean autoCommit;
  //表示默認的事務級別
  private Integer defaultTransactionIsolationLevel;
  • 靜態代碼塊,初始化驅動器。
    該靜態代碼塊的作用就是爲registeredDrivers集合賦值,保證在該類加載的時候就將默認的驅動器實例加載到靜態集合中以備用
static {
	//DriverManager是JDK提供的驅動器管理類
	//這裏調用DriverManager中getDrivers()方法,將會獲取DriverManager中在集合registeredDrivers中的保存的驅動器實例,
	//在獲取的時候會進行類加載驗證,驗證的目的是確保使用本類加載器獲取的驅動器類與在registeredDrivers中保存的對應驅動類實例的類是
	//同一類型(==)。最後獲取到的是驅動實例的枚舉。
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
      Driver driver = drivers.nextElement();
      registeredDrivers.put(driver.getClass().getName(), driver);
    }
  }
  • 構造函數
    UnpooledDataSource提供了一系列的構造函數,主要用於實例化。
/**
   * 無參構造器,這個構造器在數據源工廠的無參構造器中被調用,用於創建一個空的UnpolledDataSource實例,
   * 然後使用工廠類中的setProperties()方法,爲這個空實例中的各個字段進行賦值
   * 
   */
  public UnpooledDataSource() {
  }
  /**
   * 構造器
   */
  public UnpooledDataSource(String driver, String url, String username, String password) {
    this.driver = driver;
    this.url = url;
    this.username = username;
    this.password = password;
  }
  /**
   * 構造器
   */
  public UnpooledDataSource(String driver, String url, Properties driverProperties) {
    this.driver = driver;
    this.url = url;
    this.driverProperties = driverProperties;
  }
  /**
   * 構造器
   */
  public UnpooledDataSource(ClassLoader driverClassLoader, String driver, String url, String username, String password) {
    this.driverClassLoader = driverClassLoader;
    this.driver = driver;
    this.url = url;
    this.username = username;
    this.password = password;
  }
  /**
   * 構造器
   */
  public UnpooledDataSource(ClassLoader driverClassLoader, String driver, String url, Properties driverProperties) {
    this.driverClassLoader = driverClassLoader;
    this.driver = driver;
    this.url = url;
    this.driverProperties = driverProperties;
  }
  • 提供獲取真正數據源的方法 getConnection()方法
    getConnection()方法是獲取真正數據源的方法,其中是通過doGetConnection()方法實現真正數據源的實例化工作。下面分別介紹其中涉及到的方法。
/**
   * 獲取數據源連接,通過調用方法doGetConnection()實現,該方法是真正執行數據庫連接並獲取這個連接的實現。
   */
  @Override
  public Connection getConnection() throws SQLException {
    return doGetConnection(username, password);
  }
  /**
   * 獲取數據源連接,通過調用方法doGetConnection()實現,該方法是真正執行數據庫連接並獲取這個連接的實現。
   */
  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return doGetConnection(username, password);
  }

doGetConnection(String username, String password)方法主要是處理參數,真正的工作交給了doGetConnection(Properties properties)方法。

private Connection doGetConnection(String username, String password) throws SQLException {
    Properties props = new Properties();
    if (driverProperties != null) {
      props.putAll(driverProperties);
    }
    if (username != null) {
      props.setProperty("user", username);
    }
    if (password != null) {
      props.setProperty("password", password);
    }
    return doGetConnection(props);
  }

在doGetConnection(Properties properties)方法中,真正實現了java.sql.Connection對象的實例化工作。主要分爲了三步:

  1. 第一步:通過 initializeDriver();完成了驅動器的初始化工作。
  2. 第二步:通過調用DriverManager的getConnection方法來獲取數據庫連接connection。其中參數包括了用戶名、密碼、數據庫連接屬性等。
  3. 調用configureConnection()方法進行數據庫連接的配置,配置內容:自動提交(boolean值)與事務級別
 private Connection doGetConnection(Properties properties) throws SQLException {
	//驅動器初始化
    initializeDriver();
    //通過調用DriverManager的getConnection方法來獲取數據庫連接connection
    Connection connection = DriverManager.getConnection(url, properties);
    //調用configureConnection()方法進行數據庫連接的配置,配置內容:自動提交(boolean值)與事務級別
    configureConnection(connection);
    return connection;
  }

主要完成了驅動器的初始化工作。

private synchronized void initializeDriver() throws SQLException {
    if (!registeredDrivers.containsKey(driver)) {
      Class<?> driverType;
      try {
        if (driverClassLoader != null) {
          driverType = Class.forName(driver, true, driverClassLoader);
        } else {
          driverType = Resources.classForName(driver);
        }
        // DriverManager requires the driver to be loaded via the system ClassLoader.
        // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
        Driver driverInstance = (Driver)driverType.newInstance();
        //DriverProxy,是一個驅動代理,這個類是以靜態代理類的方式定義的,其實現了Driver接口,實現了Driver接口中的所有抽象方法,
        //是一個真正的代理類,代理Driver真正的實現類,即真正起作用的驅動類實例,代理類將驅動類的所有方法全部保護起來
        DriverManager.registerDriver(new DriverProxy(driverInstance));
        registeredDrivers.put(driver, driverInstance);
      } catch (Exception e) {
        throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
      }
    }
  }

該方法主要進行數據庫連接的配置,配置內容:自動提交(boolean值)與事務級別。

private void configureConnection(Connection conn) throws SQLException {
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
      conn.setAutoCommit(autoCommit);
    }
    if (defaultTransactionIsolationLevel != null) {
      conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
  }
五、PooledDataSource 數據源

  數據庫連接的創建過程是非常耗時的,數據庫能夠建立的連接數也非常有限,所以在大多數場景下,數據庫連接是非常珍貴的資源,爲了減少在創建數據庫連接的過程中的消耗,就引入了數據庫連接池的概念。PooledDataSource實現了簡易數據庫連接池的功能。其中爲了實現數據庫連接的池化,引入了兩個兩個對象,通過三個類實現了數據庫連接的池化。分別是:

  • PooledConnection
    封裝了真正的數據庫連接對象( java.sql.Connection )以及其代理對象。
  • PoolState
    用於管理PooledConnection對象狀態的。
  • PooledDataSource
    PooledDataSource 主要用來管理PooledConnection對象。
  1. PooledDataSourceFactory 具體工廠類
    PooledDataSourceFactory 具體 工廠類繼承於UnpooledDataSourceFactory工廠類,兩者基本沒有區別,唯一的不同是在PooledDataSourceFactory 構造函數中生成的是PooledDataSource數據源實例。代碼如下:
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
  /**
   * 構造器,初始化一個池化數據源的實例
   */
  public PooledDataSourceFactory() {
    this.dataSource = new PooledDataSource();
  }
}
  1. PooledConnection類
    封裝了真正的數據庫連接對象( java.sql.Connection )以及其代理對象。PooledConnection 繼承了InvocationHandler接口,如下代碼所示,即該類採用了JDK動態代理。
class PooledConnection implements InvocationHandler {
     //略
}
  • 字段
    其中,dataSource字段用來記錄當前PooledConnection對象所在的PooledDataSource對象,該PooledConnection 是從該PooledDataSource中獲取的。realConnection表示真正的數據庫連接。proxyConnection表示使用JDK動態代理創建的代理真實連接。connectionTypeCode表示由數據庫URL 、username、password計算出來的hash值,可用於標識該連接所在的連接池。其他字段,下面註釋已經表明,不再分別描述。同時還提供了相應的setter/getter方法,比較簡單,不再貼出代碼了。
  //靜態字符串常量,表示關閉
  private static final String CLOSE = "close";
  //靜態數組常量,表示連接類型,這裏使用到了多態的概念,Connection是所有連接的祖類,
  private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };
  //數據庫連接(真實連接)的HashCode值,默認爲0,表示當真實連接不存在即爲null時的值
  private final int hashCode;
  //池型數據源,爲了方便調用其內部定義的部分方法來輔助完成池型連接的一些功能判斷
  private final PooledDataSource dataSource;
  //真實的數據庫連接,屬於被代理的對象
  private final Connection realConnection;
  //使用JDK動態代理創建的代理真實連接的 
  private final Connection proxyConnection;
  //數據庫連接被檢出的時間戳,這個用於計算具體的檢出時間
  private long checkoutTimestamp;
  //數據庫連接被創建的時間戳,用於計算數據庫連接被創建的時間
  private long createdTimestamp;
  //連接被最後使用的時間戳,用於計算數據庫連接被最後使用的時間
  private long lastUsedTimestamp;
  //數據庫連接的類型編碼,格式爲:url+username+password
  private int connectionTypeCode;
  //表示連接是否可用的邏輯值
  private boolean valid;
  • 構造函數
    有參構造函數,主要用來實例化對象,並初始化部分屬性字段。
public PooledConnection(Connection connection, PooledDataSource dataSource) {
    this.hashCode = connection.hashCode();
    this.realConnection = connection;
    this.dataSource = dataSource;
    this.createdTimestamp = System.currentTimeMillis();
    this.lastUsedTimestamp = System.currentTimeMillis();
    this.valid = true;
    //創建代理連接實例
    this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
  }
  • hashCode()方法
    重寫了hashCode()方法,主要是使用真正的數據庫連接的hashCode作爲該實例對象的hashCode,且該值已經在構造函數中進行了初始化。
 @Override
  public int hashCode() {
    return hashCode;
  }
  • equals()方法
    重寫了equals()方法,主要用來把當前數據庫連接與其他連接進行比較,驗證是不是同一個連接,其實就是根據hashCode來判斷是否是同一個連接。
@Override
  public boolean equals(Object obj) {
    if (obj instanceof PooledConnection) {//如果是PooledConnection類型,則比較真實連接是否一樣
      return realConnection.hashCode() == ((PooledConnection) obj).realConnection.hashCode();
    } else if (obj instanceof Connection) {//如果是Connection,則比較連接的HashCode值
      return hashCode == obj.hashCode();
    } else {
      return false;
    }
  }
  • 是否失效invalidate()、checkConnection() 方法
    PooledConnection提供了一個設置失效的方法invalidate(),同時也提供了一個樣式否是失效的方法checkConnection()。
public void invalidate() {
    valid = false;
  }
private void checkConnection() throws SQLException {
  if (!valid) {
    throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
  }
}`
  • invoke()方法
    InvocationHandler接口中定義的方法,是JDK動態代理中最核心的方法之一。
    這個方法主要做了兩件事,
  1. 當對close()方法進行調用時,實際上是通過dataSource.pushConnection(this);方法,實現釋放連接,把連接放回連接池中,而不是銷燬連接。
  2. 當時不是close()方法的時候,交由真正的realConnection對象來執行該方法。
    其中,dataSource對象的pushConnection()方法會在後面詳細介紹。
@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    //如果關閉當前連接,則當前連接會被重新添加到連接池中
    if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
      dataSource.pushConnection(this);
      return null;
    } else {//反之
      try {
        if (!Object.class.equals(method.getDeclaringClass())) {//判斷這個方法是不是來自Object類
          // issue #579 toString() should never fail
          // throw an SQLException instead of a Runtime
          //除了toString()方法,其他方法調用之前要檢查connection是否還是合法的,不合法要拋出SQLException
          checkConnection();
        }
        //其他的方法,則交給真正的connection去調用
        return method.invoke(realConnection, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }
  1. PoolState類
    用於管理PooledConnection對象狀態的。該實例對象通過idleConnections(List<PooledConnection>)和activeConnections(List<PooledConnection>)兩個集合,分別管理了空閒狀態的鏈接和活動狀態的鏈接。下面詳細介紹該類。
  • 字段
    除了idleConnections和activeConnections兩個字段,dataSource字段表示該狀態管理的池化的數據源對象,其他還定義了一些系列用於統計的字段,各個字段的含有下面代碼註釋已經描述,不再重複。同時還提供了這些統計字段相應的setter/getter方法,比較簡單,不再貼出代碼了。
 //池化數據源
  protected PooledDataSource dataSource;
  //空閒的連接
  protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>();
  //活動的連接
  protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>();
  //請求次數
  protected long requestCount = 0;
  //總請求時間
  protected long accumulatedRequestTime = 0;
  //總檢出時間
  protected long accumulatedCheckoutTime = 0;
  //過期連接數量
  protected long claimedOverdueConnectionCount = 0;
  //過期連接累計檢出時間
  protected long accumulatedCheckoutTimeOfOverdueConnections = 0;
  //總等待時間
  protected long accumulatedWaitTime = 0;
  //要等待的次數
  protected long hadToWaitCount = 0;
  //壞的連接次數
  protected long badConnectionCount = 0;
  • 構造函數
    構造函數,主要是初始化實例對象,同時在構建實例對象的時候初始化了對應的池化數據源對象。
public PoolState(PooledDataSource dataSource) {
    this.dataSource = dataSource;
  }
  1. PooledDataSource類
    PooledDataSource主要用來管理PooledConnection對象。通過PoolState和UnpooledDataSource實例對象,實現了一個具有池化功能的數據源。其中,UnpooledDataSource非池化數據源,用來生成真實的數據庫連接對象;PoolState用來管理數據池和池中連接的狀態。下面分別介紹該類的組成部分:
  • 字段
    其中,通過PoolState和UnpooledDataSource實例對象,實現了一個具有池化功能的數據源。其他字段主要是用來定義池化數據源的一些默認配置信息或者數據源標識信息,下面代碼註釋已經很清楚了,不再重複描述。相關的getter/setter方法也比較簡單,不再提出代碼了。
  //連接池狀態
  private final PoolState state = new PoolState(this);
  //非池化數據源,一個池型數據源就是一個非池型數據源加上一個連接池,也就是說,池型數據源是在非池型數據源基礎上擴展而來
  private final UnpooledDataSource dataSource;

  // OPTIONAL CONFIGURATION FIELDS
  //連接池中最多可擁有的活動連接數
  protected int poolMaximumActiveConnections = 10;
  //連接池中最多可擁有的空閒連接數
  protected int poolMaximumIdleConnections = 5;
  //連接池最大檢出時間,如果一個連接驗證時間超過設定值,則將這個連接設置爲過期(發生在推出連接時)
  protected int poolMaximumCheckoutTime = 20000;
  //池等待時間,當需要從池中獲取一個連接時,如果空閒連接數量爲0,而活動連接的數量也達到了最大值,
  //那麼就針對那個最早取出的連接進行檢查驗證(checkout),如果驗證成功(即在上面poolMaximumCheckoutTime限定的時間內驗證通過),
  //說明這個連接還處於使用狀態,這時取出操作暫停,線程等待限定時間,這個限定時間就是這個參數的使用位置。
  protected int poolTimeToWait = 20000;
  //這是一個關於壞連接容忍度的底層設置, 作用於每一個嘗試從緩存池獲取連接的線程. 如果這個線程獲取到的是一個壞的連接,那麼這個數據源允許這 
  //個線程嘗試重新獲取一個新的連接,但是這個重新嘗試的次數不應該超過 poolMaximumIdleConnections 與 poolMaximumLocalBadConnectionTolerance 之和。
  //默認值:3 (Since: 3.4.5)
  protected int poolMaximumLocalBadConnectionTolerance = 3;
  // 發送到數據庫的偵測查詢,用來檢驗連接是否處在正常工作秩序中並準備接受請求。默認是“NO PING QUERY SET”,
  //這會導致多數數據庫驅動失敗時帶有一個恰當的錯誤消息。
  protected String poolPingQuery = "NO PING QUERY SET";
  //是否啓用偵測查詢。若開啓,也必須使用一個可執行的 SQL 語句設置 poolPingQuery 屬性(最好是一個非常快的 SQL),默認值:false。
  protected boolean poolPingEnabled;
  //配置 poolPingQuery 的使用頻度。這可以被設置成匹配具體的數據庫連接超時時間,來避免不必要的偵測,默認值:0
  //(即所有連接每一時刻都被偵測 — 當然僅當 poolPingEnabled 爲 true 時適用)。
  protected int poolPingConnectionsNotUsedFor;
  //連接的類型編碼,這個類型編碼在創建池型數據源實例的時候會被組裝,他的組裝需要從數據源中獲取連接的url、username、password三個值,
  //將其按順序組合在一起,這個類型編碼可用於區別連接種類。
  private int expectedConnectionTypeCode;
  • 構造函數
    提供了多個構造函數,所有的構造函數都提供了構建對象、初始化dataSource、expectedConnectionTypeCode 等字段值的功能。其中expectedConnectionTypeCode 字段的賦值,是根據url、username、password三個參數,通過assembleConnectionTypeCode()方法,獲取對應的hashCode值。
 public PooledDataSource() {
    dataSource = new UnpooledDataSource();
  }
 
  public PooledDataSource(UnpooledDataSource dataSource) {
    this.dataSource = dataSource;
  }

  public PooledDataSource(String driver, String url, String username, String password) {
    dataSource = new UnpooledDataSource(driver, url, username, password);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }

  public PooledDataSource(String driver, String url, Properties driverProperties) {
    dataSource = new UnpooledDataSource(driver, url, driverProperties);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }

  public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, String username, String password) {
    dataSource = new UnpooledDataSource(driverClassLoader, driver, url, username, password);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }

  public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, Properties driverProperties) {
    dataSource = new UnpooledDataSource(driverClassLoader, driver, url, driverProperties);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }
private int assembleConnectionTypeCode(String url, String username, String password) {
    return ("" + url + username + password).hashCode();
  }
  • getConnection()方法、popConnection()方法
    PooledDataSource類提供了兩個getConnection()方法用於獲取真正的數據源連接。底層其實是首先通過popConnection()方法實現獲取取PooledConnection對象,然後通過PooledConnection.getProxyConnection ()方法獲取數據庫連接的代理對象proxyConnection。下面詳細介紹popConnection()方法的實現:
  1. 這是個同步方法,線程安全。但是將synchronized鎖同步放置到循環內部,而不是循環之外的原因是因爲:如果將同步鎖放置在循環之外,當多個線程執行到鎖的位置,其中一個線程獲得鎖然後開始執行循環,如果發生問題導致無限循環,那麼這個鎖將是一直被這個線程所持有,導致其他線程永久處於等待鎖的狀態,程序無法執行下去。而將鎖放置到循環內部,當多個線程來到鎖之前,其中一個線程獲得鎖,執行循環內部代碼,當執行完成一次循環,無論成功失敗,都會釋放鎖,而其他線程就可以獲得鎖進而執行。
  2. 首先驗證空閒連接集合是否爲空(驗證是否還有空閒連接備用),如果存在空閒連接,那麼直接獲取這個空閒連接,將這個連接從空閒連接集合中刪除。
  3. 如果沒有空閒連接,那麼就驗證活動連接集合中連接的數量是否達到最大值(poolMaximumActiveConnections),如果未達到最大值,這時,我們可以直接創建一個新的池型連接(需要一個真實連接於與一個池型數據源實例作爲參數)
  4. 如果活動連接集合中的連接數目已經達到最大值(poolMaximumActiveConnections),那麼就針對最早的那個活動連接(即在集合中排在0位的那個連接實例)進行驗證。並獲取其驗證時間間隔值(該連接上一次記錄驗證時間戳到當前時間的間隔),將其與池連接的最大驗證時限(poolMaximumCheckoutTime)進行比較,如果前者大,說明針對這個連接距上一次記錄驗證時間戳的時間超過了限定時限,這時將這個老連接從活動連接集合中刪除,並新建一個池連接,還以老連接所代理的真實連接爲真實連接(實際上就是創建一個新的代理),並將老的池連接設爲無效。
  5. 如果驗證時間與顯示時間比較結果爲驗證時間小於限定時限(這個限定時限的設置需要根據項目實際情況來設置,或通過經驗來設置,確保在這個時間之內連接的數據庫操作執行完畢,不然貿然將連接關閉會導致原本的數據庫操作失敗),說明這個連接還可能處於使用狀態,這時候只有等待一途,這裏將線程設置等待限定秒數(poolTimeToWait),線程進入等待狀態,那麼就會釋放同步鎖,此時其他線程就能獲得鎖來進行執行。當前線程在等待N秒之後自動進入準備狀態準備重新獲得鎖。
  6. 然後就獲得的連接進行判斷,如果連接不爲空,那麼驗證連接是否可用(isValid),如果連接可用則設置連接類型編碼,並記錄驗證時間戳(setCheckoutTimestamp)與最後一次使用時間戳(setLastUsedTimestamp),這兩個時間戳可用於計算該連接的驗證時間與最後一次使用時間,在前面會使用到這些值進行判斷。再然後將該鏈接添加到活動連接集合中。
  7. 如果獲取的連接爲空,或者說沒有獲取到連接,則壞連接數加1,將連接置null,並驗證壞連接數值,如果比當前空閒連接數量+3都大的話,那麼就放棄獲取連接,並拋出SqlException,(拋出異常也就意味着執行的終止,這個線程將不再執行循環操作)
@Override
  public Connection getConnection() throws SQLException {
    return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
  }
  
  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return popConnection(username, password).getProxyConnection();
  }
private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;
    //最外面是while死循環,如果一直拿不到connection,則不斷嘗試
    while (conn == null) {
      //這是個同步方法,線程安全。但是將synchronized鎖同步放置到循環內部,而不是循環之外的原因是因爲:
    	//如果將同步鎖放置在循環之外,當多個線程執行到鎖的位置,其中一個線程獲得鎖然後開始執行循環,如果發生問題導致無限循環,
    	//那麼這個鎖將是一直被這個線程所持有,導致其他線程永久處於等待鎖的狀態,程序無法執行下去。而將鎖放置到循環內部,
    	//當多個線程來到鎖之前,其中一個線程獲得鎖,
    	//執行循環內部代碼,當執行完成一次循環,無論成功失敗,都會釋放鎖,而其他線程就可以獲得鎖進而執行。
      synchronized (state) {
        if (!state.idleConnections.isEmpty()) {//如果有空閒的連接的話,刪除空閒列表裏第一個,並返回
          // Pool has available connection
          conn = state.idleConnections.remove(0);
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {//如果沒有空閒的連接
          // Pool does not have available connection
          //如果當前活動連接數小於最大活動連接,就創建一個池化連接
          if (state.activeConnections.size() < poolMaximumActiveConnections) {
            // Can create new connection
            conn = new PooledConnection(dataSource.getConnection(), this);
            if (log.isDebugEnabled()) {
              log.debug("Created connection " + conn.getRealHashCode() + ".");
            }
          } else {//如果當前活動連接數大於最大活動連接,暫時不能創建活動連接,需要等待
            // Cannot create new connection
        	//獲取最早創建的活動連接
            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
            //獲取該連接的檢出時間
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
            if (longestCheckoutTime > poolMaximumCheckoutTime) {//當該連接的檢出時間大於設定的最大檢出時間時
              // Can claim overdue connection
              //過期連接數加一
              state.claimedOverdueConnectionCount++;
              //累計過期連接檢出時間添加當前連接的檢出時間
              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
              //總檢出時間添加當前連接的檢出時間
              state.accumulatedCheckoutTime += longestCheckoutTime;
              //從活動連接隊列中移除當前活動連接
              state.activeConnections.remove(oldestActiveConnection);
              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {//如果當前連接是非自動提交的配置,需要手動進行回滾。
                try {
                  oldestActiveConnection.getRealConnection().rollback();
                } catch (SQLException e) {
                  /*
                     Just log a message for debug and continue to execute the following
                     statement like nothing happend.
                     Wrap the bad connection with a new PooledConnection, this will help
                     to not intterupt current executing thread and give current thread a
                     chance to join the next competion for another valid/good database
                     connection. At the end of this loop, bad {@link @conn} will be set as null.
                   */
                  log.debug("Bad connection. Could not roll back");
                }  
              }
              //根據原有連接,創建一個新的連接
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
              //設置連接創建時間(使用原來連接的時間)
              conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
             //設置連接最後使用時間(使用原來連接的時間)
              conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
              //設置原來連接無效
              oldestActiveConnection.invalidate();
              if (log.isDebugEnabled()) {
                log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
              }
            } else {//當該連接的檢出時間小於設定的最大檢出時間時,就必須等待
              // Must wait
              try {
                if (!countedWait) {
                  //等待連接數加一
                  state.hadToWaitCount++;
                  //表示當前連接已經在等待中
                  countedWait = true;
                }
                if (log.isDebugEnabled()) {
                  log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                }
                long wt = System.currentTimeMillis();
                state.wait(poolTimeToWait);
                //總等待時間累加
                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
              } catch (InterruptedException e) {
                break;
              }
            }
          }
        }
        if (conn != null) {
          // ping to server and check the connection is valid or not
          if (conn.isValid()) {//連接有效時
            if (!conn.getRealConnection().getAutoCommit()) {
              //連接是非自動提交時,把連接進行回滾操作
              conn.getRealConnection().rollback();
            }
            //設置編碼類型
            conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
            //設置連接檢出時間
            conn.setCheckoutTimestamp(System.currentTimeMillis());
            //設置連接最後使用實際
            conn.setLastUsedTimestamp(System.currentTimeMillis());
            state.activeConnections.add(conn);
            //請求次數加一
            state.requestCount++;
            //請求總時間累加
            state.accumulatedRequestTime += System.currentTimeMillis() - t;
          } else {//連接無效時
            if (log.isDebugEnabled()) {
              log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
            }
            //壞連接數加一
            state.badConnectionCount++;
            localBadConnectionCount++;
            conn = null;
            //當壞連接次數大於 空閒連接數和壞連接容忍設置數時,拋出異常
            if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
              if (log.isDebugEnabled()) {
                log.debug("PooledDataSource: Could not get a good connection to the database.");
              }
              throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
            }
          }
        }
      }

    }

    if (conn == null) {//當獲取不到連接時,拋出異常。
      if (log.isDebugEnabled()) {
        log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
      }
      throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    }

    return conn;
  }
  • forceCloseAll()方法
    關閉所有的activeConnections和idleConnections。 使用場景:連接是在數據源完全設置完整的情況下才生成的,數據源就是連接生成的基礎,當我們要修改數據源的基礎屬性的時候,原有設置上產生的連接必定不再適合新的設置,需要全部推倒重來。
public void forceCloseAll() {
    synchronized (state) {
      expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
      for (int i = state.activeConnections.size(); i > 0; i--) {//關閉所有活動的連接
        try {
          PooledConnection conn = state.activeConnections.remove(i - 1);
          conn.invalidate();

          Connection realConn = conn.getRealConnection();
          if (!realConn.getAutoCommit()) {
            realConn.rollback();
          }
          realConn.close();
        } catch (Exception e) {
          // ignore
        }
      }
      for (int i = state.idleConnections.size(); i > 0; i--) {//關閉所有空閒的連接
        try {
          PooledConnection conn = state.idleConnections.remove(i - 1);
          conn.invalidate();

          Connection realConn = conn.getRealConnection();
          if (!realConn.getAutoCommit()) {
            realConn.rollback();
          }
          realConn.close();
        } catch (Exception e) {
          // ignore
        }
      }
    }
    if (log.isDebugEnabled()) {
      log.debug("PooledDataSource forcefully closed/removed all connections.");
    }
  }
  • pushConnection()方法
  1. 這個方法同樣是一個同步方法,擁有同步鎖,以池狀態實例爲鎖。
  2. 首先我們將當前要推入的連接實例從活動連接中刪除,表示其不再處於使用狀態。
  3. 然後對連接額可用性進行(valid)判斷,如果還處於可用狀態,則驗證空閒連接集合中的空閒連接數量是否小於設置的限定值(poolMaximumIdleConnections)和當前連接實例的類型編碼是否與當前池型數據源中的連接類型編碼一致,如果上面兩點都滿足,則進行下一步:
  4. 新建一個池型連接實例並將其添加到空閒連接集合中,這個池型連接實例是以之前要推入的連接爲基礎重新創建的,也就是說是針對那個要推入的池型連接的真實連接重新創建一個池型連接代理(只改變外包裝,實質不改變),並將原池型連接的時間戳設置統統設置到新的連接中,保持連接的持續性,然後將原池型連接置爲無效。
  5. 然後喚醒所有沉睡線程notifyAll()。
  6. 如果第(3)點中的判斷中有一個不成立(空閒連接數量達到最大值或者連接的類型編碼不一致)那麼直接將該連接的真實連接關閉,池連接置爲無效即可。
protected void pushConnection(PooledConnection conn) throws SQLException {

    synchronized (state) {
      //活動連接隊列中移除該連接
      state.activeConnections.remove(conn);
      if (conn.isValid()) {//連接有效時
    	//當前空閒連接隊列中連接數量小於最大空閒連接數且連接的類型編碼符合要求
        if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
          //累計連接檢出時間增加
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          if (!conn.getRealConnection().getAutoCommit()) {//回滾連接
            conn.getRealConnection().rollback();
          }
          //重新歡迎該連接,並添加到空閒連接隊列中
          PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
          state.idleConnections.add(newConn);
          newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
          newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
          conn.invalidate();
          if (log.isDebugEnabled()) {
            log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
          }
          state.notifyAll();
        } else {//其他情況,該連接需要真正的被關閉
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          if (!conn.getRealConnection().getAutoCommit()) {//回滾
            conn.getRealConnection().rollback();
          }
          //真正的關閉連接
          conn.getRealConnection().close();
          if (log.isDebugEnabled()) {
            log.debug("Closed connection " + conn.getRealHashCode() + ".");
          }
          conn.invalidate();
        }
      } else {//無效連接時,拋出異常,壞連接數加一
        if (log.isDebugEnabled()) {
          log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
        }
        state.badConnectionCount++;
      }
    }
  }
  • pingConnection()方法
    檢查連接是否可用。該方法會被池連接PooledConnection類中的isValid()方法調用,用於判斷一個連接是否還可用。這個方法就是真正用於判斷連接可用與否的功能性方法。
  1. 首先創建一個局部變量result用於保存判斷結果,默認爲true
  2. 然後將當前池型連接包裹的真實連接的開閉狀態值的非值賦值給result(當真實連接處於關閉狀態時,result值爲false,當真實連接處於開啓狀態時,result值爲true),如果賦值過程出現了異常,則直接將result置false
  3. 判斷result的值,如果result值爲true,則判斷poolPingEnabled的值,這是偵測查詢的開關,如果這個值爲true,表示開啓偵測查詢,那麼就可以執行以下內容。
  4. 判斷poolPingConnectionsNotUsedFor的值是否大於等於0(這個判斷的意思是判斷是否設置了正確的poolPingConnectionsNotUsedFor值),並且判斷該連接的自最後一次使用以來的時間間隔是否大於設定的poolPingConnectionsNotUsedFor值(驗證該連接是否到了需要進行偵測查詢的時間,如果小於設置時間則不進行偵測查詢)
  5. 如果上述條件均滿足,則進行一次偵測查詢,這個偵測查詢就是針對這個連接的一個測試查詢,看看整個查詢過程是否通暢,若通暢(沒有任何異常出現),則將result置爲true,一旦測試過程出現了異常,則將該連接的真實連接關閉,並將result置爲false
protected boolean pingConnection(PooledConnection conn) {
	//連接是否可用的標識
    boolean result = true;

    try {
      //連接是否被關閉,關閉則表示連接不可用,否則,可用
      result = !conn.getRealConnection().isClosed();
    } catch (SQLException e) {//發送異常,說明不可用
      if (log.isDebugEnabled()) {
        log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
      }
      result = false;
    }

    if (result) {
      if (poolPingEnabled) {//如果開啓連接可用偵查
    	//偵探檢查評率在範圍內時
        if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
          try {
            if (log.isDebugEnabled()) {
              log.debug("Testing connection " + conn.getRealHashCode() + " ...");
            }
            //構建進行偵探的sql語句,如果測試正常,說明連接可用
            Connection realConn = conn.getRealConnection();
            Statement statement = realConn.createStatement();
            ResultSet rs = statement.executeQuery(poolPingQuery);
            rs.close();
            statement.close();
            if (!realConn.getAutoCommit()) {
              realConn.rollback();
            }
            result = true;
            if (log.isDebugEnabled()) {
              log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
            }
          } catch (Exception e) {//測試連接異常時,說明連接不可用
            log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());
            try {
              conn.getRealConnection().close();
            } catch (Exception e2) {
              //ignore
            }
            result = false;
            if (log.isDebugEnabled()) {
              log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
            }
          }
        }
      }
    }
    return result;
  }

六、JNDI 數據源

Jndi數據源和前面兩個不太一樣,JNDI數據源是託管型數據源,這個數據源的實現是爲了使用如 Spring 或應用服務器這類的容器, 容器可以集中或在外部配置數據源,然後放置一個 JNDI 上下文的引用。從代碼結構上來說,沒有單獨的產品實現類。其實是因爲,在Jndi數據源中,該數據源在JndiDataSourceFactory的setProperties()方法中,通過加載應用上下文,然後直接生產了對應數據源,不需要在單獨定義數據源類。代碼如下所示:

public class JndiDataSourceFactory implements DataSourceFactory {

  public static final String INITIAL_CONTEXT = "initial_context";
  public static final String DATA_SOURCE = "data_source";
  //和其他數據源配置相似, 它也可以通過名爲 “env.” 的前綴直接向初始上下文發送屬性。 比如:
  //env.encoding=UTF8
  public static final String ENV_PREFIX = "env.";

  private DataSource dataSource;

  @Override
  public void setProperties(Properties properties) {
    try {
      InitialContext initCtx;
      Properties env = getEnvProperties(properties);
      if (env == null) {
        initCtx = new InitialContext();
      } else {
        initCtx = new InitialContext(env);
      }

      if (properties.containsKey(INITIAL_CONTEXT)
          && properties.containsKey(DATA_SOURCE)) {
        Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
        dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
      } else if (properties.containsKey(DATA_SOURCE)) {
        dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
      }

    } catch (NamingException e) {
      throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e);
    }
  }

  @Override
  public DataSource getDataSource() {
    return dataSource;
  }

  private static Properties getEnvProperties(Properties allProps) {
    final String PREFIX = ENV_PREFIX;
    Properties contextProperties = null;
    for (Entry<Object, Object> entry : allProps.entrySet()) {
      String key = (String) entry.getKey();
      String value = (String) entry.getValue();
      if (key.startsWith(PREFIX)) {
        if (contextProperties == null) {
          contextProperties = new Properties();
        }
        contextProperties.put(key.substring(PREFIX.length()), value);
      }
    }
    return contextProperties;
  }

}

發佈了52 篇原創文章 · 獲贊 3 · 訪問量 3906
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章