乐趣区

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

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

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

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。

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

本文应用 mdnice 排版

退出移动版