十三、數據源的配置
在mybatis-configuration.xml 文件中,我們進行了如下的配置:
<!-- 可以配置多個運行環境,但是每個 SqlSessionFactory 實例只能選擇一個運行環境常用: 一、development:開發模式 二、work:工作模式 --> <environments default="development"> <!--id屬性必須和上面的default一樣 --> <environment id="development"> <!--使用JDBC的事務管理機制--> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </dataSource> </environment> </environments>
其中 <transactionManager type="JDBC" /> 是對事務的配置,下篇博客我們會詳細介紹。
本篇博客我們介紹 <dataSource type="POOLED"> 對於數據源的配置。
回到頂部1、解析 environments 標簽
在 XMLConfigBuilder.java 的 parseConfiguration(XNode root) 中:
進入 environmentsElement(root.evalNode("environments")) 方法:
1 private void environmentsElement(XNode context) throws Exception { 2 //如果<environments>標簽不為null 3 if (context != null) { 4 //如果 environment 值為 null 5 if (environment == null) { 6 //獲取<environments default="屬性值">中的default屬性值 7 environment = context.getStringAttribute("default"); 8 } 9 //遍歷<environments />標簽中的子標簽<environment /> 10 for (XNode child : context.getChildren()) { 11 //獲取<environment id="屬性值">中的id屬性值 12 String id = child.getStringAttribute("id"); 13 //遍歷所有<environment>的時候一次判斷相應的id是否是default設置的值 14 if (isSpecifiedEnvironment(id)) { 15 //獲取配置的事務管理器 16 TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); 17 //獲取配置的數據源信息 18 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); 19 DataSource dataSource = dsFactory.getDataSource(); 20 Environment.Builder environmentBuilder = new Environment.Builder(id) 21 .transactionFactory(txFactory) 22 .dataSource(dataSource); 23 configuration.setEnvironment(environmentBuilder.build()); 24 } 25 } 26 } 27 } 28 29 private boolean isSpecifiedEnvironment(String id) { 30 if (environment == null) { 31 throw new BuilderException("No environment specified."); 32 } else if (id == null) { 33 throw new BuilderException("Environment requires an id attribute."); 34 } else if (environment.equals(id)) { 35 return true; 36 } 37 return false; 38 }
①、第 3 行代碼:if (context != null) 也就是說我們可以不在 mybatis-configuration.xml 文件中配置<environments />標簽,這是為了和spring整合時,在spring容器中進行配置。
②、第 5 行——第 8 行代碼:獲取<environments default="屬性值">中的default屬性值,註意第 5 行 首先判斷 environment == null 。因為我們可以配置多個環境,也就是連接多個數據庫。
不過需要記住:盡管可以配置多個環境,每個 SqlSessionFactory 實例只能選擇其一,也就是說每個數據庫對應一個 SqlSessionFactory 實例。
可以用如下方法進行區別:
1 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment); 2 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);
③、第 10 行代碼:遍歷<environments />標簽中的子標簽<environment />,可以配置多個<environment />標簽。
④、第 14 行代碼:遍歷所有<environment />的時候判斷相應的id是否是default設置的值,選擇相等的 <environment />標簽進行數據源的配置。
⑤、第 16 行代碼:進行事務的配置(下篇博客進行詳解)。
⑥、第 18 行代碼:進行數據源的配置,下面我們詳細講解。
回到頂部2、mybatis 的數據源類圖
mybatis 對於數據源的所有類都在如下包中:
註意:DataSource 接口不是mybatis包下的,是JDK的 javax.sql 包下的。
回到頂部3、mybatis 三種數據源類型
前面我們在 mybatis-configuration.xml 文件中配置了數據源的類型:
mybatis 支持三種數據源類型(也就是 type=”[UNPOOLED|POOLED|JNDI]”):
①、UNPOOLED:(不使用連接池)這個數據源的實現只是每次被請求時打開和關閉連接。雖然有點慢,但對於在數據庫連接可用性方面沒有太高要求的簡單應用程序來說,是一個很好的選擇。 不同的數據庫在性能方面的表現也是不一樣的,對於某些數據庫來說,使用連接池並不重要,這個配置就很適合這種情形。
②、POOLED:(使用連接池)這種數據源的實現利用“池”的概念將 JDBC 連接對象組織起來,避免了創建新的連接實例時所必需的初始化和認證時間。
③、JNDI : 這個數據源的實現是為了能在如 EJB 或應用服務器這類容器中使用,容器可以集中或在外部配置數據源,然後放置一個 JNDI 上下文的引用。
ps:關於連接池的概念請看下面詳細介紹。
這三種數據源的類型在 mybatis 在上面所講的類圖中正好對應。那麽 mybatis 是如何產生數據源的呢?
回到頂部4、mybatis 初始化數據源
看上面的類圖,我們可以看到 DataSourceFactory 接口,這是一個工廠方法,mybatis 就是通過工廠模式來創建數據源 DataSource 對象。我們先看看該接口:
1 public interface DataSourceFactory { 2 3 void setProperties(Properties props); 4 5 DataSource getDataSource(); 6 7 }
通過調用其 getDataSource() 方法返回數據源DataSource。
而這個工廠方法也對應上面講的三種數據源類型的工廠方法。它們分別都實現了 DataSourceFactory 接口(使用連接池的數據源類型 PooledDataSourceFactory 是繼承 UnpooledDataSourceFactory,而UnpooledDataSourceFactory 實現了 DataSourceFactory 接口)。
1 public class JndiDataSourceFactory implements DataSourceFactory 2 public class UnpooledDataSourceFactory implements DataSourceFactory 3 public class PooledDataSourceFactory extends UnpooledDataSourceFactory
這裏的三個數據源工廠也是通過工廠模式來產生對應的三種數據源。
為什麽要這樣設計,下面我們來詳細介紹。
回到頂部5、不使用連接池 UnpooledDataSource
在 mybatis-configuration.xml 文件中,type="UNPOOLED"
回到本篇博客第一小節:解析 environments 標簽 的第 18 行代碼:通過配置的 <datasource>標簽,來實例化一個 DataSourceFactory 工廠。該工廠實際上是根據配置的 type 類型,產生對應的數據源類型工廠。
1 private DataSourceFactory dataSourceElement(XNode context) throws Exception { 2 if (context != null) { 3 String type = context.getStringAttribute("type"); 4 Properties props = context.getChildrenAsProperties(); 5 DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance(); 6 factory.setProperties(props); 7 return factory; 8 } 9 throw new BuilderException("Environment declaration requires a DataSourceFactory."); 10 }
下面我們來看 UnPooledDataSource 的 getConnection() 方法實現:
1 public Connection getConnection() throws SQLException { 2 return doGetConnection(username, password); 3 } 4 5 public Connection getConnection(String username, String password) throws SQLException { 6 return doGetConnection(username, password); 7 } 8 private Connection doGetConnection(String username, String password) throws SQLException { 9 //將用戶名、密碼、驅動都封裝到Properties文件中 10 Properties props = new Properties(); 11 if (driverProperties != null) { 12 props.putAll(driverProperties); 13 } 14 if (username != null) { 15 props.setProperty("user", username); 16 } 17 if (password != null) { 18 props.setProperty("password", password); 19 } 20 return doGetConnection(props); 21 } 22 23 /** 24 * 獲取數據庫連接 25 */ 26 private Connection doGetConnection(Properties properties) throws SQLException { 27 //1、初始化驅動 28 initializeDriver(); 29 //2、從DriverManager中獲取連接,獲取新的Connection對象 30 Connection connection = DriverManager.getConnection(url, properties); 31 //3、配置connection屬性 32 configureConnection(connection); 33 return connection; 34 }
前面的代碼比較容易看懂,最後面一個方法獲取數據庫連接有三步。
①、初始化驅動:判斷driver驅動是否已經加載到內存中,如果還沒有加載,則會動態地加載driver類,並實例化一個Driver對象,使用DriverManager.registerDriver()方法將其註冊到內存中,以供後續使用。
1 private synchronized void initializeDriver() throws SQLException { 2 if (!registeredDrivers.containsKey(driver)) { 3 Class<?> driverType; 4 try { 5 if (driverClassLoader != null) { 6 driverType = Class.forName(driver, true, driverClassLoader); 7 } else { 8 driverType = Resources.classForName(driver); 9 } 10 // DriverManager requires the driver to be loaded via the system ClassLoader. 11 // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html 12 Driver driverInstance = (Driver)driverType.newInstance(); 13 DriverManager.registerDriver(new DriverProxy(driverInstance)); 14 registeredDrivers.put(driver, driverInstance); 15 } catch (Exception e) { 16 throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e); 17 } 18 } 19 }
②、創建Connection對象: 使用DriverManager.getConnection()方法創建連接。
③、配置Connection對象: 設置是否自動提交autoCommit和隔離級別isolationLevel。
1 private void configureConnection(Connection conn) throws SQLException { 2 if (autoCommit != null && autoCommit != conn.getAutoCommit()) { 3 conn.setAutoCommit(autoCommit); 4 } 5 if (defaultTransactionIsolationLevel != null) { 6 conn.setTransactionIsolation(defaultTransactionIsolationLevel); 7 } 8 }
④、返回Connection對象。
也就是說,使用 UnpooledDataSource 類型的數據源,每次需要連接的時候都會調用 getConnection() 創建一個新的連接Connection返回。實際上創建一個Connection對象的過程,在底層就相當於和數據庫建立的通信連接,在建立通信連接的過程會消耗一些資源。有時候我們可能只是一個簡單的 SQL 查詢,然後拋棄掉這個連接,這實際上是很耗資源的。
那麽怎麽辦呢?答案就是使用數據庫連接池。
回到頂部6、數據庫連接池
其實對於共享資源,有一個很著名的設計理念:資源池。該理念正是為了解決資源的頻繁分配、釋放所造成的問題。
具體思想:初始化一個池子,裏面預先存放一定數量的資源。當需要使用該資源的時候,將該資源標記為忙狀態;當該資源使用完畢後,資源池把相關的資源的忙標示清除掉,以示該資源可以再被下一個請求使用。
對應到上面數據庫連接的問題,我們可以這樣解決:先建立一個池子,裏面存放一定數量的數據庫連接。當需要數據庫連接時,只需從“連接池”中取出一個,使用完畢之後再放回去。我們可以通過設定連接池最大連接數來防止系統無盡的與數據庫連接。這樣就能避免頻繁的進行數據庫連接和斷開耗資源操作。
回到頂部7、使用連接池 PooledDataSource
先了解一下 PooledDataSource 的實現原理:
①、PooledDataSource將java.sql.Connection對象包裹成PooledConnection對象放到了PoolState類型的容器中維護。 MyBatis將連接池中的PooledConnection分為兩種狀態: 空閑狀態(idle)和活動狀態(active),這兩種狀態的PooledConnection對象分別被存儲到PoolState容器內的idleConnections和activeConnections兩個List集合中。
②、idleConnections:空閑(idle)狀態PooledConnection對象被放置到此集合中,表示當前閑置的沒有被使用的PooledConnection集合,調用PooledDataSource的getConnection()方法時,會優先從此集合中取PooledConnection對象。當用完一個java.sql.Connection對象時,MyBatis會將其包裹成PooledConnection對象放到此集合中。
③、activeConnections:活動(active)狀態的PooledConnection對象被放置到名為activeConnections的ArrayList中,表示當前正在被使用的PooledConnection集合,調用PooledDataSource的getConnection()方法時,會優先從idleConnections集合中取PooledConnection對象,如果沒有,則看此集合是否已滿,如果未滿,PooledDataSource會創建出一個PooledConnection,添加到此集合中,並返回。
下面我們看看PooledDataSource 的getConnection()方法獲取Connection對象的實現:
1 public Connection getConnection() throws SQLException { 2 return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection(); 3 } 4 5 @Override 6 public Connection getConnection(String username, String password) throws SQLException { 7 return popConnection(username, password).getProxyConnection(); 8 }
再看看 popConnection() 方法的實現:
①、 先看是否有空閑(idle)狀態下的PooledConnection對象,如果有,就直接返回一個可用的PooledConnection對象;否則進行第②步。
②、查看活動狀態的PooledConnection池activeConnections是否已滿;如果沒有滿,則創建一個新的PooledConnection對象,然後放到activeConnections池中,然後返回此PooledConnection對象;否則進行第③步;
③、 看最先進入activeConnections池中的PooledConnection對象是否已經過期:如果已經過期,從activeConnections池中移除此對象,然後創建一個新的PooledConnection對象,添加到activeConnections中,然後將此對象返回;否則進行第④步。
④、 線程等待,循環2步
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 conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp()); 42 conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp()); 43 oldestActiveConnection.invalidate(); 44 if (log.isDebugEnabled()) { 45 log.debug("Claimed overdue connection " + conn.getRealHashCode() + "."); 46 } 47 } else { 48 // Must wait 49 try { 50 if (!countedWait) { 51 state.hadToWaitCount++; 52 countedWait = true; 53 } 54 if (log.isDebugEnabled()) { 55 log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection."); 56 } 57 long wt = System.currentTimeMillis(); 58 state.wait(poolTimeToWait); 59 state.accumulatedWaitTime += System.currentTimeMillis() - wt; 60 } catch (InterruptedException e) { 61 break; 62 } 63 } 64 } 65 } 66 if (conn != null) { 67 if (conn.isValid()) { 68 if (!conn.getRealConnection().getAutoCommit()) { 69 conn.getRealConnection().rollback(); 70 } 71 conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password)); 72 conn.setCheckoutTimestamp(System.currentTimeMillis()); 73 conn.setLastUsedTimestamp(System.currentTimeMillis()); 74 state.activeConnections.add(conn); 75 state.requestCount++; 76 state.accumulatedRequestTime += System.currentTimeMillis() - t; 77 } else { 78 if (log.isDebugEnabled()) { 79 log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection."); 80 } 81 state.badConnectionCount++; 82 localBadConnectionCount++; 83 conn = null; 84 if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) { 85 if (log.isDebugEnabled()) { 86 log.debug("PooledDataSource: Could not get a good connection to the database."); 87 } 88 throw new SQLException("PooledDataSource: Could not get a good connection to the database."); 89 } 90 } 91 } 92 } 93 94 } 95 96 if (conn == null) { 97 if (log.isDebugEnabled()) { 98 log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); 99 } 100 throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); 101 } 102 103 return conn; 104 }
對於PooledDataSource的getConnection()方法內,先是調用類PooledDataSource的popConnection()方法返回了一個PooledConnection對象,然後調用了PooledConnection的getProxyConnection()來返回Connection對象。
問題:對於連接池,我們會遇到java.sql.Connection對象的回收問題。當我們的程序中使用完Connection對象時,如果不使用數據庫連接池,我們一般會調用 connection.close()方法,關閉connection連接,釋放資源。但是
調用過close()方法的Connection對象所持有的資源會被全部釋放掉,Connection對象也就不能再使用。
那麽,如果我們使用了連接池,我們在用完了Connection對象時,需要將它放在連接池中,該怎樣做呢?
也就是說,我們在調用con.close()方法的時候,不調用close()方法,將其換成將Connection對象放到連接池容器中的代碼!
很容易想到代理模式。為真正的Connection對象創建一個代理對象,代理對象所有的方法都是調用相應的真正Connection對象的方法實現。當代理對象執行close()方法時,要特殊處理,不調用真正Connection對象的close()方法,而是將Connection對象添加到連接池中。
MyBatis的PooledDataSource的PoolState內部維護的對象是PooledConnection類型的對象,而PooledConnection則是對真正的數據庫連接java.sql.Connection實例對象的包裹器。PooledConnection對象內持有一個真正的數據庫連接java.sql.Connection實例對象和一個java.sql.Connection的代理:
其部分定義如下:
PooledConenction實現了InvocationHandler接口,並且 proxyConnection對象也是根據這個它來生成的代理對象:
class PooledConnection implements InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); 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(); } return method.invoke(realConnection, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } } }
從上述代碼可以看到,當我們使用了pooledDataSource.getConnection()返回的Connection對象的close()方法時,不會調用真正Connection的close()方法,而是將此Connection對象放到連接池中。
回到頂部8、JNDI類型的數據源 JndiDataSource
對於JNDI類型的數據源DataSource的獲取就比較簡單,MyBatis定義了一個JndiDataSourceFactory工廠來創建通過JNDI形式生成的DataSource。
這個數據源的實現是為了能在如 EJB 或應用服務器這類容器中使用,容器可以集中或在外部配置數據源,然後放置一個 JNDI 上下文的引用。這種數據源配置只需要兩個屬性:
①、initial_context – 個屬性用來在 InitialContext 中尋找上下文(即,initialContext.lookup(initial_context))。這是個可選屬性,如果忽略,那麽 data_source 屬性將會直接從 InitialContext 中尋找。
②、data_source – 這是引用數據源實例位置的上下文的路徑。提供了 initial_context 配置時會在其返回的上下文中進行查找,沒有提供時則直接在 InitialContext 中查找。
1 public void setProperties(Properties properties) { 2 try { 3 InitialContext initCtx; 4 Properties env = getEnvProperties(properties); 5 if (env == null) { 6 initCtx = new InitialContext(); 7 } else { 8 initCtx = new InitialContext(env); 9 } 10 11 if (properties.containsKey(INITIAL_CONTEXT) 12 && properties.containsKey(DATA_SOURCE)) { 13 Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT)); 14 dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE)); 15 } else if (properties.containsKey(DATA_SOURCE)) { 16 dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE)); 17 } 18 19 } catch (NamingException e) { 20 throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e); 21 } 22 }
十三、數據源的配置