jdbctemplate 执行多条sql_Spring事务源码分析专题(一)JdbcTemplate使用及源码分析...

点击上方 IT牧场 ,选择 置顶或者星标

技术干货每日送达!

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

前言

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

ec2dca63b870233af18ac79cbbfd5a18.png

对源码分析前,我希望先介绍一下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() {@Overridepublic 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 + "条数据");
 }
}

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

4d0af93bc4c9dfb9bb9427f013b73be1.png
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条数据
0f22edef6f2f227a7dfa7ab3780e23bd.png

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

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

JdbcTemplate源码分析

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

queryForObject方法分析

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

query方法分析

// 第一步,对传入的参数进行封装,将参数封装成ArgumentPreparedStatementSetter
public  T query(String sql, @Nullable Object[] args, ResultSetExtractor rse) throws DataAccessException {return query(sql, newArgPreparedStatementSetter(args), rse);
}// 第二步:对sql语句进行封装,将sql语句封装成SimplePreparedStatementCreatorpublic  T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor rse) throws DataAccessException {return query(new SimplePreparedStatementCreator(sql), pss, rse);
}public  T query(
    PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor rse)throws DataAccessException {// query方法在完成对参数及sql语句的封装后,直接调用了execute方法// execute方法是jdbcTemplate的基本API,不管是查询、更新还是保存// 最终都会进入到这个方法中return execute(psc, new PreparedStatementCallback() {@Override@Nullablepublic 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 execute(PreparedStatementCreator psc, PreparedStatementCallback 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的整合原理。

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

干货分享

最近将个人学习笔记整理成册,使用PDF分享。关注我,回复如下代码,即可获得百度盘地址,无套路领取!

•001:《Java并发与高并发解决方案》学习笔记;•002:《深入JVM内核——原理、诊断与优化》学习笔记;•003:《Java面试宝典》•004:《Docker开源书》•005:《Kubernetes开源书》•006:《DDD速成(领域驱动设计速成)》•007:全部•008:加技术群讨论

近期热文

•LinkedBlockingQueue vs ConcurrentLinkedQueue•解读Java 8 中为并发而生的 ConcurrentHashMap•Redis性能监控指标汇总•最全的DevOps工具集合,再也不怕选型了!•微服务架构下,解决数据库跨库查询的一些思路•聊聊大厂面试官必问的 MySQL 锁机制

关注我

6dd6733554607bf79f7532b4cd6bb4e0.png

喜欢就点个"在看"呗^_^

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值