開啟一個會話Session
前文分析了MyBatis將配置檔案轉換為Java物件的流程,本文開始分析一下insert方法、update方法、delete方法處理的流程,至於為什麼這三個方法要放在一起說,是因為:
- 從語義的角度,insert、update、delete都是屬於對資料庫的行進行更新操作
- 從實現的角度,我們熟悉的PreparedStatement裡面提供了兩種execute方法,一種是executeUpdate(),一種是executeQuery(),前者對應的是insert、update與delete,後者對應的是select,因此對於MyBatis來說只有update與select
示例程式碼為這段:
1 public long insertMail(Mail mail) { 2 SqlSession ss = ssf.openSession(); 3 try { 4 int rows = ss.insert(NAME_SPACE + "insertMail", mail); 5 ss.commit(); 6 if (rows > 0) { 7 return mail.getId(); 8 } 9 return 0; 10 } catch (Exception e) { 11 ss.rollback(); 12 return 0; 13 } finally { 14 ss.close(); 15 } 16 }
首先關注的是第2行的程式碼,ssf是SqlSessionFactory,其型別是DefaultSqlSessionFactory,上文最後已經分析過了,這裡通過DefaultSqlSessionFactory來開啟一個Session,通過Session去進行CRUD操作。
看一下openSession()方法的實現:
1 public SqlSession openSession() { 2 return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); 3 }
顧名思義,從DataSource中獲取Session,第一個引數的值是ExecutorType.SIMPLE,繼續跟程式碼:
1 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { 2 Transaction tx = null; 3 try { 4 final Environment environment = configuration.getEnvironment(); 5 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); 6 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); 7 final Executor executor = configuration.newExecutor(tx, execType); 8 return new DefaultSqlSession(configuration, executor, autoCommit); 9 } catch (Exception e) { 10 closeTransaction(tx); // may have fetched a connection so lets call close() 11 throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); 12 } finally { 13 ErrorContext.instance().reset(); 14 } 15 }
第4行的程式碼,獲取配置的環境資訊Environment。
第5行的程式碼,從Environment中獲取事物工廠TransactionFactory,由於<environment>中配置的是"JDBC",因此其真實型別是JdbcTransactionFactory,上文有說過。
第6行的程式碼,根據Environment中的DataSource(其實際型別是PooledDataSource)、TransactionIsolationLevel、autoCommit三個引數從TransactionFactory中獲取一個事物,注意第三個引數autoCommit,它是openSession()方法中傳過來的,其值為false,即MyBatis預設事物是不自動提交的。
第7行的程式碼,實現跟一下:
1 public Executor newExecutor(Transaction transaction, ExecutorType executorType) { 2 executorType = executorType == null ? defaultExecutorType : executorType; 3 executorType = executorType == null ? ExecutorType.SIMPLE : executorType; 4 Executor executor; 5 if (ExecutorType.BATCH == executorType) { 6 executor = new BatchExecutor(this, transaction); 7 } else if (ExecutorType.REUSE == executorType) { 8 executor = new ReuseExecutor(this, transaction); 9 } else { 10 executor = new SimpleExecutor(this, transaction); 11 } 12 if (cacheEnabled) { 13 executor = new CachingExecutor(executor); 14 } 15 executor = (Executor) interceptorChain.pluginAll(executor); 16 return executor; 17 }
這裡總結一下:
- 根據ExecutorType獲取一個執行器,這裡是第10行的SimpleExecutor
- 如果滿足第12行的判斷開啟快取功能,則執行第13行的程式碼。第13行的程式碼使用到了裝飾器模式,傳入Executor,給SimpleExecutor裝飾上了快取功能
- 第15行的程式碼用於設定外掛
這樣就獲取了一個Executor。最後將Executor、Configuration、autoCommit三個變數作為引數,例項化一個SqlSession出來,其實際型別為DefaultSqlSession。
insert方法執行流程
在看了openSession()方法知道最終獲得了一個DefaultSqlSession之後,看一下DefaultSqlSession的insert方法是如何實現的:
1 public int insert(String statement, Object parameter) { 2 return update(statement, parameter); 3 }
看到雖然呼叫的是insert方法,但是最終統一都會去執行update方法,delete方法也是如此,這個開頭已經說過了,這裡證明了這一點。
接著繼續看第2行的方法實現:
1 public int update(String statement, Object parameter) { 2 try { 3 dirty = true; 4 MappedStatement ms = configuration.getMappedStatement(statement); 5 return executor.update(ms, wrapCollection(parameter)); 6 } catch (Exception e) { 7 throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e); 8 } finally { 9 ErrorContext.instance().reset(); 10 } 11 }
第4行的程式碼根據statement從Configuration中獲取MappedStatement,MappedStatement上文已經分析過了,儲存在Configuration的mappedStatements欄位中。
第5行的程式碼分為兩部分,首先wrapCollection,顧名思義包裝集合類,原始碼為:
1 private Object wrapCollection(final Object object) { 2 if (object instanceof Collection) { 3 StrictMap<Object> map = new StrictMap<Object>(); 4 map.put("collection", object); 5 if (object instanceof List) { 6 map.put("list", object); 7 } 8 return map; 9 } else if (object != null && object.getClass().isArray()) { 10 StrictMap<Object> map = new StrictMap<Object>(); 11 map.put("array", object); 12 return map; 13 } 14 return object; 15 }
這裡做了三層處理:
- 如果引數是Collection(即集合)型別,放一個key為"collection"、value為引數的鍵值對
- 如果引數是List型別,放一個key為"list"、value為引數的鍵值對
- 如果引數是陣列型別,放一個key為"array"、value為引數的鍵值對
將集合進行包裝之後,就可以執行Executor的update方法了,Executor上面說了,是使用裝飾器模式將SimpleExecutor加上了快取功能的CacheExecutor,它的update方法實現為:
1 public int update(MappedStatement ms, Object parameterObject) throws SQLException { 2 flushCacheIfRequired(ms); 3 return delegate.update(ms, parameterObject); 4 }
第2行的程式碼是判斷是否要求清快取的,這裡首先我們的示例配置檔案mail.xml中沒有配置<cache>,其次<insert>、<delete>、<update>、<select>中沒有配置flushCache="true"屬性,因此這一句程式碼不會執行任何操作。
第3行的程式碼delegate就是SimpleExecutor本身,因為是裝飾器模式,因此會持有介面的引用,deletegate其型別就是Executor。繼續跟程式碼,看一下update方法:
1 public int update(MappedStatement ms, Object parameter) throws SQLException { 2 ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); 3 if (closed) { 4 throw new ExecutorException("Executor was closed."); 5 } 6 clearLocalCache(); 7 return doUpdate(ms, parameter); 8 }
前面的沒什麼好看的,繼續跟第7行的程式碼:
1 public int doUpdate(MappedStatement ms, Object parameter) throws SQLException { 2 Statement stmt = null; 3 try { 4 Configuration configuration = ms.getConfiguration(); 5 StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null); 6 stmt = prepareStatement(handler, ms.getStatementLog()); 7 return handler.update(stmt); 8 } finally { 9 closeStatement(stmt); 10 } 11 }
第4行的程式碼獲取MappedStatement中的Configuration物件。
第5行的程式碼獲取Statement處理器StatementHandler介面實現類,Statement是Java原生的為JDBC設計的宣告,StatementHandler介面實現類的真實型別為RoutingStatementHandler。
第6行和第7行的程式碼後文逐步分析,因為裡面一點一點封裝了我們平時寫JDBC時的一些基本步驟,比如獲取Connection,構建PreparedStatement、對execute後的結果進行處理等,先看一下prepareStatement的原始碼:
1 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { 2 Statement stmt; 3 Connection connection = getConnection(statementLog); 4 stmt = handler.prepare(connection, transaction.getTimeout()); 5 handler.parameterize(stmt); 6 return stmt; 7 }
後面逐步分析。
獲取Connection
第一步,看下獲取Connection的步驟。看一下上面getConnection方法如何實現:
1 protected Connection getConnection(Log statementLog) throws SQLException { 2 Connection connection = transaction.getConnection(); 3 if (statementLog.isDebugEnabled()) { 4 return ConnectionLogger.newInstance(connection, statementLog, queryStack); 5 } else { 6 return connection; 7 } 8 }
Connection從Transaction中獲取,配置的是JDBC,這裡程式碼進入JdbcTransaction的getConnection():
1 protected Connection getConnection(Log statementLog) throws SQLException { 2 Connection connection = transaction.getConnection(); 3 if (statementLog.isDebugEnabled()) { 4 return ConnectionLogger.newInstance(connection, statementLog, queryStack); 5 } else { 6 return connection; 7 } 8 }
先看一下第3行~第7行的程式碼,判斷的意思是是否開啟Statement的表示式,如果開啟,那麼第4行會給生成的Connection加上一個代理,代理的內容是在呼叫prepareStatement、prepareCall等方法前或者方法後列印日誌,具體可見ConnectionLogger、PreparedStatementLogger、ResultSetLogger與StatementLogger的invoke方法。
接著繼續跟第2行的程式碼:
1 public Connection getConnection() throws SQLException { 2 if (connection == null) { 3 openConnection(); 4 } 5 return connection; 6 }
跟一下第3行的程式碼:
1 protected void openConnection() throws SQLException { 2 if (log.isDebugEnabled()) { 3 log.debug("Opening JDBC Connection"); 4 } 5 connection = dataSource.getConnection(); 6 if (level != null) { 7 connection.setTransactionIsolation(level.getLevel()); 8 } 9 setDesiredAutoCommit(autoCommmit); 10 }
第6行~第8行的程式碼用於設定事物隔離級別,第9行的程式碼用於設定是否自動提交事物。下面跟一下第5行的程式碼getConnection()方法:
1 public Connection getConnection() throws SQLException { 2 return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection(); 3 }
這裡簡單提一下,在方法名中如果看到了"pop"、"push"字樣,一定要把該方法使用的資料結構和棧聯想起來,棧(stack)是一個後進先出的資料結構,"pop"、"push"是棧特有的操作,前者將棧頂的資料推送出棧讓呼叫者獲取到,後者將資料壓入棧頂。
後面的getProxyConnection()方法就是將獲取到的Connection返回而已,沒什麼特殊的操作,這裡跟一下popConnection方法實現,它位於PooledDataSource類中,這是由<dataSource>標籤中的type屬性決定的:
1 private PooledConnection popConnection(String username, String password) throws SQLException { 2 boolean countedWait = false; 3 PooledConnection conn = null; 4 long t = System.currentTimeMillis(); 5 int localBadConnectionCount = 0; 6 7 while (conn == null) { 8 synchronized (state) { 9 if (!state.idleConnections.isEmpty()) { 10 // Pool has available connection 11 conn = state.idleConnections.remove(0); 12 if (log.isDebugEnabled()) { 13 log.debug("Checked out connection " + conn.getRealHashCode() + " from pool."); 14 } 15 } else { 16 // Pool does not have available connection 17 if (state.activeConnections.size() < poolMaximumActiveConnections) { 18 // Can create new connection 19 conn = new PooledConnection(dataSource.getConnection(), this); 20 if (log.isDebugEnabled()) { 21 log.debug("Created connection " + conn.getRealHashCode() + "."); 22 } 23 } else { 24 // Cannot create new connection 25 PooledConnection oldestActiveConnection = state.activeConnections.get(0); 26 long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); 27 if (longestCheckoutTime > poolMaximumCheckoutTime) { 28 // Can claim overdue connection 29 state.claimedOverdueConnectionCount++; 30 state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime; 31 state.accumulatedCheckoutTime += longestCheckoutTime; 32 state.activeConnections.remove(oldestActiveConnection); 33 if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { 34 try { 35 oldestActiveConnection.getRealConnection().rollback(); 36 } catch (SQLException e) { 37 log.debug("Bad connection. Could not roll back"); 38 } 39 } 40 conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); 41 oldestActiveConnection.invalidate(); 42 if (log.isDebugEnabled()) { 43 log.debug("Claimed overdue connection " + conn.getRealHashCode() + "."); 44 } 45 } else { 46 // Must wait 47 try { 48 if (!countedWait) { 49 state.hadToWaitCount++; 50 countedWait = true; 51 } 52 if (log.isDebugEnabled()) { 53 log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection."); 54 } 55 long wt = System.currentTimeMillis(); 56 state.wait(poolTimeToWait); 57 state.accumulatedWaitTime += System.currentTimeMillis() - wt; 58 } catch (InterruptedException e) { 59 break; 60 } 61 } 62 } 63 } 64 if (conn != null) { 65 if (conn.isValid()) { 66 if (!conn.getRealConnection().getAutoCommit()) { 67 conn.getRealConnection().rollback(); 68 } 69 conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password)); 70 conn.setCheckoutTimestamp(System.currentTimeMillis()); 71 conn.setLastUsedTimestamp(System.currentTimeMillis()); 72 state.activeConnections.add(conn); 73 state.requestCount++; 74 state.accumulatedRequestTime += System.currentTimeMillis() - t; 75 } else { 76 if (log.isDebugEnabled()) { 77 log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection."); 78 } 79 state.badConnectionCount++; 80 localBadConnectionCount++; 81 conn = null; 82 if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) { 83 if (log.isDebugEnabled()) { 84 log.debug("PooledDataSource: Could not get a good connection to the database."); 85 } 86 throw new SQLException("PooledDataSource: Could not get a good connection to the database."); 87 } 88 } 89 } 90 } 91 92 } 93 94 if (conn == null) { 95 if (log.isDebugEnabled()) { 96 log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); 97 } 98 throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); 99 } 100 101 return conn; 102 }
這段方法很長,分解一下。
首先是第9行~第15行的判斷,假使空閒的Connection列表不是空的,Connection就是空閒Connection列表的第一個Connection,且移除空閒Connection列表的第一個Connection,這也符合PooledDataSource的定義,有一個Connection池,對Connection進行復用而不是每次都new出來,這就是典型的棧的操作。但是這裡有一點我認為MyBatis寫得不是很好,List的實際型別是ArrayList,每次的移除操作是remove(0),ArrayList處理remove效率並不高尤其還是remove(0)的操作,因此這裡替換成LinkedList會更好一些。
接著先看第23行~第63行的判斷,它的判斷邏輯是假如當前在使用的Connection數量大於或等於最大可用的Connection數量,那麼獲取當前正在使用的Connection列表中的第一個Connection做一個判斷:
- 如果當前Connection執行時間已經超過了指定的Connection最大超時時間,那麼原Connection如果不是自動Commit的,資料回滾,新建一個Connection,原Connection失效
- 如果當前Connection執行時間沒有超過指定的Connection最大超時時間,那麼使用wait方法等待
最後回到第17行~第23行的判斷,即當前在使用的Connection數量小於最大可用的Connection數量,那麼此時直接new一個PooledConnection出來,看一下PooledDataSource的getConnection()方法實現:
1 public Connection getConnection() throws SQLException { 2 return doGetConnection(username, password); 3 }
繼續跟程式碼doGetConnection方法:
1 private Connection doGetConnection(String username, String password) throws SQLException { 2 Properties props = new Properties(); 3 if (driverProperties != null) { 4 props.putAll(driverProperties); 5 } 6 if (username != null) { 7 props.setProperty("user", username); 8 } 9 if (password != null) { 10 props.setProperty("password", password); 11 } 12 return doGetConnection(props); 13 }
這裡就是先設定一下配置的屬性,繼續跟第12行的方法實現:
1 private Connection doGetConnection(Properties properties) throws SQLException { 2 initializeDriver(); 3 Connection connection = DriverManager.getConnection(url, properties); 4 configureConnection(connection); 5 return connection; 6 }
到了這裡就是我們比較熟悉的程式碼了。
第2行的程式碼意思是MyBatis維護了一個Driver池registeredDrivers,如果我們的Driver不在Driver池裡面,那麼會嘗試使用Class.forName方法初始化一下,成功的話加入Driver池中。
第3行的程式碼不說了,使用DriverManager的getConnection方法獲取Connection,第4行的程式碼配置一下Connection,主要就是設定一下自動提交屬性與事物隔離級別。
最後將生成的Connection返回出去,完成生成Connection的流程。
為Connection生成代理
上面解析了生成Connection的流程,程式碼到這裡還沒完還有一步,看一下PooledConnection的構造方法:
1 public PooledConnection(Connection connection, PooledDataSource dataSource) { 2 this.hashCode = connection.hashCode(); 3 this.realConnection = connection; 4 this.dataSource = dataSource; 5 this.createdTimestamp = System.currentTimeMillis(); 6 this.lastUsedTimestamp = System.currentTimeMillis(); 7 this.valid = true; 8 this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this); 9 }
這裡第8行的程式碼會為生成的Connection建立一個代理,PooledConnection本身就實現了InvocationHandler介面,看一下代理內容是什麼,invoke方法的實現:
1 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 2 String methodName = method.getName(); 3 if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { 4 dataSource.pushConnection(this); 5 return null; 6 } else { 7 try { 8 if (!Object.class.equals(method.getDeclaringClass())) { 9 // issue #579 toString() should never fail 10 // throw an SQLException instead of a Runtime 11 checkConnection(); 12 } 13 return method.invoke(realConnection, args); 14 } catch (Throwable t) { 15 throw ExceptionUtil.unwrapThrowable(t); 16 } 17 } 18 }
這一步操作主要是為了處理close方法的,看一下pushConnection方法的實現:
1 protected void pushConnection(PooledConnection conn) throws SQLException { 2 3 synchronized (state) { 4 state.activeConnections.remove(conn); 5 if (conn.isValid()) { 6 if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) { 7 state.accumulatedCheckoutTime += conn.getCheckoutTime(); 8 if (!conn.getRealConnection().getAutoCommit()) { 9 conn.getRealConnection().rollback(); 10 } 11 PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); 12 state.idleConnections.add(newConn); 13 newConn.setCreatedTimestamp(conn.getCreatedTimestamp()); 14 newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp()); 15 conn.invalidate(); 16 if (log.isDebugEnabled()) { 17 log.debug("Returned connection " + newConn.getRealHashCode() + " to pool."); 18 } 19 state.notifyAll(); 20 } else { 21 state.accumulatedCheckoutTime += conn.getCheckoutTime(); 22 if (!conn.getRealConnection().getAutoCommit()) { 23 conn.getRealConnection().rollback(); 24 } 25 conn.getRealConnection().close(); 26 if (log.isDebugEnabled()) { 27 log.debug("Closed connection " + conn.getRealHashCode() + "."); 28 } 29 conn.invalidate(); 30 } 31 } else { 32 if (log.isDebugEnabled()) { 33 log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection."); 34 } 35 state.badConnectionCount++; 36 } 37 } 38 }
程式碼的邏輯簡單來說就是當呼叫close方法的時候,如果當前空閒Connection列表中的Connection數量小於指定空閒Connection列表中的數量(第二個判斷connectionTypeCode的值為275950209,不知道是幹什麼的),那麼會為原Connection生成一個PooledConnection並加入空閒Connection列表中。
如果不滿足上面的條件,那麼就直接呼叫Connection的close()方法並且讓原Connection失效。