Spring事務源碼分析專題(一)JdbcTemplate使用及源碼分析

點擊藍色「程序員DMZ 」關注我喲mysql

好看記得加個「星標」哈!程序員

Spring中的數據訪問,JdbcTemplate使用及源碼分析web

前言

本系列文章爲事務專欄分析文章,整個事務分析專題將按下面這張圖完成spring

對源碼分析前,我但願先介紹一下Spring中數據訪問的相關內容,而後層層遞進到事物的源碼分析,主要分爲兩個部分sql

  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條數據

運行後數據庫中確實插入了一條數據網絡

對於JdbcTemplate的簡單使用,建議你們仍是要有必定熟悉,雖然我如今在項目中不會直接使用JdbcTemplate的API。本文關於使用不作過多介紹,主要目的是分析它底層的源碼

JdbcTemplate源碼分析

咱們直接以其queryForObject方法爲入口,對應源碼以下:

queryForObject方法分析

public <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> query(String sql, @Nullable Object[] args, ResultSetExtractor<T> rse) throws DataAccessException {
    return query(sql, newArgPreparedStatementSetter(args), rse);
}

// 第二步:對sql語句進行封裝,將sql語句封裝成SimplePreparedStatementCreator
public <T> query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor<T> rse) throws DataAccessException {
    return query(new SimplePreparedStatementCreator(sql), pss, rse);
}


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

一、獲取數據庫鏈接

對應源碼以下:

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

二、應用用戶設定的參數

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。

三、執行Sql

沒啥好說的,底層其實就是調用了jdbc的一系列API

四、處理警告

也沒啥好說的,處理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());
    }
}

五、關閉資源

最終會調用到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,一個在學習路上匍匐前行的小菜鳥!

Spring官網閱讀筆記

Spring雜談

JVM系列文章

Spring源碼專題


本文分享自微信公衆號 - 程序員DMZ(programerDmz)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索