Spring JDBC 源码解析:从环境搭建到 JdbcTemplate 查询与更新的底层调用链
Spring JDBC 源码解析从环境搭建到 JdbcTemplate 查询与更新的底层调用链【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter导读本文是 source-code-hunter 仓库中 Spring-jdbc.md 的完整展开以「环境搭建 → 连接获取 → 连接释放 → 查询执行 → 更新执行」为主线深入剖析 Spring JDBC 中JdbcTemplate、DataSourceUtils、RowMapperResultSetExtractor等核心类的底层实现。读完本文你将掌握 Spring JDBC 的最小可运行工程如何搭建、JdbcTemplate查询与更新背后完整的调用链、连接对象如何通过ConnectionHolder与事务同步机制绑定到当前线程并能结合仓库内的 Spring 事务管理器的设计与实现、Spring 事务解析 等文档继续向事务方向深入。一、环境搭建一个最小可运行的 Spring JDBC 工程1.1 依赖引入原文档基于 Gradle 构建核心依赖如下compile(project(:spring-jdbc)) compile group: com.alibaba, name: druid, version: 1.1.21 compile group: mysql, name: mysql-connector-java, version: 5.1.47三个依赖各司其职spring-jdbc本文的主角提供JdbcTemplate、DataSourceUtils、JdbcDaoSupport、RowMapper等核心类druid阿里巴巴出品的数据库连接池用于提供DataSource实现也便于在调试时观察DruidPooledResultSet等真实对象mysql-connector-javaMySQL 驱动负责将 JDBC API 调用翻译为 MySQL 协议。1.2 数据库配置通过db.properties集中管理数据库连接参数jdbc.url jdbc.driverClass jdbc.username jdbc.password四个占位符后续会在 XML 配置中通过${jdbc.url}形式被context:property-placeholder解析并注入DruidDataSource。实际使用时请将jdbc.url填写为类似jdbc:mysql://localhost:3306/test?useUnicodetruecharacterEncodingutf8的完整连接串jdbc.driverClass填写为com.mysql.jdbc.Driver。1.3 实体对象与 DAO实体类HsLog对应数据库表hs_logpublic class HsLog { private Integer id; private String source; public Integer getId() { return id; } public void setId(Integer id) { this.id id; } public String getSource() { return source; } public void setSource(String source) { this.source source; } }DAO 接口定义两个方法查询全部日志、保存一条日志public interface HsLogDao { ListHsLog findAll(); void save(HsLog hsLog); }1.4 基于 JdbcDaoSupport 的实现类public class HsLogDaoImpl extends JdbcDaoSupport implements HsLogDao { Override public ListHsLog findAll() { return this.getJdbcTemplate().query(select * from hs_log, new HsLogRowMapper()); } Override public void save(HsLog hsLog) { this.getJdbcTemplate().update(insert into hs_log (SOURCE) values(?) , new Object[]{ hsLog.getSource(), } ); } class HsLogRowMapper implements RowMapperHsLog { public HsLog mapRow(ResultSet rs, int rowNum) throws SQLException { HsLog log new HsLog(); log.setId(rs.getInt(id)); log.setSource(rs.getString(source)); return log; } } }两个关键设计点JdbcDaoSupportSpring 提供的 DAO 支持基类内部持有JdbcTemplate实例通过getJdbcTemplate()获取其setDataSource()方法会自动创建一个基于该数据源的JdbcTemplate因此 DAO 的依赖注入既可以直接注入jdbcTemplate也可以只注入dataSourceRowMapper负责把ResultSet的一行映射为领域对象。mapRow(ResultSet rs, int rowNum)是回调接口由框架在遍历结果集时逐行回调行号rowNum从 0 开始递增。1.5 XML 装配Druid 连接池 JdbcTemplate DAO?xml version1.0 encodingUTF-8? beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlnshttp://www.springframework.org/schema/beans xmlns:contexthttp://www.springframework.org/schema/context xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd context:property-placeholder locationclasspath:db.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property nameurl value${jdbc.url}/ property namedriverClassName value${jdbc.driverClass}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ !-- 配置监控统计拦截的filters -- property namefilters valuestat/ !-- 配置初始化大小、最小、最大 -- property namemaxActive value20/ property nameinitialSize value1/ property nameminIdle value1/ !-- 配置获取连接等待超时的时间 -- property namemaxWait value60000/ !-- 配置间隔多久才进行一次检测检测需要关闭的空闲连接单位是毫秒 -- property nametimeBetweenEvictionRunsMillis value60000/ !-- 配置一个连接在池中最小生存的时间单位是毫秒 -- property nameminEvictableIdleTimeMillis value300000/ property nametestWhileIdle valuetrue/ property nametestOnBorrow valuefalse/ property nametestOnReturn valuefalse/ !-- 打开PSCache并且指定每个连接上PSCache的大小 -- property namepoolPreparedStatements valuetrue/ property namemaxOpenPreparedStatements value20/ /bean bean idjdbcTemplate classorg.springframework.jdbc.core.JdbcTemplate property namedataSource refdataSource/property /bean bean idhsLogDao classcom.huifer.source.spring.dao.impl.HsLogDaoImpl property namejdbcTemplate refjdbcTemplate/ /bean /beans这份 XML 中 Druid 连接池的每个参数都值得展开说明它们直接决定连接池在真实生产环境中的行为参数值作用filtersstat开启监控统计拦截器用于统计 SQL 执行性能maxActive20连接池最大活跃连接数initialSize1初始化时创建的连接数minIdle1池中最小空闲连接数maxWait60000获取连接时的最大等待时间毫秒超过则抛异常timeBetweenEvictionRunsMillis60000每隔多少毫秒执行一次空闲连接检测minEvictableIdleTimeMillis300000连接在池中最小空闲生存时间毫秒超过即被回收testWhileIdletrue空闲检测时校验连接是否可用不占用连接testOnBorrowfalse借出连接时是否校验为true会降低性能testOnReturnfalse归还连接时是否校验poolPreparedStatementstrue开启 PSCachePreparedStatement 缓存maxOpenPreparedStatements20每个连接上 PSCache 的大小1.6 运行方法public class SpringJDBCSourceCode { public static void main(String[] args) { ApplicationContext applicationContext new ClassPathXmlApplicationContext(JDBC-demo.xml); HsLogDaoImpl bean applicationContext.getBean(HsLogDaoImpl.class); System.out.println(bean.findAll()); HsLog hsLog new HsLog(); hsLog.setSource(jlkjll); bean.save(hsLog); } }整个启动过程复用 IoC 容器的能力ClassPathXmlApplicationContext读取JDBC-demo.xml完成属性占位符解析、DruidDataSource初始化init-methodinit、JdbcTemplate与 DAO 的依赖注入之后即可直接调用findAll()与save()。IoC 的加载细节可参考仓库内的 BeanDefinition 的资源定位过程 与 依赖注入(DI).md)。二、链接对象构造DataSourceUtils.getConnection 的完整逻辑第一次调用findAll()时JdbcTemplate会先获取数据库连接入口是Connection con DataSourceUtils.getConnection(obtainDataSource());其中obtainDataSource()返回JdbcTemplate持有的DataSource即上文注入的DruidDataSource。getConnection的源码如下public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { try { return doGetConnection(dataSource); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException(Failed to obtain JDBC Connection, ex); } catch (IllegalStateException ex) { throw new CannotGetJdbcConnectionException(Failed to obtain JDBC Connection: ex.getMessage()); } }可见getConnection本身只是对doGetConnection的封装并将底层的SQLException/IllegalStateException统一转换为 Spring 的CannotGetJdbcConnectionException这是 Spring 统一异常翻译体系的一部分。org.springframework.jdbc.datasource.DataSourceUtils#doGetConnection真正获取连接的核心方法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(fetchConnection(dataSource)); } return conHolder.getConnection(); } // Else we either got no holder or an empty thread-bound holder here. logger.debug(Fetching JDBC Connection from DataSource); // 获取链接 Connection con fetchConnection(dataSource); // 当前线程支持同步 if (TransactionSynchronizationManager.isSynchronizationActive()) { try { // Use same Connection for further JDBC actions within the transaction. // Thread-bound object will get removed by synchronization at transaction completion. // 在同一个事物中使用同一个链接对象 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); } } catch (RuntimeException ex) { // Unexpected exception from external delegation call - close Connection and rethrow. releaseConnection(con, dataSource); throw ex; } } return con; }逐段拆解这段代码的逻辑命中线程绑定的连接TransactionSynchronizationManager.getResource(dataSource)从当前线程的ThreadLocal资源表中取出与该DataSource关联的ConnectionHolder。如果存在且持有连接或已与事务同步说明当前线程已经处于某个事务/同步上下文中直接复用该连接并requested()增加一次引用计数后返回。这保证了同一事务内多次 JDBC 操作使用同一条物理连接未命中则新建调用fetchConnection(dataSource)从连接池此处为 Druid真正取出物理连接同步上下文注册如果TransactionSynchronizationManager.isSynchronizationActive()为true说明当前线程处于事务同步中则把连接包装进ConnectionHolder调用requested()增加引用计数注册ConnectionSynchronization用于事务结束时自动释放并通过bindResource(dataSource, holderToUse)绑定到当前线程同时设置synchronizedWithTransaction true失败回滚若上述绑定过程抛出运行时异常立即releaseConnection(con, dataSource)释放连接并重新抛出异常避免连接泄漏。这里的ConnectionHolder与TransactionSynchronizationManager是 Spring 事务与 JDBC 之间的关键桥梁。仓库内的 Spring 事务管理器的设计与实现 中DataSourceTransactionManager.doBegin()正是通过相同的getResource/bindResource机制把事务连接绑定到线程实现「一个事务一条连接」的语义。三、释放资源releaseConnection 与引用计数查询/更新完成后JdbcTemplate会调用releaseConnection(con, dataSource);入口方法public static void releaseConnection(Nullable Connection con, Nullable DataSource dataSource) { try { doReleaseConnection(con, dataSource); } catch (SQLException ex) { logger.debug(Could not close JDBC Connection, ex); } catch (Throwable ex) { logger.debug(Unexpected exception on closing JDBC Connection, ex); } }同样是一层异常吞噬的封装——连接释放的失败以 debug 日志记录不向调用方抛出避免掩盖主流程异常。核心逻辑在doReleaseConnectionpublic static void doReleaseConnection(Nullable Connection con, Nullable DataSource dataSource) throws SQLException { if (con null) { return; } if (dataSource ! null) { ConnectionHolder conHolder (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder ! null connectionEquals(conHolder, con)) { // Its the transactional Connection: Dont close it. // 连接数-1 conHolder.released(); return; } } // 处理其他情况 doCloseConnection(con, dataSource); }关键分支事务连接不真正关闭如果该连接与线程绑定的ConnectionHolder持有的是同一条连接说明它属于当前事务不能关闭只执行conHolder.released()使引用计数减一。真正的关闭要等事务提交/回滚完成由事务同步机制统一处理普通连接直接关闭不属于任何事务绑定场景的连接走doCloseConnection(con, dataSource)调用con.close()归还给连接池。org.springframework.transaction.support.ResourceHolderSupport引用计数ConnectionHolder继承自ResourceHolderSupport其中维护了一个整型referenceCount用于引用计数/** * Increase the reference count by one because the holder has been requested * (i.e. someone requested the resource held by it). */ public void requested() { this.referenceCount; } /** * Decrease the reference count by one because the holder has been released * (i.e. someone released the resource held by it). */ public void released() { this.referenceCount--; }这一机制配合上一节的doGetConnection每次从线程绑定资源中获取连接时requested()加一每次doReleaseConnection发现是事务连接时released()减一。只有引用计数归零且事务同步结束时连接才会真正被关闭。这与 Spring 事务解析 中DataSourceTransactionManager的提交/回滚收尾逻辑unbindResource、releaseConnection形成完整闭环。四、查询解析JdbcTemplate#query 的完整调用链4.1 JdbcTemplate 的依赖注入入口bean idjdbcTemplate classorg.springframework.jdbc.core.JdbcTemplate property namedataSource refdataSource/property /bean从配置可知JdbcTemplate需要一个dataSource属性依赖注入的落点在其父类org.springframework.jdbc.support.JdbcAccessor#setDataSourcepublic void setDataSource(Nullable DataSource dataSource) { this.dataSource dataSource; }这段代码只做了简单的赋值依赖注入dataSource字段随后被obtainDataSource()读取。DAO 侧的jdbcTemplate注入同理属于 IoC 容器的常规依赖注入范畴本文不再展开。4.2 查询入口query(String, RowMapper)Override public ListHsLog findAll() { return this.getJdbcTemplate().query(select * from hs_log, new HsLogRowMapper()); }JdbcTemplate的query(String sql, RowMapperT)最终会先把RowMapper包装为RowMapperResultSetExtractor再调用接收ResultSetExtractor的重载方法Override Nullable public T T query(final String sql, final ResultSetExtractorT rse) throws DataAccessException { Assert.notNull(sql, SQL must not be null); Assert.notNull(rse, ResultSetExtractor must not be null); if (logger.isDebugEnabled()) { logger.debug(Executing SQL query [ sql ]); } /** * Callback to execute the query. */ class QueryStatementCallback implements StatementCallbackT, SqlProvider { Override Nullable public T doInStatement(Statement stmt) throws SQLException { ResultSet rs null; try { // 执行sql rs stmt.executeQuery(sql); // 1. org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData return rse.extractData(rs); } finally { JdbcUtils.closeResultSet(rs); } } Override public String getSql() { return sql; } } return execute(new QueryStatementCallback()); }这里体现了 Spring JDBC 的核心设计思想——模板方法 回调框架负责连接获取、Statement创建、设置、关闭、警告处理、异常翻译等样板代码业务代码通过StatementCallback此处为匿名内部类QueryStatementCallback只关心「拿到Statement后做什么」调用stmt.executeQuery(sql)执行 SQL再把ResultSet交给ResultSetExtractor提取数据finally中通过JdbcUtils.closeResultSet(rs)关闭结果集。4.3 执行核心execute(StatementCallback)Override Nullable public T T execute(StatementCallbackT action) throws DataAccessException { Assert.notNull(action, Callback object must not be null); Connection con DataSourceUtils.getConnection(obtainDataSource()); Statement stmt null; try { stmt con.createStatement(); applyStatementSettings(stmt); // 执行 T result action.doInStatement(stmt); handleWarnings(stmt); return result; } catch (SQLException ex) { // Release Connection early, to avoid potential connection pool deadlock // in the case when the exception translator hasnt been initialized yet. String sql getSql(action); JdbcUtils.closeStatement(stmt); stmt null; DataSourceUtils.releaseConnection(con, getDataSource()); con null; throw translateException(StatementCallback, sql, ex); } finally { JdbcUtils.closeStatement(stmt); DataSourceUtils.releaseConnection(con, getDataSource()); } }execute是模板方法的中心枢纽流程为DataSourceUtils.getConnection(obtainDataSource())获取连接即第二节分析的连接构造逻辑con.createStatement()创建StatementapplyStatementSettings(stmt)应用查询超时、获取大小等设置调用action.doInStatement(stmt)执行回调执行 SQL 提取结果handleWarnings(stmt)处理 SQL 警告异常分支捕获SQLException后先释放连接防止连接池死锁再通过translateException(StatementCallback, sql, ex)把原生异常翻译为 Spring 的DataAccessExceptionfinally 分支无论成功失败都JdbcUtils.closeStatement(stmt)关闭 Statement并DataSourceUtils.releaseConnection(con, getDataSource())释放连接即第三节的释放逻辑。4.4 结果提取RowMapperResultSetExtractor.extractData回调中调用的rse.extractData(rs)落到RowMapperResultSetExtractorOverride public ListT extractData(ResultSet rs) throws SQLException { ListT results (this.rowsExpected 0 ? new ArrayList(this.rowsExpected) : new ArrayList()); int rowNum 0; while (rs.next()) { // 调用自定义的 rowMapper 进行数据处理 T t this.rowMapper.mapRow(rs, rowNum); results.add(t); } return results; }这段代码有三个细节值得注意容量预分配如果构造时指定了rowsExpected 0则ArrayList按预期行数预分配容量避免查询大结果集时频繁扩容逐行回调while (rs.next())遍历ResultSet每行调用一次用户自定义的rowMapper.mapRow(rs, rowNum)行号从 0 递增此处正是HsLogDaoImpl.HsLogRowMapper被回调的位置返回 List所有映射后的对象汇总为ListT返回。上图是extractData方法在真实运行时的调试界面rs为 Druid 连接池包装的结果集对象DruidPooledResultSetrowMapper为HsLogDaoImpl.HsLogRowMappert为当前行映射出的HsLog对象id99results正在逐步收集映射结果——直观印证了「遍历 ResultSet → RowMapper 逐行映射 → 收集为 List」的完整过程。查询链路的最后一步是返回值校验private static T T result(Nullable T result) { Assert.state(result ! null, No result); return result; }result方法对提取结果做了空值断言后原样返回保证query不会返回null。至此findAll()的完整链路为JdbcTemplate.query→execute(QueryStatementCallback)→DataSourceUtils.getConnection获取连接 →stmt.executeQuery(sql)→RowMapperResultSetExtractor.extractData→RowMapper.mapRow逐行映射 →DataSourceUtils.releaseConnection释放连接。五、插入解析JdbcTemplate#update 的完整调用链与查询对应写入操作的入口是JdbcTemplate.updateOverride public void save(HsLog hsLog) { this.getJdbcTemplate().update(insert into hs_log (SOURCE) values(?) , new Object[]{ hsLog.getSource(), } ); }update(String sql, Object... args)内部会创建PreparedStatementCreator与PreparedStatementSetter最终落到如下方法protected int update(final PreparedStatementCreator psc, Nullable final PreparedStatementSetter pss) throws DataAccessException { logger.debug(Executing prepared SQL update); return updateCount(execute(psc, ps - { try { if (pss ! null) { // 设置请求参数 pss.setValues(ps); } int rows ps.executeUpdate(); if (logger.isTraceEnabled()) { logger.trace(SQL update affected rows rows); } return rows; } finally { if (pss instanceof ParameterDisposer) { ((ParameterDisposer) pss).cleanupParameters(); } } })); }与查询链路对比update同样是「模板方法 回调」结构execute(psc, ...)负责通过PreparedStatementCreator创建PreparedStatement、应用设置、执行回调、finally 中释放资源、异常翻译——与查询版execute的骨架一致只是从Statement换成了PreparedStatement从而支持?占位符与参数预编译回调内部如果pss非空先调用pss.setValues(ps)为占位符绑定参数此处把hsLog.getSource()绑定到第一个?然后执行ps.executeUpdate()得到受影响行数finally中若pss实现了ParameterDisposer接口则调用cleanupParameters()清理参数防止大对象如 BLOB资源泄漏最后updateCount(...)对execute的返回值做空值断言并返回受影响行数。因此save()的完整链路为JdbcTemplate.update→execute(PreparedStatementCreator, PreparedStatementCallback)→DataSourceUtils.getConnection获取连接 →PreparedStatementSetter.setValues绑定参数 →ps.executeUpdate()执行写入 → 统计受影响行数 →DataSourceUtils.releaseConnection释放连接。六、与事务管理的联动为什么连接能被复用阅读第二节、第三节时可以看到ConnectionHolder、TransactionSynchronizationManager、引用计数这些机制反复出现它们正是 Spring JDBC 与 Spring 事务无缝衔接的枢纽。仓库内的 Spring 事务管理器的设计与实现 展示了事务侧如何使用同一套机制事务开启doBegin时DataSourceTransactionManager调用TransactionSynchronizationManager.getResource(dataSource)获取ConnectionHolder若没有则新建ConnectionHolder(newCon)并setTransactionActive(true)最后通过bindResource(getDataSource(), txObject.getConnectionHolder())绑定到当前线程事务提交/回滚doCommit/doRollback时从TransactionSynchronizationManager.unbindResource(obtainDataSource())解绑资源再DataSourceUtils.releaseConnection(con, this.dataSource)释放连接。对照本文第二节的doGetConnection可以清晰看到闭环事务开启时连接被绑定到线程之后JdbcTemplate每次getConnection都会命中同一个ConnectionHolder从而在同一事务内的多条 SQL 共享同一条物理连接事务结束时统一解绑并释放。更完整的事务细节可继续阅读 Spring 与事务处理、Spring 事务解析 与 Spring 声明式事务处理。七、小结本文沿 source-code-hunter 仓库 Spring-jdbc.md 的脉络完成了从工程搭建到源码剖析的全流程工程层基于spring-jdbc Druid MySQL 搭建最小可运行工程掌握了实体、DAO、XML 装配与连接池关键参数连接生命周期DataSourceUtils.getConnection→doGetConnection负责获取连接并与事务同步绑定releaseConnection→doReleaseConnection负责释放事务连接只减引用计数不真正关闭查询链路query→execute(StatementCallback)→RowMapperResultSetExtractor.extractData→RowMapper.mapRow模板方法 回调的设计让业务只关注 SQL 与行映射更新链路update→execute(PreparedStatementCreator, ...)→PreparedStatementSetter.setValues→executeUpdate占位符参数在回调中统一绑定事务联动ConnectionHolder引用计数 TransactionSynchronizationManager线程绑定是「一个事务一条连接」的底层保障。理解了这套链路之后无论你是想深入 Spring 事务、还是借鉴其「模板方法 回调」思想阅读 MyBatis、Netty 等框架源码都能举一反三。【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →