myBatis原始碼解析-資料來源篇(3)

超人小冰發表於2020-08-05

前言:我們使用mybatis時,關於資料來源的配置多使用如c3p0,druid等第三方的資料來源。其實mybatis內建了資料來源的實現,提供了連線資料庫,池的功能。在分析了快取和日誌包的原始碼後,接下來分析mybatis中的資料來源實現。

類圖:mybatis中關於資料來源的原始碼包路徑如下:

 

 

 

mybatis中提供了一個DataSourceFactory介面,提供了設定資料來源配置資訊,獲取資料來源方法。檢視類圖可知,有三個實現類分別提供了不同的資料來源實現。JndiDataSourceFactory,PooledDataSourceFactory,unPooledDataSourceFactory。JndiDataSourceFactory實現較簡單,此處原始碼略過。如下為各類的相互關係。

 

 

 

unPooledDataSourceFactory,PooledDataSourceFactory原始碼分析:unPooledDataSourceFactory實現了DataSourceFactory介面,實現了資料來源配置及獲取資料來源方法。

// 對外提供的資料來源工廠介面
public interface DataSourceFactory {
  // 設定配置資訊
  void setProperties(Properties props);
 // 獲取資料來源
  DataSource getDataSource();

}
// 非池化的資料來源工廠類
public class UnpooledDataSourceFactory implements DataSourceFactory {
  
  private static final String DRIVER_PROPERTY_PREFIX = "driver."; // 資料庫驅動名字首    
  private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length();

  protected DataSource dataSource; // 資料來源    

  public UnpooledDataSourceFactory() { 
    this.dataSource = new UnpooledDataSource(); // 構造一個非池化的資料來源(下文分析資料來源詳細程式碼)
  }

  public void setProperties(Properties properties) { // 對資料來源進行配置,此處設計反射包的知識(本章重點不在這,可忽略)
    Properties driverProperties = new Properties();
    MetaObject metaDataSource = SystemMetaObject.forObject(dataSource); // 將dataSource類轉為metaObject類
    for (Object key : properties.keySet()) {
      String propertyName = (String) key;
      if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) { // 若是資料庫驅動配置
        String value = properties.getProperty(propertyName);
        driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value); // driverProperties儲存資料庫驅動引數
      } else if (metaDataSource.hasSetter(propertyName)) { // 如果有set方法
        String value = (String) properties.get(propertyName);
        // 根據屬性型別進行型別的轉換,主要是 Integer, Long, Boolean 三種型別的轉換
        Object convertedValue = convertValue(metaDataSource, propertyName, value); 
        // 設定DataSource 的相關屬性值
        metaDataSource.setValue(propertyName, convertedValue);
      } else {
        throw new DataSourceException("Unknown DataSource property: " + propertyName);
      }
    }
    // 設定 DataSource.driverProerties 屬性值
    if (driverProperties.size() > 0) {
      metaDataSource.setValue("driverProperties", driverProperties);
    }
  }
  // 獲取資料來源
  public DataSource getDataSource() {
    return dataSource;
  }
 // 對Integer, Long, Boolean 三種型別的轉換
  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;
  }

}
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {

  public PooledDataSourceFactory() {
    // dataSource實現類變為PooledDataSource
    this.dataSource = new PooledDataSource();
  }

}

unPooledDataSourceFactory主要工作是對資料來源進行引數配置,並提供獲取資料來源方法。分析PooledDataSourceFactory原始碼,只是繼承unPooledDataSourceFactory,將DataSource實現類改變為PooledDataSource。

unPooledDataSource原始碼分析基本的資料來源實現都實現了DataSource介面,重寫獲取資料庫連線的方法。unPooledDataSource從類名可知,不支援資料庫連線的池化。也就是說,每來一個獲取連線請求,就新建一個資料庫連線。讓我們看原始碼驗證下。

public class UnpooledDataSource implements DataSource {
  
  private ClassLoader driverClassLoader; // 資料庫驅動類載入器
  private Properties driverProperties; // 有關資料庫驅動的引數
  private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<String, Driver>(); // 快取已註冊過的資料庫驅動

  private String driver; // 資料庫驅動
  private String url; // 資料庫名
  private String username; // 連線使用者名稱
  private String password; // 密碼

  private Boolean autoCommit; // 是否自動提交
  private Integer defaultTransactionIsolationLevel; // 事物隔離級別

  static { // 初始化
    Enumeration<Driver> drivers = DriverManager.getDrivers();  // DriverManager中已存在的資料庫驅動載入到資料庫驅動快取
    while (drivers.hasMoreElements()) {
      Driver driver = drivers.nextElement();
      registeredDrivers.put(driver.getClass().getName(), driver);
    }
  }
  .....
  
  
  public Connection getConnection() throws SQLException {
    return doGetConnection(username, password);
  }
  
  // 獲取資料庫連線
  private Connection doGetConnection(Properties properties) throws SQLException {
    initializeDriver(); // 初始化資料庫驅動
    Connection connection = DriverManager.getConnection(url, properties); // 此處每次獲取連線,就新建一個資料庫連線
    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();
        DriverManager.registerDriver(new DriverProxy(driverInstance));
        registeredDrivers.put(driver, driverInstance);
      } catch (Exception e) {
        throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
      }
    }
  }

  private void configureConnection(Connection conn) throws SQLException {
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
      conn.setAutoCommit(autoCommit);
    }
    if (defaultTransactionIsolationLevel != null) {
      conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
  } 
  ....
}

 以上程式碼是UnPooledDataSource的原始碼分析,可見,UnPooledDataSource並沒有採用池化的方法對資料庫連線進行管理。每次獲取連線,就新建一個資料庫連線。我們知道資料庫連線的建立是個非常耗時耗資源的過程,為了統一管理這些資料庫連線,mybatis為我們引入了PooledDataSource類。

PooledDataSource原始碼分析:PooledDataSource是資料來源的重點,原始碼比較複雜。PooledDataSource內部使用UnPooledDataSource類建立新的資料庫連線。PooledDataSource並不直接管理java.sql.connection連線,而是管理java.sql.connection的一個代理類PooledConnection。除了管理資料庫連線的建立,PooledDataSource內部還使用PoolState來管理資料來源的狀態(即空閒連線數,活躍連線數等)。綜上,總結如下,PooledDataSource使用UnPooledDataSource類為資料來源建立真實的資料庫連線,使用PooledConnection為資料來源管理資料庫連線,使用PoolState來為資料來源管理資料來源當前狀態。

 

 

 PoolConnection是一個connection代理類,裡面封裝了真實的連線與代理連線,現在我們先來分析PoolConnection的原始碼。

class PooledConnection implements InvocationHandler {  // 連線代理類

  private static final String CLOSE = "close";
  private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };

  private int hashCode = 0;
  private PooledDataSource dataSource; // 資料來源
  private Connection realConnection; // 被代理的真實連線
  private Connection proxyConnection; // 代理連線
  private long checkoutTimestamp; // 從連線池中取出連線的時間
  private long createdTimestamp; // 連線建立的時間
  private long lastUsedTimestamp; // 連線上次使用的時間
  private int connectionTypeCode; // 用於標註該連線所在的連線池
  private boolean valid; // 連線有效的標誌

PooledConnection實現了InvocationHandler介面,則可見是一個代理物件。檢視屬性可知,內部有真實連線與代理連線,並附帶連線的一些記錄資訊。檢視該類的構造方法。

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);  // 使用動態代理生成連線的代理類
  }

  /*
   * Invalidates the connection
   */
  // 將該連結置為無效    
  public void invalidate() {
    valid = false;
  }

檢視構造方法可知,內部除了初始化一些屬性外,還將連線的代理類也進行初始化了。那代理類究竟做了什麼,檢視重寫的invoke方法原始碼。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  // 代理方法
    String methodName = method.getName(); // 獲取方法名
    if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { // 若是close方法,則將該連線放入資料來源中
      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
          checkConnection();
        }
        return method.invoke(realConnection, args);  //執行真實的方法
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }

  private void checkConnection() throws SQLException {
    if (!valid) {
      throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
    }
  }

由原始碼可知,代理連線在執行方法時,會先檢查此連線的有效性,然後執行真實的方法。分析完PoolConnection後,對PoolState進行原始碼解析。

public class PoolState {  // 連線池狀態資訊

  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; // 失效的連線數

PoolState是對DataSource的狀態管理類,主要包括如累計連線超時時間,失效連線的獲取等一些狀態資訊的管理。除了包括一些資料庫連線的記錄資訊外,內部還維護了兩個資料庫連線的列表idleConnections,activeConnections.。分別用來存放空閒的資料庫連線列表,活躍的資料庫連線列表,針對此兩個列表的操作,下文在分析PooledDataSource時會進行詳細介紹。

對PoolConnection和PoolState分析結束後,具體分析PoolDataSource原始碼。

public class PooledDataSource implements DataSource {

  private static final Log log = LogFactory.getLog(PooledDataSource.class);

  private final PoolState state = new PoolState(this); // 維護資料來源的狀態

  private final UnpooledDataSource dataSource; // 使用UnpooledDataSource來建立真正的連線

  // OPTIONAL CONFIGURATION FIELDS
  protected int poolMaximumActiveConnections = 10; // 最大活躍的連線數
  protected int poolMaximumIdleConnections = 5;  // 最大空閒的連線數
  protected int poolMaximumCheckoutTime = 20000; // 最大checkout時間(checkOutTime指的是從資料來源中獲取連線到歸還連線的時間)
  protected int poolTimeToWait = 20000; // 最大等待時間
  protected String poolPingQuery = "NO PING QUERY SET"; // 使用該語句來驗證該連線是否有效
  protected boolean poolPingEnabled = false;
  protected int poolPingConnectionsNotUsedFor = 0;

  private int expectedConnectionTypeCode; // hashcode

檢視PoolDataSource基本屬性,可知內部使用PoolState來維護資料來源的狀態資訊,使用UnpooledDataSource來產真正的連線。並提供了一些如設定最大空閒,活躍連線數的配置資訊。作為DataSource的實現,PooledDataSource不僅提供瞭如popConnection獲取資料庫連線的介面。還提供了forceCloseAll來關閉所有資料連線。pushConnection將使用結束的資料庫連線放入資料來源中。現在開始分析第一個方法popConnection,流程圖如下,程式碼中都有詳細註釋,請耐看。

 

 

 

 // 獲取連線
  private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    PooledConnection conn = null;
    long t = System.currentTimeMillis(); // 開始取連線時間
    int localBadConnectionCount = 0; // 壞的連線數
    // 連線未取到,一直迴圈
    while (conn == null) {
      synchronized (state) { // 加鎖
        if (state.idleConnections.size() > 0) {  // 連線池中是否有空閒連線
          // 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); // 則使用unPooledDataSource新建一個連線,並封裝成代理連線PooledConnection
            @SuppressWarnings("unused")
            //used in logging, if enabled
            Connection realConn = conn.getRealConnection(); // 獲取真正的連線
            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()) {
                // 超時且關閉了自動提交,則進行回滾
                oldestActiveConnection.getRealConnection().rollback();
              }
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); // 新建一個代理連線
              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) {
          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++; // 記錄壞的連線數+1
            localBadConnectionCount++;
            conn = null; // 置為空,開始下一次迴圈
            if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
              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;
  }

經分析,獲取連線的過程為,先去查詢空閒連線列表,若存在空閒列表,則直接從空閒列表中拿出資料庫連線。若無空閒連線,則判斷當前存活的資料庫連線是否超過了指定的活躍連線數,若沒有超過,則新建資料庫連線。若超過了,則去拿活躍連線數的第一個連線判斷是否連線超時(為什麼拿第一個?因為是佇列,隊尾插入,對頭獲取,對頭的連線沒有超時,則後面的肯定沒有超時)若發現第一個連線已經超過指定的資料庫連線時間,則將此連線從活躍列表中移除,並標誌為失效,然後自己新建一個資料庫連線。若第一個連線沒有過期,則代表現在資料來源不能提供任何連線了,必須等待,直接wait,釋放鎖,等待執行緒喚醒。拿到了資料庫連線後,需要檢查該連線是否有效,若有效,則放入活躍連線列表中,並返回給使用者。

當一個連線使用完畢後,需要放回到資料來源中進行管理,現在分析pushConnection原始碼。流程圖和原始碼分析如下:

 

 

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++;
      }
    }
  }

連線使用結束後並不是立馬釋放,而是檢查當前空閒列表的連線數是否已超過指定空閒的連線數,若沒有超過,則放入到空閒連線列表中。否則將該連線設為無效。並喚醒阻塞中的獲取連線的執行緒。

當使用者指定變更資料來源配置資訊時,如資料庫地址,使用者名稱,密碼等,都需要對資料來源進行重置,清空現存的資料庫連線後修改配置資訊。現檢視清空資料來源的方法forceCloseAll原始碼。此方法較簡單,就不貼流程圖了。

//關閉池中所有的活躍連線和空閒連線 
  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.");
    }
  }

經分析,forceCloseAll對所有的空閒列表中,活躍列表中的資料庫連線全部移除並置為不可用。池中恢復到初始化狀態。

總結:本文對mybatis中的資料來源部分進行了原始碼解析。在學習原始碼的過程中,加深了對很多設計模式的理解,體會到了大神們的程式設計習慣,不僅僅是原始碼本身,更多的是思想上的理解。在學習中也知道了不急於求成,一個一個的包去分析,然後再去整和業務流程。如你對此原始碼也感興趣,可以評論下,我會把自己的mybatis中文註釋原始碼包分享。但此註釋都是自己手寫,不能確保準確性,僅提供參考。任重而道遠,原始碼之路希望自己能堅持下來。

相關文章