Mybaits 原始碼解析 (七)----- Select 語句的執行過程分析(下篇)全網最詳細,沒有之一
我們上篇文章講到了查詢方法裡面的doQuery方法,這裡面就是呼叫JDBC的API了,其中的邏輯比較複雜,我們這邊文章來講,先看看我們上篇文章分析的地方
SimpleExecutor
1 public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { 2 Statement stmt = null; 3 try { 4 Configuration configuration = ms.getConfiguration(); 5 // 建立 StatementHandler 6 StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); 7 // 建立 Statement 8 stmt = prepareStatement(handler, ms.getStatementLog()); 9 // 執行查詢操作 10 return handler.<E>query(stmt, resultHandler); 11 } finally { 12 // 關閉 Statement 13 closeStatement(stmt); 14 } 15 }
上篇文章我們分析完了第6行程式碼,在第6行處我們建立了一個PreparedStatementHandler,我們要接著第8行程式碼開始分析,也就是建立 Statement,先不忙著分析,我們先來回顧一下 ,我們以前是怎麼使用jdbc的
jdbc
public class Login { /** * 第一步,載入驅動,建立資料庫的連線 * 第二步,編寫sql * 第三步,需要對sql進行預編譯 * 第四步,向sql裡面設定引數 * 第五步,執行sql * 第六步,釋放資源 * @throws Exception */ public static final String URL = "jdbc:mysql://localhost:3306/chenhao"; public static final String USER = "liulx"; public static final String PASSWORD = "123456"; public static void main(String[] args) throws Exception { login("lucy","123"); } public static void login(String username , String password) throws Exception{ Connection conn = null; PreparedStatement psmt = null; ResultSet rs = null; try { //載入驅動程式 Class.forName("com.mysql.jdbc.Driver"); //獲得資料庫連線 conn = DriverManager.getConnection(URL, USER, PASSWORD); //編寫sql String sql = "select * from user where name =? and password = ?";//問號相當於一個佔位符 //對sql進行預編譯 psmt = conn.prepareStatement(sql); //設定引數 psmt.setString(1, username); psmt.setString(2, password); //執行sql ,返回一個結果集 rs = psmt.executeQuery(); //輸出結果 while(rs.next()){ System.out.println(rs.getString("user_name")+" 年齡:"+rs.getInt("age")); } } catch (Exception e) { e.printStackTrace(); }finally{ //釋放資源 conn.close(); psmt.close(); rs.close(); } } }
上面程式碼中註釋已經很清楚了,我們來看看mybatis中是怎麼和資料庫打交道的。
SimpleExecutor
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; // 獲取資料庫連線 Connection connection = getConnection(statementLog); // 建立 Statement, stmt = handler.prepare(connection, transaction.getTimeout()); // 為 Statement 設定引數 handler.parameterize(stmt); return stmt; }
在上面的程式碼中我們終於看到了和jdbc相關的內容了,大概分為下面三個步驟:
- 獲取資料庫連線
- 建立PreparedStatement
- 為PreparedStatement設定執行時引數
我們先來看看獲取資料庫連線,跟進程式碼看看
BaseExecutor
protected Connection getConnection(Log statementLog) throws SQLException { //通過transaction來獲取Connection Connection connection = this.transaction.getConnection(); return statementLog.isDebugEnabled() ? ConnectionLogger.newInstance(connection, statementLog, this.queryStack) : connection; }
我們看到是通過Executor中的transaction屬性來獲取Connection,那我們就先來看看transaction,根據前面的文章中的配置
<
transactionManager
type="jdbc"/>,
則MyBatis會建立一個JdbcTransactionFactory.class 例項,Executor中的transaction是一個JdbcTransaction.class 例項,其實現Transaction介面,那我們先來看看Transaction
JdbcTransaction
我們先來看看其介面Transaction
Transaction
public interface Transaction { //獲取資料庫連線 Connection getConnection() throws SQLException; //提交事務 void commit() throws SQLException; //回滾事務 void rollback() throws SQLException; //關閉事務 void close() throws SQLException; //獲取超時時間 Integer getTimeout() throws SQLException; }
接著我們看看其實現類JdbcTransaction
JdbcTransaction
public class JdbcTransaction implements Transaction { private static final Log log = LogFactory.getLog(JdbcTransaction.class); //資料庫連線 protected Connection connection; //資料來源資訊 protected DataSource dataSource; //隔離級別 protected TransactionIsolationLevel level; //是否為自動提交 protected boolean autoCommmit; public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) { dataSource = ds; level = desiredLevel; autoCommmit = desiredAutoCommit; } public JdbcTransaction(Connection connection) { this.connection = connection; } public Connection getConnection() throws SQLException { //如果事務中不存在connection,則獲取一個connection並放入connection屬性中 //第一次肯定為空 if (connection == null) { openConnection(); } //如果事務中已經存在connection,則直接返回這個connection return connection; } /** * commit()功能 * @throws SQLException */ public void commit() throws SQLException { if (connection != null && !connection.getAutoCommit()) { if (log.isDebugEnabled()) { log.debug("Committing JDBC Connection [" + connection + "]"); } //使用connection的commit() connection.commit(); } } /** * rollback()功能 * @throws SQLException */ public void rollback() throws SQLException { if (connection != null && !connection.getAutoCommit()) { if (log.isDebugEnabled()) { log.debug("Rolling back JDBC Connection [" + connection + "]"); } //使用connection的rollback() connection.rollback(); } } /** * close()功能 * @throws SQLException */ public void close() throws SQLException { if (connection != null) { resetAutoCommit(); if (log.isDebugEnabled()) { log.debug("Closing JDBC Connection [" + connection + "]"); } //使用connection的close() connection.close(); } } protected void openConnection() throws SQLException { if (log.isDebugEnabled()) { log.debug("Opening JDBC Connection"); } //通過dataSource來獲取connection,並設定到transaction的connection屬性中 connection = dataSource.getConnection(); if (level != null) { //通過connection設定事務的隔離級別 connection.setTransactionIsolation(level.getLevel()); } //設定事務是否自動提交 setDesiredAutoCommit(autoCommmit); } protected void setDesiredAutoCommit(boolean desiredAutoCommit) { try { if (this.connection.getAutoCommit() != desiredAutoCommit) { if (log.isDebugEnabled()) { log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + this.connection + "]"); } //通過connection設定事務是否自動提交 this.connection.setAutoCommit(desiredAutoCommit); } } catch (SQLException var3) { throw new TransactionException("Error configuring AutoCommit. Your driver may not support getAutoCommit() or setAutoCommit(). Requested setting: " + desiredAutoCommit + ". Cause: " + var3, var3); } } }
我們看到JdbcTransaction中有一個Connection屬性和dataSource屬性,使用connection來進行提交、回滾、關閉等操作,也就是說JdbcTransaction其實只是在jdbc的connection上面封裝了一下,實際使用的其實還是jdbc的事務。我們看看getConnection()方法
//資料庫連線 protected Connection connection; //資料來源資訊 protected DataSource dataSource; public Connection getConnection() throws SQLException { //如果事務中不存在connection,則獲取一個connection並放入connection屬性中 //第一次肯定為空 if (connection == null) { openConnection(); } //如果事務中已經存在connection,則直接返回這個connection return connection; } protected void openConnection() throws SQLException { if (log.isDebugEnabled()) { log.debug("Opening JDBC Connection"); } //通過dataSource來獲取connection,並設定到transaction的connection屬性中 connection = dataSource.getConnection(); if (level != null) { //通過connection設定事務的隔離級別 connection.setTransactionIsolation(level.getLevel()); } //設定事務是否自動提交 setDesiredAutoCommit(autoCommmit); }
先是判斷當前事務中是否存在connection,如果存在,則直接返回connection,如果不存在則通過dataSource來獲取connection,這裡我們明白了一點,如果當前事務沒有關閉,也就是沒有釋放connection,那麼在同一個Transaction中使用的是同一個connection,我們再來想想,transaction是SimpleExecutor中的屬性,SimpleExecutor又是SqlSession中的屬性,那我們可以這樣說,同一個SqlSession中只有一個SimpleExecutor,SimpleExecutor中有一個Transaction,Transaction有一個connection。我們來看看如下例子
public static void main(String[] args) throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //建立一個SqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); try { EmployeeMapper employeeMapper = sqlSession.getMapper(Employee.class); UserMapper userMapper = sqlSession.getMapper(User.class); List<Employee> allEmployee = employeeMapper.getAll(); List<User> allUser = userMapper.getAll(); Employee employee = employeeMapper.getOne(); } finally { sqlSession.close(); } }
我們看到同一個sqlSession可以獲取多個Mapper代理物件,則多個Mapper代理物件中的sqlSession引用應該是同一個,那麼多個Mapper代理物件呼叫方法應該是同一個Connection,直到呼叫close(),所以說我們的sqlSession是執行緒不安全的,如果所有的業務都使用一個sqlSession,那Connection也是同一個,一個業務執行完了就將其關閉,那其他的業務還沒執行完呢。大家明白了嗎?我們迴歸到原始碼,connection = dataSource.getConnection();,最終還是呼叫dataSource來獲取連線,那我們是不是要來看看dataSource呢?
我們還是從前面的配置檔案來看<dataSource type="UNPOOLED|POOLED">,這裡有UNPOOLED和POOLED兩種DataSource,一種是使用連線池,一種是普通的DataSource,UNPOOLED將會創將new UnpooledDataSource()例項,POOLED將會new pooledDataSource()例項,都實現DataSource介面,那我們先來看看DataSource介面
DataSource
public interface DataSource extends CommonDataSource,Wrapper { //獲取資料庫連線 Connection getConnection() throws SQLException; Connection getConnection(String username, String password) throws SQLException; }
很簡單,只有一個獲取資料庫連線的介面,那我們來看看其實現類
UnpooledDataSource
UnpooledDataSource,從名稱上即可知道,該種資料來源不具有池化特性。該種資料來源每次會返回一個新的資料庫連線,而非複用舊的連線。其核心的方法有三個,分別如下:
- initializeDriver - 初始化資料庫驅動
- doGetConnection - 獲取資料連線
- configureConnection - 配置資料庫連線
初始化資料庫驅動
看下我們上面使用JDBC的例子,在執行 SQL 之前,通常都是先獲取資料庫連線。一般步驟都是載入資料庫驅動,然後通過 DriverManager 獲取資料庫連線。UnpooledDataSource 也是使用 JDBC 訪問資料庫的,因此它獲取資料庫連線的過程一樣
UnpooledDataSource
public class UnpooledDataSource implements DataSource { private ClassLoader driverClassLoader; private Properties driverProperties; private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap(); private String driver; private String url; private String username; private String password; private Boolean autoCommit; private Integer defaultTransactionIsolationLevel; public UnpooledDataSource() { } public UnpooledDataSource(String driver, String url, String username, String password) { this.driver = driver; this.url = url; this.username = username; this.password = password; } private synchronized void initializeDriver() throws SQLException { // 檢測當前 driver 對應的驅動例項是否已經註冊 if (!registeredDrivers.containsKey(driver)) { Class<?> driverType; try { // 載入驅動型別 if (driverClassLoader != null) { // 使用 driverClassLoader 載入驅動 driverType = Class.forName(driver, true, driverClassLoader); } else { // 通過其他 ClassLoader 載入驅動 driverType = Resources.classForName(driver); } // 通過反射建立驅動例項 Driver driverInstance = (Driver) driverType.newInstance(); /* * 註冊驅動,注意這裡是將 Driver 代理類 DriverProxy 物件註冊到 DriverManager 中的,而非 Driver 物件本身。 */ DriverManager.registerDriver(new DriverProxy(driverInstance)); // 快取驅動類名和例項,防止多次註冊 registeredDrivers.put(driver, driverInstance); } catch (Exception e) { throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e); } } } //略... } //DriverManager private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>(); public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException { if(driver != null) { registeredDrivers.addIfAbsent(new DriverInfo(driver)); } else { // This is for compatibility with the original DriverManager throw new NullPointerException(); } }
通過反射機制載入驅動Driver,並將其註冊到DriverManager中的一個常量集合中,供後面獲取連線時使用,為什麼這裡是一個List呢?我們實際開發中有可能使用到了多種資料庫型別,如Mysql、Oracle等,其驅動都是不同的,不同的資料來源獲取連線時使用的是不同的驅動。
在我們使用JDBC的時候,也沒有通過DriverManager.registerDriver(new DriverProxy(driverInstance));去註冊Driver啊,如果我們使用的是Mysql資料來源,那我們來看Class.forName("com.mysql.jdbc.Driver");這句程式碼發生了什麼
Class.forName主要是做了什麼呢?它主要是要求JVM查詢並裝載指定的類。這樣我們的類com.mysql.jdbc.Driver就被裝載進來了。而且在類被裝載進JVM的時候,它的靜態方法就會被執行。我們來看com.mysql.jdbc.Driver的實現程式碼。在它的實現裡有這麼一段程式碼:
static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } }
很明顯,這裡使用了DriverManager並將該類給註冊上去了。所以,對於任何實現前面Driver介面的類,只要在他們被裝載進JVM的時候註冊DriverManager就可以實現被後續程式使用。
作為那些被載入的Driver實現,他們本身在被裝載時會在執行的static程式碼段裡通過呼叫DriverManager.registerDriver()來把自身註冊到DriverManager的registeredDrivers列表中。這樣後面就可以通過得到的Driver來取得連線了。
獲取資料庫連線
在上面例子中使用 JDBC 時,我們都是通過 DriverManager 的介面方法獲取資料庫連線。我們來看看UnpooledDataSource是如何獲取的。
UnpooledDataSource
public Connection getConnection() throws SQLException { return doGetConnection(username, password); } private Connection doGetConnection(String username, String password) throws SQLException { Properties props = new Properties(); if (driverProperties != null) { props.putAll(driverProperties); } if (username != null) { // 儲存 user 配置 props.setProperty("user", username); } if (password != null) { // 儲存 password 配置 props.setProperty("password", password); } // 呼叫過載方法 return doGetConnection(props); } private Connection doGetConnection(Properties properties) throws SQLException { // 初始化驅動,我們上一節已經講過了,只用初始化一次 initializeDriver(); // 獲取連線 Connection connection = DriverManager.getConnection(url, properties); // 配置連線,包括自動提交以及事務等級 configureConnection(connection); return connection; } private void configureConnection(Connection conn) throws SQLException { if (autoCommit != null && autoCommit != conn.getAutoCommit()) { // 設定自動提交 conn.setAutoCommit(autoCommit); } if (defaultTransactionIsolationLevel != null) { // 設定事務隔離級別 conn.setTransactionIsolation(defaultTransactionIsolationLevel); } }
上面方法將一些配置資訊放入到 Properties 物件中,然後將資料庫連線和 Properties 物件傳給 DriverManager 的 getConnection 方法即可獲取到資料庫連線。我們來看看是怎麼獲取資料庫連線的
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException { // 獲取類載入器 ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; synchronized(DriverManager.class) { if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } } // 此處省略部分程式碼 // 這裡遍歷的是在registerDriver(Driver driver)方法中註冊的驅動物件 // 每個DriverInfo包含了驅動物件和其資訊 for(DriverInfo aDriver : registeredDrivers) { // 判斷是否為當前執行緒類載入器載入的驅動類 if(isDriverAllowed(aDriver.driver, callerCL)) { try { println("trying " + aDriver.driver.getClass().getName()); // 獲取連線物件,這裡呼叫了Driver的父類的方法 // 如果這裡有多個DriverInfo,比喻Mysql和Oracle的Driver都註冊registeredDrivers了 // 這裡所有的Driver都會嘗試使用url和info去連線,哪個連線上了就返回 // 會不會所有的都會連線上呢?不會,因為url的寫法不同,不同的Driver會判斷url是否適合當前驅動 Connection con = aDriver.driver.connect(url, info); if (con != null) { // 列印連線成功資訊 println("getConnection returning " + aDriver.driver.getClass().getName()); // 返回連線對像 return (con); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println(" skipping: " + aDriver.getClass().getName()); } } }
程式碼中迴圈所有註冊的驅動,然後通過驅動進行連線,所有的驅動都會嘗試連線,但是不同的驅動,連線的URL是不同的,如Mysql的url是jdbc:mysql://localhost:3306/chenhao,以jdbc:mysql://開頭,則其Mysql的驅動肯定會判斷獲取連線的url符合,Oracle的也類似,我們來看看Mysql的驅動獲取連線
由於篇幅原因,我這裡就不分析了,大家有興趣的可以看看,最後由URL對應的驅動獲取到Connection返回,好了我們再來看看下一種DataSource
PooledDataSource
PooledDataSource 內部實現了連線池功能,用於複用資料庫連線。因此,從效率上來說,PooledDataSource 要高於 UnpooledDataSource。但是最終獲取Connection還是通過UnpooledDataSource,只不過PooledDataSource 提供一個儲存Connection的功能。
輔助類介紹
PooledDataSource 需要藉助兩個輔助類幫其完成功能,這兩個輔助類分別是 PoolState 和 PooledConnection。PoolState 用於記錄連線池執行時的狀態,比如連接獲取次數,無效連線數量等。同時 PoolState 內部定義了兩個 PooledConnection 集合,用於儲存空閒連線和活躍連線。PooledConnection 內部定義了一個 Connection 型別的變數,用於指向真實的資料庫連線。以及一個 Connection 的代理類,用於對部分方法呼叫進行攔截。至於為什麼要攔截,隨後將進行分析。除此之外,PooledConnection 內部也定義了一些欄位,用於記錄資料庫連線的一些執行時狀態。接下來,我們來看一下 PooledConnection 的定義。
PooledConnection
class PooledConnection implements InvocationHandler { private static final String CLOSE = "close"; private static final Class<?>[] IFACES = new Class<?>[]{Connection.class}; private final int hashCode; private final PooledDataSource dataSource; // 真實的資料庫連線 private final Connection realConnection; // 資料庫連線代理 private final Connection proxyConnection; // 從連線池中取出連線時的時間戳 private long checkoutTimestamp; // 資料庫連線建立時間 private long createdTimestamp; // 資料庫連線最後使用時間 private long lastUsedTimestamp; // connectionTypeCode = (url + username + password).hashCode() 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; // 建立 Connection 的代理類物件 this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {...} // 省略部分程式碼 }
下面再來看看 PoolState 的定義。
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; }
大家記住上面的空閒連線列表和活躍連線列表
獲取連線
前面已經說過,PooledDataSource 會將用過的連線進行回收,以便可以複用連線。因此從 PooledDataSource 獲取連線時,如果空閒連結列表裡有連線時,可直接取用。那如果沒有空閒連線怎麼辦呢?此時有兩種解決辦法,要麼建立新連線,要麼等待其他連線完成任務。
PooledDataSource
public class PooledDataSource implements DataSource { private static final Log log = LogFactory.getLog(PooledDataSource.class); //這裡有輔助類PoolState private final PoolState state = new PoolState(this); //還有一個UnpooledDataSource屬性,其實真正獲取Connection是由UnpooledDataSource來完成的 private final UnpooledDataSource dataSource; protected int poolMaximumActiveConnections = 10; protected int poolMaximumIdleConnections = 5; protected int poolMaximumCheckoutTime = 20000; protected int poolTimeToWait = 20000; protected String poolPingQuery = "NO PING QUERY SET"; protected boolean poolPingEnabled = false; protected int poolPingConnectionsNotUsedFor = 0; private int expectedConnectionTypeCode; public PooledDataSource() { this.dataSource = new UnpooledDataSource(); } public PooledDataSource(String driver, String url, String username, String password) { //構造器中建立UnpooledDataSource物件 this.dataSource = new UnpooledDataSource(driver, url, username, password); } public Connection getConnection() throws SQLException { return this.popConnection(this.dataSource.getUsername(), this.dataSource.getPassword()).getProxyConnection(); } 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) { // 檢測空閒連線集合(idleConnections)是否為空 if (!state.idleConnections.isEmpty()) { // idleConnections 不為空,表示有空閒連線可以使用,直接從空閒連線集合中取出一個連線 conn = state.idleConnections.remove(0); } else { /* * 暫無空閒連線可用,但如果活躍連線數還未超出限制 *(poolMaximumActiveConnections),則可建立新的連線 */ if (state.activeConnections.size() < poolMaximumActiveConnections) { // 建立新連線,看到沒,還是通過dataSource獲取連線,也就是UnpooledDataSource獲取連線 conn = new PooledConnection(dataSource.getConnection(), this); } else { // 連線池已滿,不能建立新連線 // 取出執行時間最長的連線 PooledConnection oldestActiveConnection = state.activeConnections.get(0); // 獲取執行時長 long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); // 檢測執行時長是否超出限制,即超時 if (longestCheckoutTime > poolMaximumCheckoutTime) { // 累加超時相關的統計欄位 state.claimedOverdueConnectionCount++; state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime; state.accumulatedCheckoutTime += longestCheckoutTime; // 從活躍連線集合中移除超時連線 state.activeConnections.remove(oldestActiveConnection); // 若連線未設定自動提交,此處進行回滾操作 if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { try { oldestActiveConnection.getRealConnection().rollback(); } catch (SQLException e) {...} } /* * 建立一個新的 PooledConnection,注意, * 此處複用 oldestActiveConnection 的 realConnection 變數 */ conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); /* * 複用 oldestActiveConnection 的一些資訊,注意 PooledConnection 中的 * createdTimestamp 用於記錄 Connection 的建立時間,而非 PooledConnection * 的建立時間。所以這裡要複用原連線的時間資訊。 */ conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp()); conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp()); // 設定連線為無效狀態 oldestActiveConnection.invalidate(); } else {// 執行時間最長的連線並未超時 try { if (!countedWait) { state.hadToWaitCount++; countedWait = true; } 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 { // 連線無效,此時累加無效連線相關的統計欄位 state.badConnectionCount++; localBadConnectionCount++; conn = null; if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) { throw new SQLException(...); } } } } } if (conn == null) { throw new SQLException(...); } return conn; } }
從連線池中獲取連線首先會遇到兩種情況:
- 連線池中有空閒連線
- 連線池中無空閒連線
對於第一種情況,把連線取出返回即可。對於第二種情況,則要進行細分,會有如下的情況。
- 活躍連線數沒有超出最大活躍連線數
- 活躍連線數超出最大活躍連線數
對於上面兩種情況,第一種情況比較好處理,直接建立新的連線即可。至於第二種情況,需要再次進行細分。
- 活躍連線的執行時間超出限制,即超時了
- 活躍連線未超時
對於第一種情況,我們直接將超時連線強行中斷,並進行回滾,然後複用部分欄位重新建立 PooledConnection 即可。對於第二種情況,目前沒有更好的處理方式了,只能等待了。
回收連線
相比於獲取連線,回收連線的邏輯要簡單的多。回收連線成功與否只取決於空閒連線集合的狀態,所需處理情況很少,因此比較簡單。
我們還是來看看
public Connection getConnection() throws SQLException { return this.popConnection(this.dataSource.getUsername(), this.dataSource.getPassword()).getProxyConnection(); }
返回的是PooledConnection的一個代理類,為什麼不直接使用PooledConnection的realConnection呢?我們可以看下PooledConnection這個類
class PooledConnection implements InvocationHandler {
很熟悉是吧,標準的代理類用法,看下其invoke方法
PooledConnection
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); // 重點在這裡,如果呼叫了其close方法,則實際執行的是將連線放回連線池的操作 if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { dataSource.pushConnection(this); return null; } else { try { if (!Object.class.equals(method.getDeclaringClass())) { // issue #579 toString() should never fail // throw an SQLException instead of a Runtime checkConnection(); } // 其他的操作都交給realConnection執行 return method.invoke(realConnection, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } }
那我們來看看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 PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); state.idleConnections.add(newConn); // 複用時間資訊 newConn.setCreatedTimestamp(conn.getCreatedTimestamp()); newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp()); // 將原連線置為無效狀態 conn.invalidate(); // 通知等待的執行緒 state.notifyAll(); } else {// 空閒連線集合已滿 state.accumulatedCheckoutTime += conn.getCheckoutTime(); // 回滾未提交的事務 if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } // 關閉資料庫連線 conn.getRealConnection().close(); conn.invalidate(); } } else { state.badConnectionCount++; } } }
先將連線從活躍連線集合中移除,如果空閒集合未滿,此時複用原連線的欄位資訊建立新的連線,並將其放入空閒集合中即可;若空閒集合已滿,此時無需回收連線,直接關閉即可。
連線池總覺得很神祕,但仔細分析完其程式碼之後,也就沒那麼神祕了,就是將連線使用完之後放到一個集合中,下面再獲取連線的時候首先從這個集合中獲取。 還有PooledConnection的代理模式的使用,值得我們學習
好了,我們已經獲取到了資料庫連線,接下來要建立PrepareStatement了,我們上面JDBC的例子是怎麼獲取的? psmt = conn.prepareStatement(sql);,直接通過Connection來獲取,並且把sql傳進去了,我們看看Mybaits中是怎麼建立PrepareStatement的
建立PreparedStatement
PreparedStatementHandler
stmt = handler.prepare(connection, transaction.getTimeout()); public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException { Statement statement = null; try { // 建立 Statement statement = instantiateStatement(connection); // 設定超時和 FetchSize setStatementTimeout(statement, transactionTimeout); setFetchSize(statement); return statement; } catch (SQLException e) { closeStatement(statement); throw e; } catch (Exception e) { closeStatement(statement); throw new ExecutorException("Error preparing statement. Cause: " + e, e); } } protected Statement instantiateStatement(Connection connection) throws SQLException { //獲取sql字串,比如"select * from user where id= ?" String sql = boundSql.getSql(); // 根據條件呼叫不同的 prepareStatement 方法建立 PreparedStatement if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) { String[] keyColumnNames = mappedStatement.getKeyColumns(); if (keyColumnNames == null) { //通過connection獲取Statement,將sql語句傳進去 return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); } else { return connection.prepareStatement(sql, keyColumnNames); } } else if (mappedStatement.getResultSetType() != null) { return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY); } else { return connection.prepareStatement(sql); } }
看到沒和jdbc的形式一模一樣,我們具體來看看connection.prepareStatement做了什麼
1 public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { 2 3 boolean canServerPrepare = true; 4 5 String nativeSql = getProcessEscapeCodesForPrepStmts() ? nativeSQL(sql) : sql; 6 7 if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) { 8 canServerPrepare = canHandleAsServerPreparedStatement(nativeSql); 9 } 10 11 if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) { 12 canServerPrepare = canHandleAsServerPreparedStatement(nativeSql); 13 } 14 15 if (this.useServerPreparedStmts && canServerPrepare) { 16 if (this.getCachePreparedStatements()) { 17 ...... 18 } else { 19 try { 20 //這裡使用的是ServerPreparedStatement建立PreparedStatement 21 pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType, resultSetConcurrency); 22 23 pStmt.setResultSetType(resultSetType); 24 pStmt.setResultSetConcurrency(resultSetConcurrency); 25 } catch (SQLException sqlEx) { 26 // Punt, if necessary 27 if (getEmulateUnsupportedPstmts()) { 28 pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false); 29 } else { 30 throw sqlEx; 31 } 32 } 33 } 34 } else { 35 pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false); 36 } 37 }
我們只用看最關鍵的第21行程式碼,使用ServerPreparedStatement的getInstance返回一個PreparedStatement,其實本質上ServerPreparedStatement繼承了PreparedStatement物件,我們看看其構造方法
protected ServerPreparedStatement(ConnectionImpl conn, String sql, String catalog, int resultSetType, int resultSetConcurrency) throws SQLException { //略... try { this.serverPrepare(sql); } catch (SQLException var10) { this.realClose(false, true); throw var10; } catch (Exception var11) { this.realClose(false, true); SQLException sqlEx = SQLError.createSQLException(var11.toString(), "S1000", this.getExceptionInterceptor()); sqlEx.initCause(var11); throw sqlEx; } //略... }
繼續呼叫this.serverPrepare(sql);
public class ServerPreparedStatement extends PreparedStatement { //存放執行時引數的陣列 private ServerPreparedStatement.BindValue[] parameterBindings; //伺服器預編譯好的sql語句返回的serverStatementId private long serverStatementId; private void serverPrepare(String sql) throws SQLException { synchronized(this.connection.getMutex()) { MysqlIO mysql = this.connection.getIO(); try { //向sql伺服器傳送了一條PREPARE指令 Buffer prepareResultPacket = mysql.sendCommand(MysqlDefs.COM_PREPARE, sql, (Buffer)null, false, characterEncoding, 0); //記錄下了預編譯好的sql語句所對應的serverStatementId this.serverStatementId = prepareResultPacket.readLong(); this.fieldCount = prepareResultPacket.readInt(); //獲取引數個數,比喻 select * from user where id= ?and name = ?,其中有兩個?,則這裡返回的引數個數應該為2 this.parameterCount = prepareResultPacket.readInt(); this.parameterBindings = new ServerPreparedStatement.BindValue[this.parameterCount]; for(int i = 0; i < this.parameterCount; ++i) { //根據引數個數,初始化陣列 this.parameterBindings[i] = new ServerPreparedStatement.BindValue(); } } catch (SQLException var16) { throw sqlEx; } finally { this.connection.getIO().clearInputStream(); } } } }
ServerPreparedStatement繼承PreparedStatement,ServerPreparedStatement初始化的時候就向sql伺服器傳送了一條PREPARE指令,把SQL語句傳到mysql伺服器,如select * from user where id= ?and name = ?,mysql伺服器會對sql進行編譯,並儲存在伺服器,返回預編譯語句對應的id,並儲存在
ServerPreparedStatement中,同時建立BindValue[] parameterBindings陣列,後面設定引數就直接新增到此陣列中。好了,此時我們建立了一個ServerPreparedStatement並返回,下面就是設定執行時引數了
設定執行時引數到 SQL 中
我們已經獲取到了PreparedStatement,接下來就是將執行時引數設定到PreparedStatement中,如下程式碼
handler.parameterize(stmt);
JDBC是怎麼設定的呢?我們看看上面的例子,很簡單吧
psmt = conn.prepareStatement(sql); //設定引數 psmt.setString(1, username); psmt.setString(2, password);
我們來看看parameterize方法
public void parameterize(Statement statement) throws SQLException { // 通過引數處理器 ParameterHandler 設定執行時引數到 PreparedStatement 中 parameterHandler.setParameters((PreparedStatement) statement); } public class DefaultParameterHandler implements ParameterHandler { private final TypeHandlerRegistry typeHandlerRegistry; private final MappedStatement mappedStatement; private final Object parameterObject; private final BoundSql boundSql; private final Configuration configuration; public void setParameters(PreparedStatement ps) { /* * 從 BoundSql 中獲取 ParameterMapping 列表,每個 ParameterMapping 與原始 SQL 中的 #{xxx} 佔位符一一對應 */ List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; // 獲取屬性名 String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { // 為使用者傳入的引數 parameterObject 建立元資訊物件 MetaObject metaObject = configuration.newMetaObject(parameterObject); // 從使用者傳入的引數中獲取 propertyName 對應的值 value = metaObject.getValue(propertyName); } TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { jdbcType = configuration.getJdbcTypeForNull(); } try { // 由型別處理器 typeHandler 向 ParameterHandler 設定引數 typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (TypeException e) { throw new TypeException(...); } catch (SQLException e) { throw new TypeException(...); } } } } } }
首先從boundSql中獲取parameterMappings 集合,這塊大家可以看看我前面的文章,然後遍歷獲取 parameterMapping中的propertyName ,如#{name} 中的name,然後從執行時引數parameterObject中獲取name對應的引數值,最後設定到PreparedStatement 中,我們主要來看是如何設定引數的。也就是
typeHandler.setParameter(ps, i + 1, value, jdbcType);,這句程式碼最終會向我們例子中一樣執行,如下
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter); }
還記得我們的PreparedStatement是什麼嗎?是ServerPreparedStatement,那我們就來看看ServerPreparedStatement的setString方法
public void setString(int parameterIndex, String x) throws SQLException { this.checkClosed(); if (x == null) { this.setNull(parameterIndex, 1); } else { //根據引數下標從parameterBindings陣列總獲取BindValue ServerPreparedStatement.BindValue binding = this.getBinding(parameterIndex, false); this.setType(binding, this.stringTypeCode); //設定引數值 binding.value = x; binding.isNull = false; binding.isLongData = false; } } protected ServerPreparedStatement.BindValue getBinding(int parameterIndex, boolean forLongData) throws SQLException { this.checkClosed(); if (this.parameterBindings.length == 0) { throw SQLError.createSQLException(Messages.getString("ServerPreparedStatement.8"), "S1009", this.getExceptionInterceptor()); } else { --parameterIndex; if (parameterIndex >= 0 && parameterIndex < this.parameterBindings.length) { if (this.parameterBindings[parameterIndex] == null) { this.parameterBindings[parameterIndex] = new ServerPreparedStatement.BindValue(); } else if (this.parameterBindings[parameterIndex].isLongData && !forLongData) { this.detectedLongParameterSwitch = true; } this.parameterBindings[parameterIndex].isSet = true; this.parameterBindings[parameterIndex].boundBeforeExecutionNum = (long)this.numberOfExecutions; //根據引數下標從parameterBindings陣列總獲取BindValue return this.parameterBindings[parameterIndex]; } else { throw SQLError.createSQLException(Messages.getString("ServerPreparedStatement.9") + (parameterIndex + 1) + Messages.getString("ServerPreparedStatement.10") + this.parameterBindings.length, "S1009", this.getExceptionInterceptor()); } } }
就是根據引數下標從ServerPreparedStatement的引數陣列parameterBindings中獲取BindValue物件,然後設定值,好了現在ServerPreparedStatement包含了預編譯SQL語句的Id和引數陣列,最後一步便是執行SQL了。
執行查詢
執行查詢操作就是我們文章開頭的最後一行程式碼,如下
return handler.<E>query(stmt, resultHandler);
我們來看看query是怎麼做的
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement)statement; //直接執行ServerPreparedStatement的execute方法 ps.execute(); return this.resultSetHandler.handleResultSets(ps); } public boolean execute() throws SQLException { this.checkClosed(); ConnectionImpl locallyScopedConn = this.connection; if (!this.checkReadOnlySafeStatement()) { throw SQLError.createSQLException(Messages.getString("PreparedStatement.20") + Messages.getString("PreparedStatement.21"), "S1009", this.getExceptionInterceptor()); } else { ResultSetInternalMethods rs = null; CachedResultSetMetaData cachedMetadata = null; synchronized(locallyScopedConn.getMutex()) { //略.... rs = this.executeInternal(rowLimit, sendPacket, doStreaming, this.firstCharOfStmt == 'S', metadataFromCache, false); //略.... } return rs != null && rs.reallyResult(); } }
省略了很多程式碼,只看最關鍵的executeInternal
ServerPreparedStatement
protected ResultSetInternalMethods executeInternal(int maxRowsToRetrieve, Buffer sendPacket, boolean createStreamingResultSet, boolean queryIsSelectOnly, Field[] metadataFromCache, boolean isBatch) throws SQLException { try { return this.serverExecute(maxRowsToRetrieve, createStreamingResultSet, metadataFromCache); } catch (SQLException var11) { throw sqlEx; } } private ResultSetInternalMethods serverExecute(int maxRowsToRetrieve, boolean createStreamingResultSet, Field[] metadataFromCache) throws SQLException { synchronized(this.connection.getMutex()) { //略.... MysqlIO mysql = this.connection.getIO(); Buffer packet = mysql.getSharedSendPacket(); packet.clear(); packet.writeByte((byte)MysqlDefs.COM_EXECUTE); //將該語句對應的id寫入資料包 packet.writeLong(this.serverStatementId); int i; //將對應的引數寫入資料包 for(i = 0; i < this.parameterCount; ++i) { if (!this.parameterBindings[i].isLongData) { if (!this.parameterBindings[i].isNull) { this.storeBinding(packet, this.parameterBindings[i], mysql); } else { nullBitsBuffer[i / 8] = (byte)(nullBitsBuffer[i / 8] | 1 << (i & 7)); } } } //傳送資料包,表示執行id對應的預編譯sql Buffer resultPacket = mysql.sendCommand(MysqlDefs.COM_EXECUTE, (String)null, packet, false, (String)null, 0); //略.... ResultSetImpl rs = mysql.readAllResults(this, this.resultSetType, resultPacket, true, (long)this.fieldCount, metadataFromCache); //返回結果 return rs; } }
ServerPreparedStatement在記錄下serverStatementId後,對於相同SQL模板的操作,每次只是傳送serverStatementId和對應的引數,省去了編譯sql的過程。 至此我們的已經從資料庫拿到了查詢結果,但是結果是ResultSetImpl型別,我們還需要將返回結果轉化成我們的java物件呢,留在下一篇來講吧