若是咱們從數據源直接獲取鏈接,且在使用完成後不主動歸還給數據源(調用Connection#close()),則將形成數據鏈接泄漏的問題。 java
package com.baobaotao.connleak; … @Service("jdbcUserService") public class JdbcUserService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional public void logon(String userName) { try { //①直接從數據源獲取鏈接,後續程序沒有顯式釋放該鏈接 Connection conn = jdbcTemplate.getDataSource().getConnection(); String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?"; jdbcTemplate.update(sql, System.currentTimeMillis(), userName); //②模擬程序代碼的執行時間 Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } }
JdbcUserService經過Spring AOP事務加強的配置,讓全部public方法都工做在事務環境中,即讓logon()和updateLastLogonTime()方法擁有事務功能。 在logon()方法內部,咱們在①處經過調用jdbcTemplate.getDataSource().getConnection()顯式獲取一個 鏈接,這個鏈接不是logon()方法事務上下文線程綁定的鏈接,因此若是開發者沒有手工釋放這個鏈接(顯式調用Connection#close()方 法),則這個鏈接將永久被佔用(處於active狀態),形成鏈接泄漏!下面,咱們編寫模擬運行的代碼,查看方法執行對數據鏈接的實際佔用狀況:spring
package com.baobaotao.connleak; … @Service("jdbcUserService") public class JdbcUserService { … //①以異步線程的方式執行JdbcUserService#logon()方法,以模擬多線程的環境 public static void asynchrLogon(JdbcUserService userService, String userName) { UserServiceRunner runner = new UserServiceRunner(userService, userName); runner.start(); } private static class UserServiceRunner extends Thread { private JdbcUserService userService; private String userName; public UserServiceRunner(JdbcUserService userService, String userName) { this.userService = userService; this.userName = userName; } public void run() { userService.logon(userName); } } //②讓主執行線程睡眠一段指定的時間 public static void sleep(long time) { try { Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } //③彙報數據源的鏈接佔用狀況 public static void reportConn(BasicDataSource basicDataSource) { System.out.println("鏈接數[active:idle]-[" + basicDataSource.getNumActive()+":"+basicDataSource.getNumIdle()+"]"); } public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("com/baobaotao/connleak/applicatonContext.xml"); JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService"); BasicDataSource basicDataSource = (BasicDataSource) ctx.getBean("dataSource"); //④彙報數據源初始鏈接佔用狀況 JdbcUserService.reportConn(basicDataSource); JdbcUserService.asynchrLogon(userService, "tom");//啓動一個異常線程A JdbcUserService.sleep(500); //⑤此時線程A正在執行JdbcUserService#logon()方法 JdbcUserService.reportConn(basicDataSource); JdbcUserService.sleep(2000); //⑥此時線程A所執行的JdbcUserService#logon()方法已經執行完畢 JdbcUserService.reportConn(basicDataSource); JdbcUserService.asynchrLogon(userService, "john");//啓動一個異常線程B JdbcUserService.sleep(500); //⑦此時線程B正在執行JdbcUserService#logon()方法 JdbcUserService.reportConn(basicDataSource); JdbcUserService.sleep(2000); //⑧此時線程A和B都已完成JdbcUserService#logon()方法的執行 JdbcUserService.reportConn(basicDataSource); }
在JdbcUserService中添加一個可異步執行logon()方法的asynchrLogon()方法,咱們經過異步執行logon()以及讓主 線程睡眠的方式模擬多線程環境下的執行場景。在不一樣的執行點,經過reportConn()方法彙報數據源鏈接的佔用狀況。
經過Spring事務聲明,對JdbcUserServie的logon()方法進行事務加強,配置代碼以下所示: sql
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" … http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> <context:component-scan base-package="com.baobaotao.connleak"/> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" p:driverClassName="${jdbc.driverClassName}" p:url="${jdbc.url}" p:username="${jdbc.username}" p:password="${jdbc.password}"/> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="dataSource"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"/> <!--①啓用註解驅動的事務加強--> <tx:annotation-driven/> </beans>
而後,運行JdbcUserServie,在控制檯將觀察到以下的輸出信息:數據庫
時間 | 執行線程1 | 執行線程2 | 數據源鏈接 | ||
|
active | idle | leak | ||
T0 | 未啓動 | 未啓動 | 0 | 0 | 0 |
T1 | 正在執行方法 | 未啓動 | 2 | 0 | 0 |
T2 | 執行完畢 | 未啓動 | 1 | 1 | 1 |
T3 | 執行完畢 | 正式執行方法 | 3 | 0 | 1 |
T4 | 執行完畢 | 執行完畢 | 2 | 1 | 2 |
來看一下DataSourceUtils從數據源獲取鏈接的關鍵代碼: apache
public abstract class DataSourceUtils { … public static Connection doGetConnection(DataSource dataSource) throws SQLException { Assert.notNull(dataSource, "No DataSource specified"); //①首先嚐試從事務同步管理器中獲取數據鏈接 ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested(); if (!conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(dataSource.getConnection()); } return conHolder.getConnection(); } //②若是獲取不到鏈接,則直接從數據源中獲取鏈接 Connection con = dataSource.getConnection(); //③若是擁有事務上下文,則將鏈接綁定到事務上下文中 if (TransactionSynchronizationManager.isSynchronizationActive()) { ConnectionHolder holderToUse = conHolder; if (holderToUse == null) { holderToUse = new ConnectionHolder(con); } else {holderToUse.setConnection(con);} holderToUse.requested(); TransactionSynchronizationManager.registerSynchronization( new ConnectionSynchronization(holderToUse, dataSource)); holderToUse.setSynchronizedWithTransaction(true); if (holderToUse != conHolder) { TransactionSynchronizationManager.bindResource( dataSource, holderToUse); } } return con; } … }
它首先查看當前是否存在事務管理上下文,並嘗試從事務管理上下文獲取鏈接,若是獲取失敗,直接從數據源中獲取鏈接。在獲取鏈接後,若是當前擁有事務上下文,則將鏈接綁定到事務上下文中。
咱們在JdbcUserService中,使用DataSourceUtils.getConnection()替換直接從數據源中獲取鏈接的代碼: 多線程
package com.baobaotao.connleak; … @Service("jdbcUserService") public class JdbcUserService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional public void logon(String userName) { try { //①使用DataSourceUtils獲取數據鏈接 Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource()); //Connection conn = jdbcTemplate.getDataSource().getConnection(); String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?"; jdbcTemplate.update(sql, System.currentTimeMillis(), userName); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } }
從新運行代碼,獲得以下的執行結果:
app
時間 | 執行線程1 | 執行線程2 | 數據源鏈接 | ||
|
active | idle | leak | ||
T0 | 未啓動 | 未啓動 | 0 | 0 | 0 |
T1 | 正在執行方法 | 未啓動 | 1 | 1 | 0 |
T2 | 執行完畢 | 未啓動 | 1 | 1 | 1 |
T3 | 執行完畢 | 正式執行方法 | 2 | 1 | 1 |
T4 | 執行完畢 | 執行完畢 | 2 | 1 | 2 |
要堵上這個鏈接泄漏的漏洞,須要對logon()方法進行以下的改造: 框架
package com.baobaotao.connleak; … @Service("jdbcUserService") public class JdbcUserService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional public void logon(String userName) { try { Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource()); String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?"; jdbcTemplate.update(sql, System.currentTimeMillis(), userName); Thread.sleep(1000); //① } catch (Exception e) { e.printStackTrace(); }finally { //②顯式使用DataSourceUtils釋放鏈接 DataSourceUtils.releaseConnection(conn,jdbcTemplate.getDataSource()); } } }
在②處顯式調用DataSourceUtils.releaseConnection()方法釋放獲取的鏈接。特別須要指出的是:必定不能在①處釋放連 接!由於若是logon()在獲取鏈接後,①處代碼前這段代碼執行時發生異常,則①處釋放鏈接的動做將得不到執行。這將是一個很是具備隱蔽性的鏈接泄漏的 隱患點。
JdbcTemplate如何作到對鏈接泄漏的免疫
分析JdbcTemplate的代碼,咱們能夠清楚地看到它開放的每一個數據操做方法,首先都使用DataSourceUtils獲取鏈接,在方法返回以前使用DataSourceUtils釋放鏈接。
來看一下JdbcTemplate最核心的一個數據操做方法execute(): 異步
public <T> T execute(StatementCallback<T> action) throws DataAccessException { //①首先根據DataSourceUtils獲取數據鏈接 Connection con = DataSourceUtils.getConnection(getDataSource()); Statement stmt = null; try { Connection conToUse = con; … handleWarnings(stmt); return result; } catch (SQLException ex) { JdbcUtils.closeStatement(stmt); stmt = null; //②發生異常時,使用DataSourceUtils釋放數據鏈接 DataSourceUtils.releaseConnection(con, getDataSource()); con = null; throw getExceptionTranslator().translate( "StatementCallback", getSql(action), ex); } finally { JdbcUtils.closeStatement(stmt); //③最後再使用DataSourceUtils釋放數據鏈接 DataSourceUtils.releaseConnection(con, getDataSource()); } }
在①處經過DataSourceUtils.getConnection()獲取鏈接,在②和③處經過 DataSourceUtils.releaseConnection()釋放鏈接。全部JdbcTemplate開放的數據訪問API最終都是直接或間 接由execute(StatementCallback<T> action)方法執行數據訪問操做的,所以這個方法表明了JdbcTemplate數據操做的最終實現方式。
正是由於JdbcTemplate嚴謹的獲取鏈接及釋放鏈接的模式化流程保證了JdbcTemplate對數據鏈接泄漏問題的免疫性。因此,若有可能儘可能 使用JdbcTemplate、HibernateTemplate等這些模板進行數據訪問操做,避免直接獲取數據鏈接的操做。
使用TransactionAwareDataSourceProxy
若是不得已要顯式獲取數據鏈接,除了使用DataSourceUtils獲取事務上下文綁定的鏈接外,還能夠經過 TransactionAwareDataSourceProxy對數據源進行代理。數據源對象被代理後就具備了事務上下文感知的能力,經過代理數據源的 getConnection()方法獲取鏈接和使用DataSourceUtils.getConnection()獲取鏈接的效果是同樣的。
下面是使用TransactionAwareDataSourceProxy對數據源進行代理的配置: async
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" … http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> <context:component-scan base-package="com.baobaotao.connleak"/> <context:property-placeholder location="classpath:jdbc.properties"/> <!--①未被代理的數據源 --> <bean id="originDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" p:driverClassName="${jdbc.driverClassName}" p:url="${jdbc.url}" p:username="${jdbc.username}" p:password="${jdbc.password}"/> <!--②對數據源進行代碼,使數據源具體事務上下文感知性 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy" p:targetDataSource-ref="originDataSource" /> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="dataSource"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"/> <tx:annotation-driven/> </beans>
對數據源進行代理後,咱們就能夠經過數據源代理對象的getConnection()獲取事務上下文中綁定的數據鏈接了。所以,若是數據源已經進行了 TransactionAwareDataSourceProxy的代理,並且方法存在事務上下文,那麼代碼清單10-19的代碼也不會生產鏈接泄漏的問 題。
其餘數據訪問技術的等價類
理解了Spring JDBC的數據鏈接泄漏問題,其中的道理能夠平滑地推廣到其餘框架中去。Spring爲每一個數據訪問技術框架都提供了一個獲取事務上下文綁定的數據鏈接(或其衍生品)的工具類和數據源(或其衍生品)的代理類。
表10-5列出了不一樣數據訪問技術對應DataSourceUtils的等價類。
表10-5 不一樣數據訪問框架DataSourceUtils的等價類
數據訪問技術框架 | 鏈接(或衍生品)獲取工具類 |
Spring JDBC | org.springframework.jdbc.datasource.DataSourceUtils |
Hibernate | org.springframework.orm.hibernate3.SessionFactoryUtils |
iBatis | org.springframework.jdbc.datasource.DataSourceUtils |
JPA | org.springframework.orm.jpa.EntityManagerFactoryUtils |
JDO | org.springframework.orm.jdo.PersistenceManagerFactoryUtils |
數據訪問技術框架 | 鏈接(或衍生品)獲取工具類 |
Spring JDBC | org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy |
Hibernate | org.springframework.orm.hibernate3.LocalSessionFactoryBean |
iBatis | org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy |
JPA | 無 |
JDO | org.springframework.orm.jdo.TransactionAwarePersistenceManagerFactoryProxy |