Mybaits 源碼解析 (七)—– Select 語句的執行過程分析(下篇)全網最詳細,沒有之一

  • 2019 年 11 月 6 日
  • 筆記

我們上篇文章講到了查詢方法裡面的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相關的內容了,大概分為下面三個步驟:

  1. 獲取資料庫連接
  2. 創建PreparedStatement
  3. 為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,從名稱上即可知道,該種數據源不具有池化特性。該種數據源每次會返回一個新的資料庫連接,而非復用舊的連接。其核心的方法有三個,分別如下:

  1. initializeDriver – 初始化資料庫驅動
  2. doGetConnection – 獲取數據連接
  3. 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;      }  }

從連接池中獲取連接首先會遇到兩種情況:

  1. 連接池中有空閑連接
  2. 連接池中無空閑連接

對於第一種情況,把連接取出返回即可。對於第二種情況,則要進行細分,會有如下的情況。

  1. 活躍連接數沒有超出最大活躍連接數
  2. 活躍連接數超出最大活躍連接數

對於上面兩種情況,第一種情況比較好處理,直接創建新的連接即可。至於第二種情況,需要再次進行細分。

  1. 活躍連接的運行時間超出限制,即超時了
  2. 活躍連接未超時

對於第一種情況,我們直接將超時連接強行中斷,並進行回滾,然後復用部分欄位重新創建 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對象呢,留在下一篇來講吧