Spring中的資料訪問,JdbcTemplate使用及原始碼分析
前言
本系列文章為事務專欄分析文章,整個事務分析專題將按下面這張圖完成
對原始碼分析前,我希望先介紹一下Spring中資料訪問的相關內容,然後層層遞進到事物的原始碼分析,主要分為兩個部分
JdbcTemplate
使用及原始碼分析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 + "條資料");
}
}
資料庫中目前只有一條資料:
配置檔案如下:
<?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> 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());
}
從上面程式碼可以看出,主要設立了兩個引數
fetchSize
:該引數的設計目的主要是為了減少網路互動,當訪問ResultSet
的時候,如果它每次只從伺服器讀取一條資料,則會產生大量的開銷,setFetchSize
的含義在於,當呼叫rs.next
時,它可以直接從記憶體中獲取而不需要網路互動,提高了效率。這個設定可能會被某些JDBC
驅動忽略,而且設定過大會造成記憶體上升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、關閉資源
最終會呼叫到DataSourceUtils
的doReleaseConnection
方法,原始碼如下:
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,一個在學習路上匍匐前行的小菜鳥!