若是咱们从数据源直接获取链接,且在使用完成后不主动归还给数据源(调用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 |