Spring源码分析八:JdbcTemplate 的源码分析

一、前言

本文是笔者阅读Spring源码的记录文章,由于本人技术水平有限,在文章中难免出现错误,如有发现,感谢各位指正。在阅读过程中也创建了一些衍生文章,衍生文章的意义是因为自己在看源码的过程中,部分知识点并不了解或者对某些知识点产生了兴趣,所以为了更好的阅读源码,所以开设了衍生篇的文章来更好的对这些知识点进行进一步的学习。


JdbcTemplate无论是query 或者 update,其底层调用的都是 execute 方法。execute 方法的作用其实就是获取数据库连接,准备好Statement, 随后调用预先传入的回调方法。

二、 核心方法 - execute

execute 作为数据库操作的核心入口,其实实现逻辑和我们一起最基础的写法类似。将大多数数据库操作对相同的统一封装,而将个性化的操作使用 StatementCallback 进行回调,并进行数据库资源释放的一些收尾操作。

下面我们来看看 execute 方法:

	public <T> T execute(StatementCallback<T> action) throws DataAccessException {
    
    
		Assert.notNull(action, "Callback object must not be null");
		// 从数据源中获取数据连接
		Connection con = DataSourceUtils.getConnection(obtainDataSource());
		Statement stmt = null;
		try {
    
    
			// 创建 Statement 。
			stmt = con.createStatement();
			// 应用一些设置
			applyStatementSettings(stmt);
			// 回调执行个性化业务
			T result = action.doInStatement(stmt);
			// 警告处理
			handleWarnings(stmt);
			return result;
		}
		catch (SQLException ex) {
    
    
			// Release Connection early, to avoid potential connection pool deadlock
			// in the case when the exception translator hasn't been initialized yet.
			// 释放数据库连接,避免当异常转换器没有被初始化的时候出现潜在的连接池死锁
			String sql = getSql(action);
			JdbcUtils.closeStatement(stmt);
			stmt = null;
			DataSourceUtils.releaseConnection(con, getDataSource());
			con = null;
			throw translateException("StatementCallback", sql, ex);
		}
		finally {
    
    
			JdbcUtils.closeStatement(stmt);
			DataSourceUtils.releaseConnection(con, getDataSource());
		}
	}

另一种形式的 execute 。思路基本相同,不再赘述

public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
			throws DataAccessException {
    
    

		Assert.notNull(psc, "PreparedStatementCreator must not be null");
		Assert.notNull(action, "Callback object must not be null");
		if (logger.isDebugEnabled()) {
    
    
			String sql = getSql(psc);想·
			logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
		}

		Connection con = DataSourceUtils.getConnection(obtainDataSource());
		PreparedStatement ps = null;
		try {
    
    
			ps = psc.createPreparedStatement(con);
			applyStatementSettings(ps);
			T result = action.doInPreparedStatement(ps);
			handleWarnings(ps);
			return result;
		}
		catch (SQLException ex) {
    
    
			// Release Connection early, to avoid potential connection pool deadlock
			// in the case when the exception translator hasn't been initialized yet.
			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. 获取数据库连接

	Connection con = DataSourceUtils.getConnection(obtainDataSource());

obtainDataSource() 就是简单的获取 dataSource。这里的 dataSource 是 JdbcTemplate 在创建的时候,作为构造注入时候的参数传递进来。

	protected DataSource obtainDataSource() {
    
    
		DataSource dataSource = getDataSource();
		Assert.state(dataSource != null, "No DataSource set");
		return dataSource;
	}

DataSourceUtils.getConnection 方法中调用了 doGetConnection 方法。下面我们来看看 doGetConnection 方法。

在数据库连接方面,Spring 主要考虑的是关于事务方面的处理。基于事务处理的特殊性,Spring需要保证线程中的数据库操作都是使用同一个事务连接。

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
    
    
		Assert.notNull(dataSource, "No DataSource specified");

		ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
		if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
    
    
			conHolder.requested();
			if (!conHolder.hasConnection()) {
    
    
				logger.debug("Fetching resumed JDBC Connection from DataSource");
				conHolder.setConnection(fetchConnection(dataSource));
			}
			return conHolder.getConnection();
		}
		// Else we either got no holder or an empty thread-bound holder here.

		logger.debug("Fetching JDBC Connection from DataSource");
		Connection con = fetchConnection(dataSource);
		// 当前事务支持同步
		if (TransactionSynchronizationManager.isSynchronizationActive()) {
    
    
			try {
    
    
				// Use same Connection for further JDBC actions within the transaction.
				// Thread-bound object will get removed by synchronization at transaction completion.
				ConnectionHolder holderToUse = conHolder;
				if (holderToUse == null) {
    
    
					holderToUse = new ConnectionHolder(con);
				}
				else {
    
    
					holderToUse.setConnection(con);
				}
				// 记录数据库连接。
				holderToUse.requested();
				TransactionSynchronizationManager.registerSynchronization(
						new ConnectionSynchronization(holderToUse, dataSource));
				holderToUse.setSynchronizedWithTransaction(true);
				if (holderToUse != conHolder) {
    
    
					TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
				}
			}
			catch (RuntimeException ex) {
    
    
				// Unexpected exception from external delegation call -> close Connection and rethrow.
				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时,如果它每次只从服务器上读取一行数据,会产生大量开销。 FetchSize 参数的作用是 在调用 rs.next时, ResultSet会一次性从服务器上取多少行数据回来。这样在下次 rs.next 时,他可以直接从内存中获取数据而不需要网络交互,提高了效率。但是这个设置可能会被某些jdbc驱动忽略。设置过大也会造成内存上升。
  • MaxRow :其作用是将此 Statement 对象生成的所有 ResultSet 对象可以包含的最大行数限制设置为给定值。

3. 告警处理

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

这里用到了一个类 SQWarning ,SQWarning 提供关于数据库访问警告信息的异常。这些警告直接链接到导致报告警告的方法所在的对象。警告可以从Connection、Statement 和 ResultSet 对象中获取。视图在已经关闭的连接上获取警告将导致抛出异常。注意,关闭语句时还会关闭它可能生成得到结果集。

扫描二维码关注公众号,回复: 11960592 查看本文章

对于警告的处理方式并不是直接抛出异常,出现警告很可能会出现数据错误,但是并不一定会影响程序执行,所以这里用户可以自己设置处理警告的方式,如果默认是忽略警告,当出现警告时仅打印警告日志,不抛出异常。

4. 资源释放

数据库的连接释放并不是直接调用了 Connection 的API 中的close 方法。考虑到存在事务的情况,如果当前线程存在事务,那么说明在当前线程中存在共用数据库连接,这种情况下直接使用 ConnectionHolder 中的released 方法进行连接数减一,而不是真正的释放连接。

	public static void doReleaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) throws SQLException {
    
    
		if (con == null) {
    
    
			return;
		}
		if (dataSource != null) {
    
    
			// 当前线程存在事务的情况下说明存在共用数据库连接直接使用 ConnectionHolder 中的 released 方法进行连接数减一而不是真正的释放连接。
			ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
			if (conHolder != null && connectionEquals(conHolder, con)) {
    
    
				// It's the transactional Connection: Don't close it.
				conHolder.released();
				return;
			}
		}
		doCloseConnection(con, dataSource);
	}

...

	public static void doCloseConnection(Connection con, @Nullable DataSource dataSource) throws SQLException {
    
    
		if (!(dataSource instanceof SmartDataSource) || ((SmartDataSource) dataSource).shouldClose(con)) {
    
    
			con.close();
		}
	}

三、execute 的回调

上面说了 execute 方式是整个JdbcTemplate 的核心,其他个性化定制都是在其基础上,通过StatementCallback 回调完成的。下面我们简单看一看。

1. Update 中的回调函数

我们挑一个最简单的Update 方法: JdbcTemplate#update(java.lang.String)

	public int update(final String sql) throws DataAccessException {
    
    
		Assert.notNull(sql, "SQL must not be null");
		if (logger.isDebugEnabled()) {
    
    
			logger.debug("Executing SQL update [" + sql + "]");
		}

		/**
		 * Callback to execute the update statement.
		 */
		 // 该种形式的回调方法。不同形式的回调实现类并不相同。
		class UpdateStatementCallback implements StatementCallback<Integer>, SqlProvider {
    
    
			@Override
			public Integer doInStatement(Statement stmt) throws SQLException {
    
    
				// 执行sql 语句
				int rows = stmt.executeUpdate(sql);
				if (logger.isTraceEnabled()) {
    
    
					logger.trace("SQL update affected " + rows + " rows");
				}
				return rows;
			}
			@Override
			public String getSql() {
    
    
				return sql;
			}
		}
		// 通过 execute 将 Statement  回调给 UpdateStatementCallback。
		return updateCount(execute(new UpdateStatementCallback()));
	}

除此之外,还有另一种形式的更新,其思路都相同。调用的也是另一种 execute 。

	protected int update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss)
			throws DataAccessException {
    
    

		logger.debug("Executing prepared SQL update");

		return updateCount(execute(psc, ps -> {
    
    
			try {
    
    
				if (pss != null) {
    
    
					pss.setValues(ps);
				}
				int rows = ps.executeUpdate();
				if (logger.isTraceEnabled()) {
    
    
					logger.trace("SQL update affected " + rows + " rows");
				}
				return rows;
			}
			finally {
    
    
				if (pss instanceof ParameterDisposer) {
    
    
					((ParameterDisposer) pss).cleanupParameters();
				}
			}
		}));
	}

2. query 功能的实现

可以看到,思路基本相同,这里不再赘述。

	public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
    
    
		Assert.notNull(sql, "SQL must not be null");
		Assert.notNull(rse, "ResultSetExtractor must not be null");
		if (logger.isDebugEnabled()) {
    
    
			logger.debug("Executing SQL query [" + sql + "]");
		}

		/**
		 * Callback to execute the query.
		 */
		class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
    
    
			@Override
			@Nullable
			public T doInStatement(Statement stmt) throws SQLException {
    
    
				ResultSet rs = null;
				try {
    
    
					rs = stmt.executeQuery(sql);
					return rse.extractData(rs);
				}
				finally {
    
    
					JdbcUtils.closeResultSet(rs);
				}
			}
			@Override
			public String getSql() {
    
    
				return sql;
			}
		}

		return execute(new QueryStatementCallback());
	}

四、 其他

由于笔者平时很少使用 JdbcTemplate,所以也没有刻意去记录api的使用。所以在使用的时候根据api 的名字来判断。那么问题来了, 关于 JdbcTemplate#queryForList(java.lang.String, java.lang.Class<T>)方法, 这个方法居然只能解析单列的数据(不能解析通用List,别起这种名字啊)

后续看到使用可以使用如下查询 List 数据,记录一下

 List<User> maps = jdbcTemplate.query("select * from user ", new BeanPropertyRowMapper<>(User.class));

以上:内容部分参考
《Spring源码深度解析》
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

猜你喜欢

转载自blog.csdn.net/qq_36882793/article/details/106675967