1. 程式人生 > >Spring事務原始碼分析專題(一)JdbcTemplate使用及原始碼分析

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

Spring中的資料訪問,JdbcTemplate使用及原始碼分析 # 前言 本系列文章為事務專欄分析文章,整個事務分析專題將按下面這張圖完成 ![image-20200718220712800](https://gitee.com/wx_cc347be696/blogImage/raw/master/image-20200718220712800.png) 對原始碼分析前,我希望先介紹一下Spring中資料訪問的相關內容,然後層層遞進到事物的原始碼分析,主要分為兩個部分 1. `JdbcTemplate`使用及原始碼分析 2. `Mybatis`的基本使用及Spring對`Mybatis`的整合 本文將要介紹的是第一點。 # JdbcTemplate使用示例 ```java 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() { @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()}); } } ``` ```java 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](https://gitee.com/wx_cc347be696/blogImage/raw/master/image-20200708153438245.png) 配置檔案如下: ```xml
``` 程式允許結果: ``` 查詢的資料為:User{id=1, name='dmz', age=18} 插入了1條資料 ``` ![image-20200708153656393](https://gitee.com/wx_cc347be696/blogImage/raw/master/image-20200708153656393.png) 執行後資料庫中確實插入了一條資料 對於`JdbcTemplate`的簡單使用,建議大家還是要有一定熟悉,雖然我現在在專案中不會直接使用`JdbcTemplate`的API。本文關於使用不做過多介紹,主要目的是分析它底層的原始碼 # JdbcTemplate原始碼分析 我們直接以其`queryForObject`方法為入口,對應原始碼如下: ## queryForObject方法分析 ```java 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方法分析 ```java // 第一步,對傳入的引數進行封裝,將引數封裝成ArgumentPreparedStatementSetter public T query(String sql, @Nullable Object[] args, ResultSetExtractor rse) throws DataAccessException { return query(sql, newArgPreparedStatementSetter(args), rse); } // 第二步:對sql語句進行封裝,將sql語句封裝成SimplePreparedStatementCreator public 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 @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方法分析 ```java // 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、獲取資料庫連線 對應原始碼如下: ```java public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { // 這裡省略了異常處理 // 直接呼叫了doGetConnection方法 return doGetConnection(dataSource); } ``` `doGetConnection`方法是最終獲取連線的方法 ```java 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、應用使用者設定的引數 ```java 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`中的警告資訊 ```java 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、關閉資源 最終會呼叫到`DataSourceUtils`的`doReleaseConnection`方法,原始碼如下: ```java 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。 ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9naXRlZS5jb20vd3hfY2MzNDdiZTY5Ni9ibG9nSW1hZ2UvcmF3L21hc3Rlci8lRTUlODUlQUMlRTQlQkMlOTclRTUlOEYlQjcuanBn?x-oss-process=image/format,png) 我叫DMZ,一個在學習路上匍匐前行的小