转载

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

Spring中的数据访问,JdbcTemplate使用及源码分析

前言

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

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

image-20200718220712800

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

  1. JdbcTemplate 使用及源码分析
  2. Mybatis 的基本使用及Spring对 Mybatis 的整合

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

JdbcTemplate使用示例

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 + "条数据");
 }
}

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

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

image-20200708153438245

配置文件如下:

<?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条数据

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

image-20200708153656393

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

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

JdbcTemplate源码分析

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

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);
}

query方法分析

// 第一步,对传入的参数进行封装,将参数封装成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方法分析

// 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());
    }
}

1、获取数据库连接

对应源码如下:

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;
}

2、应用用户设定的参数

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());
}

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

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

3、执行Sql

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

4、处理警告

也没啥好说的,处理 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());
    }
}

5、关闭资源

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

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。

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

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

本文使用 mdnice 排版

原文  https://segmentfault.com/a/1190000023312939
正文到此结束
Loading...