Spring中的数据访问,JdbcTemplate使用及源码分析
本系列文章为事务专栏分析文章,整个事务分析专题将按下面这张图完成
对源码分析前,我希望先介绍一下Spring中数据访问的相关内容,然后层层递进到事物的源码分析,主要分为两个部分
JdbcTemplate
使用及源码分析 Mybatis
的基本使用及Spring对 Mybatis
的整合 本文将要介绍的是第一点。
public class DmzService { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { jdbcTemplate = new JdbcTemplate(dataSource); } /** * 查询 * @param id 根据id查询 * @return 对应idd的user对象 */ public User getUserById(int id) { return jdbcTemplate .queryForObject("select * from `user` where id = ?", new RowMapper<User>() { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setId(rs.getInt("id")); user.setAge(rs.getInt("age")); user.setName(rs.getString("name")); return user; } }, id); } public int saveUser(User user){ return jdbcTemplate.update("insert into user values(?,?,?)", new Object[]{user.getId(),user.getName(),user.getAge()}); } }
public class Main { public static void main(String[] args) { ClassPathXmlApplicationContext cc = new ClassPathXmlApplicationContext("tx.xml"); DmzService dmzService = cc.getBean(DmzService.class); User userById = dmzService.getUserById(1); System.out.println("查询的数据为:" + userById); userById.setId(userById.getId() + 1); int i = dmzService.saveUser(userById); System.out.println("插入了" + i + "条数据"); } }
数据库中目前只有一条数据:
配置文件如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource"> <property name="password" value="123"/> <property name="username" value="root"/> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC"/> </bean> <bean id="dmzService" class="com.dmz.spring.tx.service.DmzService"> <property name="dataSource" ref="dataSource"/> </bean> </beans>
程序允许结果:
查询的数据为:User{id=1, name='dmz', age=18} 插入了1条数据
运行后数据库中确实插入了一条数据
对于 JdbcTemplate
的简单使用,建议大家还是要有一定熟悉,虽然我现在在项目中不会直接使用 JdbcTemplate
的API。本文关于使用不做过多介绍,主要目的是分析它底层的源码
我们直接以其 queryForObject
方法为入口,对应源码如下:
public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException { // 核心在这个query方法中 List<T> results = query(sql, args, new RowMapperResultSetExtractor<>(rowMapper, 1)); // 这个方法很简单,就是返回结果集中的数据 // 如果少于1条或者多余1条都报错 return DataAccessUtils.nullableSingleResult(results); }
// 第一步,对传入的参数进行封装,将参数封装成ArgumentPreparedStatementSetter public <T> T query(String sql, @Nullable Object[] args, ResultSetExtractor<T> rse) throws DataAccessException { return query(sql, newArgPreparedStatementSetter(args), rse); } // 第二步:对sql语句进行封装,将sql语句封装成SimplePreparedStatementCreator public <T> T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor<T> rse) throws DataAccessException { return query(new SimplePreparedStatementCreator(sql), pss, rse); } public <T> T query( PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor<T> rse) throws DataAccessException { // query方法在完成对参数及sql语句的封装后,直接调用了execute方法 // execute方法是jdbcTemplate的基本API,不管是查询、更新还是保存 // 最终都会进入到这个方法中 return execute(psc, new PreparedStatementCallback<T>() { @Override @Nullable public T doInPreparedStatement(PreparedStatement ps) throws SQLException { ResultSet rs = null; try { if (pss != null) { pss.setValues(ps); } rs = ps.executeQuery(); return rse.extractData(rs); } finally { JdbcUtils.closeResultSet(rs); if (pss instanceof ParameterDisposer) { ((ParameterDisposer) pss).cleanupParameters(); } } } }); }
// execute方法封装了一次数据库访问的基本操作 // 例如:获取连接,释放连接等 // 其定制化操作是通过传入的PreparedStatementCallback参数来实现的 public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action) throws DataAccessException { // 1.获取数据库连接 Connection con = DataSourceUtils.getConnection(obtainDataSource()); PreparedStatement ps = null; try { // 2.获取一个PreparedStatement,并应用用户设定的参数 ps = psc.createPreparedStatement(con); applyStatementSettings(ps); // 3.执行sql并返回结果 T result = action.doInPreparedStatement(ps); // 4.处理警告 handleWarnings(ps); return result; } catch (SQLException ex) { // 出现异常的话,需要关闭数据库连接 if (psc instanceof ParameterDisposer) { ((ParameterDisposer) psc).cleanupParameters(); } String sql = getSql(psc); psc = null; JdbcUtils.closeStatement(ps); ps = null; DataSourceUtils.releaseConnection(con, getDataSource()); con = null; throw translateException("PreparedStatementCallback", sql, ex); } finally { // 关闭资源 if (psc instanceof ParameterDisposer) { ((ParameterDisposer) psc).cleanupParameters(); } JdbcUtils.closeStatement(ps); DataSourceUtils.releaseConnection(con, getDataSource()); } }
对应源码如下:
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { // 这里省略了异常处理 // 直接调用了doGetConnection方法 return doGetConnection(dataSource); }
doGetConnection
方法是最终获取连接的方法
public static Connection doGetConnection(DataSource dataSource) throws SQLException { Assert.notNull(dataSource, "No DataSource specified"); // 如果使用了事务管理器来对事务进行管理(申明式事务跟编程式事务都依赖于事务管理器) // 那么在开启事务时,Spring会提前绑定一个数据库连接到当前线程中 // 这里做的就是从当前线程中获取对应的连接池中的连接 ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { // 记录当前这个连接被使用的次数,每次调用+1 conHolder.requested(); if (!conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(fetchConnection(dataSource)); } return conHolder.getConnection(); } Connection con = fetchConnection(dataSource); // 如果开启了一个空事务(例如事务的传播级别设置为SUPPORTS时,就会开启一个空事务) // 会激活同步,那么在这里需要将连接绑定到当前线程 if (TransactionSynchronizationManager.isSynchronizationActive()) { try { ConnectionHolder holderToUse = conHolder; if (holderToUse == null) { holderToUse = new ConnectionHolder(con); } else { holderToUse.setConnection(con); } // 当前连接被使用的次数+1(能进入到这个方法,说明这个连接是刚刚从连接池中获取到) // 当释放资源时,只有被使用的次数归为0时才放回到连接池中 holderToUse.requested(); TransactionSynchronizationManager.registerSynchronization( new ConnectionSynchronization(holderToUse, dataSource)); holderToUse.setSynchronizedWithTransaction(true); if (holderToUse != conHolder) { TransactionSynchronizationManager.bindResource(dataSource, holderToUse); } } catch (RuntimeException ex) { // 出现异常时释放连接,如果开启了事务,不会真正调用close方法关闭连接 // 而是把当前连接的使用数-1 releaseConnection(con, dataSource); throw ex; } } return con; }
protected void applyStatementSettings(Statement stmt) throws SQLException { int fetchSize = getFetchSize(); if (fetchSize != -1) { stmt.setFetchSize(fetchSize); } int maxRows = getMaxRows(); if (maxRows != -1) { stmt.setMaxRows(maxRows); } DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout()); }
从上面代码可以看出,主要设立了两个参数
fetchSize
:该参数的设计目的主要是为了减少网络交互,当访问 ResultSet
的时候,如果它每次只从服务器读取一条数据,则会产生大量的开销, setFetchSize
的含义在于,当调用 rs.next
时,它可以直接从内存中获取而不需要网络交互,提高了效率。这个设置可能会被某些 JDBC
驱动忽略,而且设置过大会造成内存上升 setMaxRows
,是将此 Statement
生成的所有 ResultSet
的最大返回行数限定为指定数,作用类似于limit。 没啥好说的,底层其实就是调用了 jdbc
的一系列 API
也没啥好说的,处理 Statement
中的警告信息
protected void handleWarnings(Statement stmt) throws SQLException { if (isIgnoreWarnings()) { if (logger.isDebugEnabled()) { SQLWarning warningToLog = stmt.getWarnings(); while (warningToLog != null) { logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() + "', error code '" + warningToLog.getErrorCode() + "', message [" + warningToLog.getMessage() + "]"); warningToLog = warningToLog.getNextWarning(); } } } else { handleWarnings(stmt.getWarnings()); } }
最终会调用到 DataSourceUtils
的 doReleaseConnection
方法,源码如下:
public 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)) { // 说明开启了事务,那么不会调用close方法,之后将连接的占用数减1 conHolder.released(); return; } } // 调用close方法关闭连接 doCloseConnection(con, dataSource); }
总的来说,这篇文章涉及到的内容都是比较简单的,通过这篇文章是希望让大家对Spring中的数据访问有一定了解,相当于热身吧,后面的文章难度会加大,下篇文章我们将介绍更高级的数据访问, myBatis
的使用以及基本原理、事务管理以及它跟Spring的整合原理。
如果本文对你由帮助的话,记得点个赞吧!也欢迎关注我的公众号,微信搜索:程序员DMZ,或者扫描下方二维码,跟着我一起认认真真学Java,踏踏实实做一个coder。
我叫DMZ,一个在学习路上匍匐前行的小菜鸟!