myBatis原始碼解析-資料來源篇(3)
前言:我們使用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中文註釋原始碼包分享。但此註釋都是自己手寫,不能確保準確性,僅提供參考。任重而道遠,原始碼之路希望自己能堅持下來。