zoukankan      html  css  js  c++  java
  • 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,一个在学习路上匍匐前行的小菜鸟!

  • 相关阅读:
    GUID
    Castle学习笔记二:框架结构及配置
    Linux编程基础
    Linux文件编程
    单臂路由配置实例
    CCNA学习笔记#02第7章操作与配置Cisco IOS
    Linux基本命令
    Linux系统中rar、unrar命令安装和使用详解
    批处理设置IP与DNS服务器
    Linux下载安装5笔输入法
  • 原文地址:https://www.cnblogs.com/daimzh/p/13353007.html
Copyright © 2011-2022 走看看