飞道的博客

Spring事务源码分析专题(一)JdbcTemplate使用及源码分析

411人阅读  评论(0)

目录

前言

JdbcTemplate使用示例

JdbcTemplate源码分析

queryForObject方法分析

query方法分析

execute方法分析

1、获取数据库连接

2、应用用户设定的参数

3、执行Sql

4、处理警告

5、关闭资源

总结

前言

本系列文章为事务专栏分析文章,整个事务分析专题将按下面这张图完成

对源码分析前,我希望先介绍一下Spring中数据访问的相关内容,然后层层递进到事物的源码分析,主要分为两个部分

  1. JdbcTemplate使用及源码分析

  2. Mybatis的基本使用及Spring对Mybatis的整合

本文将要介绍的是第一点。

JdbcTemplate使用示例


   
  1. public class DmzService {
  2.  private JdbcTemplate jdbcTemplate;
  3.  public void setDataSource(DataSource dataSource) {
  4.   jdbcTemplate =  new JdbcTemplate(dataSource);
  5.  }
  6.   /**
  7.   * 查询
  8.   * @param id 根据id查询
  9.   * @return 对应idd的user对象
  10.   */
  11.  public User getUserById( int id) {
  12.    return jdbcTemplate
  13.     .queryForObject( "select * from `user` where id  =  ?"new RowMapper<User>() {
  14.      @Override
  15.      public User mapRow(ResultSet rs,  int rowNum) throws SQLException {
  16.       User user =  new User();
  17.       user.setId(rs.getInt( "id"));
  18.       user.setAge(rs.getInt( "age"));
  19.       user.setName(rs.getString( "name"));
  20.        return user;
  21.      }
  22.     }, id);
  23.  }
  24.  public  int saveUser(User user){
  25.    return jdbcTemplate.update( "insert into user values(?,?,?)",
  26.      new Object[]{user.getId(),user.getName(),user.getAge()});
  27.  }
  28. }

   
  1. public class Main {
  2.  public static void main(String[] args) {
  3.   ClassPathXmlApplicationContext cc =  new ClassPathXmlApplicationContext( "tx.xml");
  4.   DmzService dmzService = cc.getBean(DmzService.class);
  5.   User userById = dmzService.getUserById( 1);
  6.   System.out. println( "查询的数据为:" + userById);
  7.   userById.setId(userById.getId() +  1);
  8.    int i = dmzService.saveUser(userById);
  9.   System.out. println( "插入了" + i +  "条数据");
  10.  }
  11. }

数据库中目前只有一条数据:

image-20200708153438245

配置文件如下:


  
  1. <?xml version= "1.0" encoding= "UTF-8"?>
  2. <beans xmlns= "http://www.springframework.org/schema/beans"
  3.     xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
  4.     xsi:schemaLocation= "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
  5.  <bean class= "org.springframework.jdbc.datasource.DriverManagerDataSource" id= "dataSource">
  6.   <property name= "password" value= "123"/>
  7.   <property name= "username" value= "root"/>
  8.   <property name= "driverClassName" value= "com.mysql.jdbc.Driver"/>
  9.   <property name= "url"
  10.       value= "jdbc:mysql://localhost:3306/test?serverTimezone=UTC"/>
  11.  </bean>
  12.  <bean id= "dmzService" class= "com.dmz.spring.tx.service.DmzService">
  13.   <property name= "dataSource" ref= "dataSource"/>
  14.   </bean>
  15. </beans>

程序允许结果:


  
  1. 查询的数据为:User{id= 1, name= 'dmz', age= 18}
  2. 插入了 1条数据

运行后数据库中确实插入了一条数据

对于JdbcTemplate的简单使用,建议大家还是要有一定熟悉,虽然我现在在项目中不会直接使用JdbcTemplate的API。本文关于使用不做过多介绍,主要目的是分析它底层的源码

JdbcTemplate源码分析

我们直接以其queryForObject方法为入口,对应源码如下:

queryForObject方法分析


  
  1. public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException {
  2.    
  3.      // 核心在这个query方法中
  4.     List<T> results = query(sql, args,  new RowMapperResultSetExtractor<>(rowMapper,  1));
  5.    
  6.      // 这个方法很简单,就是返回结果集中的数据
  7.      // 如果少于1条或者多余1条都报错
  8.      return DataAccessUtils.nullableSingleResult(results);
  9. }

query方法分析


  
  1. // 第一步,对传入的参数进行封装,将参数封装成ArgumentPreparedStatementSetter
  2. public <T> T query(String sql, @Nullable Object[] args, ResultSetExtractor<T> rse) throws DataAccessException {
  3.      return query(sql, newArgPreparedStatementSetter(args), rse);
  4. }
  5. // 第二步:对sql语句进行封装,将sql语句封装成SimplePreparedStatementCreator
  6. public <T> T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor<T> rse) throws DataAccessException {
  7.      return query( new SimplePreparedStatementCreator(sql), pss, rse);
  8. }
  9. public <T> T query(
  10.     PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor<T> rse)
  11.     throws DataAccessException {
  12.   // query方法在完成对参数及sql语句的封装后,直接调用了execute方法
  13.      // execute方法是jdbcTemplate的基本API,不管是查询、更新还是保存
  14.      // 最终都会进入到这个方法中
  15.      return execute(psc,  new PreparedStatementCallback<T>() {
  16.         @Override
  17.         @Nullable
  18.         public T doInPreparedStatement(PreparedStatement ps) throws SQLException {
  19.             ResultSet rs = null;
  20.             try {
  21.                  if (pss != null) {
  22.                     pss.setValues(ps);
  23.                 }
  24.                 rs = ps.executeQuery();
  25.                  return rse.extractData(rs);
  26.             }
  27.             finally {
  28.                 JdbcUtils.closeResultSet(rs);
  29.                  if (pss instanceof ParameterDisposer) {
  30.                     ((ParameterDisposer) pss).cleanupParameters();
  31.                 }
  32.             }
  33.         }
  34.     });
  35. }

execute方法分析


  
  1. // execute方法封装了一次数据库访问的基本操作
  2. // 例如:获取连接,释放连接等
  3. // 其定制化操作是通过传入的PreparedStatementCallback参数来实现的
  4. public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
  5.     throws DataAccessException {
  6.   // 1.获取数据库连接
  7.     Connection con = DataSourceUtils.getConnection(obtainDataSource());
  8.     PreparedStatement ps = null;
  9.     try {
  10.          // 2.获取一个PreparedStatement,并应用用户设定的参数
  11.         ps = psc.createPreparedStatement(con);
  12.         applyStatementSettings(ps);
  13.          // 3.执行sql并返回结果
  14.         T result = action.doInPreparedStatement(ps);
  15.          // 4.处理警告
  16.         handleWarnings(ps);
  17.          return result;
  18.     }
  19.     catch (SQLException ex) {
  20.         // 出现异常的话,需要关闭数据库连接
  21.          if (psc instanceof ParameterDisposer) {
  22.             ((ParameterDisposer) psc).cleanupParameters();
  23.         }
  24.         String sql = getSql(psc);
  25.         psc = null;
  26.         JdbcUtils.closeStatement(ps);
  27.         ps = null;
  28.         DataSourceUtils.releaseConnection(con, getDataSource());
  29.         con = null;
  30.         throw translateException( "PreparedStatementCallback", sql, ex);
  31.     }
  32.     finally {
  33.          // 关闭资源
  34.          if (psc instanceof ParameterDisposer) {
  35.             ((ParameterDisposer) psc).cleanupParameters();
  36.         }
  37.         JdbcUtils.closeStatement(ps);
  38.         DataSourceUtils.releaseConnection(con, getDataSource());
  39.     }
  40. }

1、获取数据库连接

对应源码如下:


  
  1. public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
  2.      // 这里省略了异常处理
  3.      // 直接调用了doGetConnection方法
  4.      return doGetConnection(dataSource); 
  5.   
  6. }

doGetConnection方法是最终获取连接的方法


  
  1. public static Connection doGetConnection(DataSource dataSource) throws SQLException {
  2.     Assert.notNull(dataSource,  "No DataSource specified");
  3.      // 如果使用了事务管理器来对事务进行管理(申明式事务跟编程式事务都依赖于事务管理器)
  4.      // 那么在开启事务时,Spring会提前绑定一个数据库连接到当前线程中
  5.      // 这里做的就是从当前线程中获取对应的连接池中的连接
  6.     ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
  7.      if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
  8.          // 记录当前这个连接被使用的次数,每次调用+1
  9.         conHolder.requested();
  10.          if (!conHolder.hasConnection()) {
  11.             logger.debug( "Fetching resumed JDBC Connection from DataSource");
  12.             conHolder.setConnection(fetchConnection(dataSource));
  13.         }
  14.          return conHolder.getConnection();
  15.     }
  16.     Connection con = fetchConnection(dataSource);
  17.      // 如果开启了一个空事务(例如事务的传播级别设置为SUPPORTS时,就会开启一个空事务)
  18.      // 会激活同步,那么在这里需要将连接绑定到当前线程
  19.      if (TransactionSynchronizationManager.isSynchronizationActive()) {
  20.         try {
  21.             ConnectionHolder holderToUse = conHolder;
  22.              if (holderToUse == null) {
  23.                 holderToUse =  new ConnectionHolder(con);
  24.             }
  25.              else {
  26.                 holderToUse.setConnection(con);
  27.             }
  28.              // 当前连接被使用的次数+1(能进入到这个方法,说明这个连接是刚刚从连接池中获取到)
  29.              // 当释放资源时,只有被使用的次数归为0时才放回到连接池中
  30.             holderToUse.requested();
  31.             TransactionSynchronizationManager.registerSynchronization(
  32.                  new ConnectionSynchronization(holderToUse, dataSource));
  33.             holderToUse.setSynchronizedWithTransaction( true);
  34.              if (holderToUse != conHolder) {
  35.                 TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
  36.             }
  37.         }
  38.         catch (RuntimeException ex) {
  39.     // 出现异常时释放连接,如果开启了事务,不会真正调用close方法关闭连接
  40.              // 而是把当前连接的使用数-1
  41.             releaseConnection(con, dataSource);
  42.             throw ex;
  43.         }
  44.     }
  45.      return con;
  46. }

2、应用用户设定的参数


  
  1. protected void applyStatementSettings(Statement stmt) throws SQLException {
  2.      int fetchSize = getFetchSize();
  3.      if (fetchSize !=  -1) {
  4.         stmt.setFetchSize(fetchSize);
  5.     }
  6.      int maxRows = getMaxRows();
  7.      if (maxRows !=  -1) {
  8.         stmt.setMaxRows(maxRows);
  9.     }
  10.     DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout());
  11. }

从上面代码可以看出,主要设立了两个参数

  1. fetchSize:该参数的设计目的主要是为了减少网络交互,当访问ResultSet的时候,如果它每次只从服务器读取一条数据,则会产生大量的开销,setFetchSize的含义在于,当调用rs.next时,它可以直接从内存中获取而不需要网络交互,提高了效率。这个设置可能会被某些JDBC驱动忽略,而且设置过大会造成内存上升

  2. setMaxRows,是将此Statement生成的所有ResultSet的最大返回行数限定为指定数,作用类似于limit。

3、执行Sql

没啥好说的,底层其实就是调用了jdbc的一系列API

4、处理警告

也没啥好说的,处理Statement中的警告信息


  
  1. protected void handleWarnings(Statement stmt) throws SQLException {
  2.      if (isIgnoreWarnings()) {
  3.          if (logger.isDebugEnabled()) {
  4.             SQLWarning warningToLog = stmt.getWarnings();
  5.             while (warningToLog != null) {
  6.                 logger.debug( "SQLWarning ignored: SQL state '" + warningToLog.getSQLState() +  "', error code '" +
  7.                              warningToLog.getErrorCode() +  "', message [" + warningToLog.getMessage() +  "]");
  8.                 warningToLog = warningToLog.getNextWarning();
  9.             }
  10.         }
  11.     }
  12.      else {
  13.         handleWarnings(stmt.getWarnings());
  14.     }
  15. }

5、关闭资源

最终会调用到DataSourceUtilsdoReleaseConnection方法,源码如下:


  
  1. public static void doReleaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) throws SQLException {
  2.      if (con == null) {
  3.          return;
  4.     }
  5.      if (dataSource != null) {
  6.         ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
  7.          if (conHolder != null && connectionEquals(conHolder, con)) {
  8.              // 说明开启了事务,那么不会调用close方法,之后将连接的占用数减1
  9.             conHolder.released();
  10.              return;
  11.         }
  12.     }
  13.      // 调用close方法关闭连接
  14.     doCloseConnection(con, dataSource);
  15. }

总结

总的来说,这篇文章涉及到的内容都是比较简单的,通过这篇文章是希望让大家对Spring中的数据访问有一定了解,相当于热身吧,后面的文章难度会加大,下篇文章我们将介绍更高级的数据访问,myBatis的使用以及基本原理、事务管理以及它跟Spring的整合原理。

如果本文对你有帮助的话,记得点个赞吧!

我叫DMZ,一个在学习路上匍匐前行的小菜鸟!

Spring官网阅读笔记

Spring杂谈

JVM系列文章

Spring源码专题


转载:https://blog.csdn.net/qq_41907991/article/details/107479109
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场