Spring源码——JDBC
前言
内容主要参考自《Spring源码深度解析》一书,算是读书笔记或是原书的补充。进入正文后可能会引来各种不适,毕竟阅读源码是件极其痛苦的事情。
本文主要涉及书中第八章的部分,依照书中内容以及个人理解对Spring源码进行了注释,详见Github仓库:https://github.com/MrSorrow/spring-framework
本章内容基于之前所说的Spring IOC 和 AOP功能,开始下一阶段有关应用方面的一些研究。首先介绍Spring中关于JDBC给我们带来怎样的封装与支持。
I. JDBC连接MySQL
JDBC连接数据库的流程
- 在开发环境中加载指定数据库的驱动程序。以数据库 MySQL 为例,我们需要下载 MySQL 支持 JDBC 的驱动程序包(mysql-connector-java-xxx.jar);
- 在 Java 代码中利用反射加载驱动程序,例如加载 MySQL 数据库驱动程序的代码为
Class.forName("com.mysql.jdbc.Driver")
; - 创建数据库连接对象。通过
DriverManager
类来创建数据库连接对象Connection
。DriverManager
类作用于 Java 程序和 JDBC 驱动程序之间,用于检测所加载的驱动程序是否可以建立连接,然后通过getConnection()
方法根据数据库的 URL、用户名、密码 创建一个 JDBCConnection
对象,如:Connection connection = DriverManager.getConnection(“URL”, ”用户名”, ”密码”)
。其中,URL = 协议名 + IP地址(域名) + 端口 + 数据库名称; - 创建
Statement
对象。Statement
对象主要用于执行静态的 SQL 语句并返回它所生成结果的对象。通过数据库连接Connection
对象的createStatement()
可以创建一个Statement
对象。例如:Statement statement = connection.createStatement()
; - 调用
Statement
对象的相关方法执行相对应的 SQL 语句。例如对数据更新、插入和删除操作的executeUpdate()
函数以及对数据库进行查询的executeQuery()
函数。执行这些函数相当于执行了我们定义的 SQL 语句,自然会获得结果。查询操作返回的结果是ResultSet
对象,其中包含了我们需要的数据。通过ResultSet
对象的next()
方法,使得指针指向下一行,然后将数据以列号或者字段名取出。 当next()
方法返回为 false 时,表示下一行没有数据存在。 - ==关闭数据库连接。==使用完数据库或者不需要访问数据库时,通过数据库连接
Connection
对象的close()
方法及时关闭数据库连接。
JDBC连接MySQL示例
按照上述6个流程,我们搬出最常见 JDBC 流程代码。在此之前,我们需要在我们的 spring-mytest 模块中添加上 MySQL 数据库驱动。
compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.18'
我们使用的数据库表如下。表中有四个字段,id、first_name、last_name 以及 last_update。
创建对应的实体类 Actor。
public class Actor {
private Integer id;
private String firstName;
private String lastName;
private Date lastDate;
public Actor(Integer id, String firstName, String lastName, Date lastDate) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.lastDate = lastDate;
}
// getter,setter and toString()
}
按照 JDBC 连接 MySQL 数据库的流程,贴上示例代码。代码主要实现从数据库中查询 id 在固定区间中的所有演员信息。关于 PreparedStatement
和 Statement
的区别可以参考阅读JDBC:深入理解PreparedStatement和Statement。
public class JdbcTest {
@Test
public void testJDBC() {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
// 加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
// 获取数据库连接
connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/sakila",
"root", "1234");
// 利用SQL创建PreparedStatement
preparedStatement = connection.prepareStatement("select * from actor where actor_id > ? and actor_id < ?");
// 设置PreparedStatement参数
preparedStatement.setInt(1, 5);
preparedStatement.setInt(2, 11);
// 执行PreparedStatement获取结果集ResultSet
resultSet = preparedStatement.executeQuery();
// 遍历结果集封装成Actor
while (resultSet.next()) {
Integer id = resultSet.getInt("actor_id");
String first_name = resultSet.getString("first_name");
String last_name = resultSet.getString("last_name");
Date date = resultSet.getDate("last_update");
System.out.println(new Actor(id, first_name, last_name, date));
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
使用Spring的JdbcTemplate
Spring中的 JDBC 连接与直接使用 JDBC 去连接还是有所差别的,Spring对于JDBC做了大量封装,不过其基本流程思路是不会变的,核心实现仍然是那一套流程。Spring对于原始JDBC的封装思路来源于模板设计模式,对于用户CURD 数据库来说,之前的流程大部分都是相同的,不同的只是执行的 SQL 语句,或者说是 Statement
的区别,还有就是对于结果集 ResultSet
的解析操作也不尽相同。所以,利用模板模式可以很好的让用户着重关注变化的部分,固定的流程在模板中就已经定义好,甚至 Statement
的包装、 ResultSet
的解析流程也部分实现了,用户仅仅需要传入需要的参数即可。
对于直接使用 JDBC 到Spring中的 JdbcTemplate
包装思路中如何一步步过来的,可以阅读:spring源码解读之 JdbcTemplate源码。
下面还是先演示Spring如何使用 JdbcTemplate
,依然使用之前使用的数据库表。首先,先配置我们测试工程需要的一些包,包括 dpcp 数据库连接池,依赖 spring-jdbc 模块。gradle 文件如下所示:
dependencies {
compile(project(":spring-beans"))
compile(project(":spring-context"))
compile(project(":spring-aop"))
compile group: 'org.springframework', name: 'spring-aspects', version: '5.0.7.RELEASE'
compile(project(":spring-jdbc"))
compile group: 'org.apache.commons', name: 'commons-dbcp2', version: '2.5.0'
compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.18'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
创建一个数据库表和我们 Actor 实体之间的映射关系。很简单就是解析 ResultSet
结果集,包装成 Actor
类对象。
public class ActorRowMapper implements RowMapper<Actor> {
@Override
public Actor mapRow(ResultSet set, int rowNum) throws SQLException {
return new Actor(set.getInt("actor_id"), set.getString("first_name"),
set.getString("last_name"), set.getDate("last_update"));
}
}
然后创建一个持久层的接口,包括新增和查询 Actor 的功能。
public interface IActorService {
void save(Actor actor);
List<Actor> getUsers();
List<Actor> getAllUsers();
Integer getActorsCount();
}
编写实现类方法。其中依赖了Spring的 JdbcTemplate
去连接操作数据库,JdbcTemplate
的初始化依赖连接池对象,所以我们需要在配置文件中注册一个连接池的 bean。
public class ActorServiceImpl implements IActorService {
private JdbcTemplate jdbcTemplate;
// 设置数据源
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public void save(Actor actor) {
jdbcTemplate.update("insert into actor(first_name, last_name, last_update) values (?, ?, ?)",
new Object[]{actor.getFirstName(), actor.getLastName(), actor.getLastDate()},
new int[]{Types.VARCHAR, Types.VARCHAR, Types.DATE});
}
@Override
public List<Actor> getUsers() {
return jdbcTemplate.query("select * from actor where actor_id < ?",
new Object[]{10}, new int[]{Types.INTEGER}, new ActorRowMapper());
}
@Override
public List<Actor> getAllUsers() {
return jdbcTemplate.query("select * from actor", new ActorRowMapper());
}
@Override
public Integer getActorsCount() {
return jdbcTemplate.queryForObject("select count(*) from actor", Integer.class);
}
}
配置文件。在配置文件中,我们注册了一个连接池 bean,名称为 dataSource
,这里我们用的 dbcp2 连接池,其中还配置了一些数据库的连接信息,连接池的属性设置。经过之前的分析,Spring容器会帮我们实例化这个对象。此外,我们还需要在容器中注册一个 ActorService
的 bean,需要注入依赖的连接池对象 dataSource
。
<?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 id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://127.0.0.1:3306/sakila" />
<property name="username" value="root" />
<property name="password" value="1234" />
<!--连接池启动时的初始值-->
<property name="initialSize" value="1" />
<!--连接池的最大活动连接数-->
<property name="maxTotal" value="5" />
<!--最大空闲值,当经过一个高峰时间后,连接池可以慢慢将已经用不到的连接慢慢释放掉一部分,一致减少到maxIdel为止-->
<property name="maxIdle" value="2" />
<!--最小空闲值,当空闲的连接数少于阈值时,连接池就会预申请取一些连接,以免洪峰来时来不及申请-->
<property name="minIdle" value="1" />
</bean>
<!--配置业务bean-->
<bean id="actorService" class="guo.ping.jdbc.ActorServiceImpl">
<!--向属性中注入数据源-->
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
测试代码如下。首先从Spring容器中获得 ActorServiceImpl
实例 bean,通过调用其内部的方法实现对数据库的访问操作等。
public class SpringJDBCTest {
@Test
public void testSpringJDBC() {
ApplicationContext context = new ClassPathXmlApplicationContext("jdbc-Test.xml");
IActorService actorService = (IActorService) context.getBean("actorService");
Actor actor = new Actor(null, "guoping", "wang", new Date());
actorService.save(actor);
int actorsCount = actorService.getActorsCount();
System.out.println(actorsCount);
List<Actor> users = actorService.getUsers();
for (Actor user : users) {
System.out.println(user);
}
List<Actor> allUsers = actorService.getAllUsers();
for (Actor user : allUsers) {
System.out.println(user);
}
}
}
II. save/update功能实现
上面我们已经演示了如何使用Spring的 JdbcTemplate
,其实从 ActorServiceImpl
中我们可以感受到,增删改查操作 JdbcTemplate
都提供了对应的函数供我们使用,我们需要的是传入SQL语句、如何解析数据等。
我们从 ActorServiceImpl
的插入一条记录方法入手,分析 JdbcTemplate
。
public void save(Actor actor) {
jdbcTemplate.update("insert into actor(first_name, last_name, last_update) values (?, ?, ?)",
new Object[]{actor.getFirstName(), actor.getLastName(), actor.getLastDate()},
new int[]{Types.VARCHAR, Types.VARCHAR, Types.DATE});
}
进入 update(String sql, Object[] args, int[] argTypes)
方法,可以看到第一个参数是 SQL 语句,第二个参数是一个数组,数组中存放了需要传递给SQL传入数据,第三个参数也是数组,和第二个数组元素对应,指定传入的数据的类型。
// 使用newArgTypePreparedStatementSetter对参数以及参数类型进行包装
@Override
public int update(String sql, Object[] args, int[] argTypes) throws DataAccessException {
return update(sql, newArgTypePreparedStatementSetter(args, argTypes));
}
// 使用SimplePreparedStatementCreator对sql语句进行包装
@Override
public int update(String sql, @Nullable PreparedStatementSetter pss) throws DataAccessException {
return update(new SimplePreparedStatementCreator(sql), pss);
}
可以看到,update()
拥有多个重载方法,查看上面的重载函数调用逻辑,可以看到Spring并未急着进行核心处理,而是对参数进行了包装。
首先是利用 ArgumentTypePreparedStatementSetter
类对需要传进SQL语句的参数以及它们对应到数据库中的参数类型进行了封装,同时又利用 SimplePreparedStatementCreator
类又将 SQL 语句封装起来。我们看一下这两个类。
先来看看对需要传进SQL语句的参数以及它们对应到数据库中的参数类型封装过程,通过 newArgTypePreparedStatementSetter()
方法创建 ArgumentTypePreparedStatementSetter
类实例。
/**
* Create a new arg-type-based PreparedStatementSetter using the args and types passed in.
* <p>By default, we'll create an {@link ArgumentTypePreparedStatementSetter}.
* This method allows for the creation to be overridden by subclasses.
* @param args object array with arguments
* @param argTypes int array of SQLTypes for the associated arguments
* @return the new PreparedStatementSetter to use
*/
protected PreparedStatementSetter newArgTypePreparedStatementSetter(Object[] args, int[] argTypes) {
return new ArgumentTypePreparedStatementSetter(args, argTypes);
}
查看 ArgumentTypePreparedStatementSetter
类的定义,主要包含两个成员变量,分别就是参数与参数类型数组。该类实现了 PreparedStatementSetter
接口,重写了 setValues()
方法,该方法主要是对 PreparedStatement
的参数和参数类型进行封装。
public class ArgumentTypePreparedStatementSetter implements PreparedStatementSetter, ParameterDisposer {
@Nullable
private final Object[] args;
@Nullable
private final int[] argTypes;
/**
* Create a new ArgTypePreparedStatementSetter for the given arguments.
* @param args the arguments to set
* @param argTypes the corresponding SQL types of the arguments
*/
public ArgumentTypePreparedStatementSetter(@Nullable Object[] args, @Nullable int[] argTypes) {
if ((args != null && argTypes == null) || (args == null && argTypes != null) ||
(args != null && args.length != argTypes.length)) {
throw new InvalidDataAccessApiUsageException("args and argTypes parameters must match");
}
this.args = args;
this.argTypes = argTypes;
}
/**
* 重写的setValues,对PreparedStatement的参数和参数类型进行封装
* @param ps the PreparedStatement to invoke setter methods on
* @throws SQLException
*/
@Override
public void setValues(PreparedStatement ps) throws SQLException {
int parameterPosition = 1;
if (this.args != null && this.argTypes != null) {
// 遍历每个参数以作类型匹配及转换
for (int i = 0; i < this.args.length; i++) {
Object arg = this.args[i];
// 如果参数是集合类型,且配置的参数类型不是数组,那么就需要进入集合内部递归解析集合内部属性
if (arg instanceof Collection && this.argTypes[i] != Types.ARRAY) {
Collection<?> entries = (Collection<?>) arg;
for (Object entry : entries) {
if (entry instanceof Object[]) {
Object[] valueArray = ((Object[]) entry);
for (Object argValue : valueArray) {
doSetValue(ps, parameterPosition, this.argTypes[i], argValue);
parameterPosition++;
}
}
else {
doSetValue(ps, parameterPosition, this.argTypes[i], entry);
parameterPosition++;
}
}
}
else {
// 解析当前属性
doSetValue(ps, parameterPosition, this.argTypes[i], arg);
parameterPosition++;
}
}
}
}
/**
* 对单个参数及类型的匹配处理
* Set the value for the prepared statement's specified parameter position using the passed in
* value and type. This method can be overridden by sub-classes if needed.
* @param ps the PreparedStatement
* @param parameterPosition index of the parameter position
* @param argType the argument type
* @param argValue the argument value
* @throws SQLException if thrown by PreparedStatement methods
*/
protected void doSetValue(PreparedStatement ps, int parameterPosition, int argType, Object argValue)
throws SQLException {
StatementCreatorUtils.setParameterValue(ps, parameterPosition, argType, argValue);
}
@Override
public void cleanupParameters() {
StatementCreatorUtils.cleanupParameters(this.args);
}
}
再看看 SimplePreparedStatementCreator
类的定义,该类叫为简单,主要是封装 SQL 语句,并能根据 SQL 语句创建出 PreparedStatement
对象。
/**
* 通过sql语句和数据库连接返回PreparedStatement
* Simple adapter for PreparedStatementCreator, allowing to use a plain SQL statement.
*/
private static class SimplePreparedStatementCreator implements PreparedStatementCreator, SqlProvider {
private final String sql;
public SimplePreparedStatementCreator(String sql) {
Assert.notNull(sql, "SQL must not be null");
this.sql = sql;
}
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
return con.prepareStatement(this.sql);
}
@Override
public String getSql() {
return this.sql;
}
}
所以两个封装类,职责明确,一个负责封装 SQL 最终创建一个 PreparedStatement
并返回,另一个封装好参数与参数类型并把它们设置给 PreparedStatement
。当然这些方法后面才会调用,我们现在只是预热。我们继续跟随代码调用逻辑。经过了数据封装,Spring便可进入核心的数据处理代码了。
// sql和参数
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) {
// 设置PreparedStatement(ps)所需要的全部参数
pss.setValues(ps);
}
// 执行PreparedStatement
int rows = ps.executeUpdate();
if (logger.isTraceEnabled()) {
logger.trace("SQL update affected " + rows + " rows");
}
return rows;
}
finally {
if (pss instanceof ParameterDisposer) {
((ParameterDisposer) pss).cleanupParameters();
}
}
}));
}
private static int updateCount(@Nullable Integer result) {
Assert.state(result != null, "No update count");
return result;
}
可以看到核心代码就是调用 execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
方法了,主要是通过创建回调接口实例,实现了其中的 T doInPreparedStatement(PreparedStatement ps)
方法。由于Spring5函数式编程的缘故,可能没有那么直观。我们先进入 execute()
方法一探究竟。
基础方法execute
execute()
方法作为数据库的核心入口,将大多数数据库操作相同的步骤统一封装,而将个性化的操作使用参数 PreparedStatementCallback
进行回调。
/**
* 作为数据库操作的核心入口,将大多数数据库操作相同的步骤统一封装,而将个性化的操作使用参数PreparedStatementCallback进行回调
* @param psc object that can create a PreparedStatement given a Connection
* @param action callback object that specifies the action
* @param <T>
* @return
* @throws DataAccessException
*/
@Override
@Nullable
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 + "]" : ""));
}
// 获取数据库连接(Spring保证线程中的数据库操作都是使用同一个事务连接)
Connection con = DataSourceUtils.getConnection(obtainDataSource());
PreparedStatement ps = null;
try {
// 获取PreparedStatement
ps = psc.createPreparedStatement(con);
// 设置PreparedStatement属性(应用用户设定的输入参数)
applyStatementSettings(ps);
// 执行PreparedStatement,返回执行结果
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);
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());
}
}
相信通过推荐阅读的文章spring源码解读之 JdbcTemplate源码,可以看到和文章符合的部分就是这个 execute()
方法了。execute()
方法中定义了JDBC连接数据库的通用模板流程代码,个性化的操作步骤采用回调的形式。传入方法中的两个参数包含着我们的 SQL 语句以及需要传入的参数、参数类型信息。execute()
方法实现流程如下。
① 获取数据库连接
获取数据库的连接是通过 DataSourceUtils.getConnection(obtainDataSource())
从连接池中获取的。首先通过 obtainDataSource()
获取连接池对象,相信代码一看就懂了。
/**
* Obtain the DataSource for actual use.
* @return the DataSource (never {@code null})
* @throws IllegalStateException in case of no DataSource set
* @since 5.0
*/
protected DataSource obtainDataSource() {
DataSource dataSource = getDataSource();
Assert.state(dataSource != null, "No DataSource set");
return dataSource;
}
/**
* Return the DataSource used by this template.
*/
@Nullable
public DataSource getDataSource() {
return this.dataSource;
}
/**
* Construct a new JdbcTemplate, given a DataSource to obtain connections from.
* <p>Note: This will not trigger initialization of the exception translator.
* @param dataSource the JDBC DataSource to obtain connections from
*/
public JdbcTemplate(DataSource dataSource) {
// 注入连接池对象依赖
setDataSource(dataSource);
afterPropertiesSet();
}
获取到 dataSource
后,随后利用 DataSourceUtils
中的 getConnection(DataSource dataSource)
方法从连接池中获取数据库连接。
/**
* 从连接池中获取一个数据库连接
* Obtain a Connection from the given DataSource. Translates SQLExceptions into
* the Spring hierarchy of unchecked generic data access exceptions, simplifying
* calling code and making any exception that is thrown more meaningful.
* <p>Is aware of a corresponding Connection bound to the current thread, for example
* when using {@link DataSourceTransactionManager}. Will bind a Connection to the
* thread if transaction synchronization is active, e.g. when running within a
* {@link org.springframework.transaction.jta.JtaTransactionManager JTA} transaction).
* @param dataSource the DataSource to obtain Connections from
* @return a JDBC Connection from the given DataSource
* @throws org.springframework.jdbc.CannotGetJdbcConnectionException
* if the attempt to get a Connection failed
* @see #releaseConnection
*/
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
try {
return doGetConnection(dataSource);
}
catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex);
}
catch (IllegalStateException ex) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection: " + ex.getMessage());
}
}
/**
* 从连接池中获取一个数据库连接
* Actually obtain a JDBC Connection from the given DataSource.
* Same as {@link #getConnection}, but throwing the original SQLException.
* <p>Is aware of a corresponding Connection bound to the current thread, for example
* when using {@link DataSourceTransactionManager}. Will bind a Connection to the thread
* if transaction synchronization is active (e.g. if in a JTA transaction).
* <p>Directly accessed by {@link TransactionAwareDataSourceProxy}.
* @param dataSource the DataSource to obtain Connections from
* @return a JDBC Connection from the given DataSource
* @throws SQLException if thrown by JDBC methods
* @see #doReleaseConnection
*/
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
// 从ThreadLocal中获取线程独有的连接,保证了同一线程拥有的是同一个连接
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
// conHolder请求加1
conHolder.requested();
if (!conHolder.hasConnection()) {
// 如果conHolder中没有连接,就保存在conHolder中
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(fetchConnection(dataSource));
}
// 如果conHolder中有连接,直接获取就行
return conHolder.getConnection();
}
// 否则我们如果在ThreadLocal中没有ConnectionHolder或者一个没有连接的ConnectionHolder,就创建并保存至ThreadLocal中
logger.debug("Fetching JDBC Connection from DataSource");
Connection con = fetchConnection(dataSource);
// 当前线程支持同步
if (TransactionSynchronizationManager.isSynchronizationActive()) {
logger.debug("Registering transaction synchronization for JDBC Connection");
// 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) {
// 没有holder和连接创建holder和连接
holderToUse = new ConnectionHolder(con);
}
else {
// 没有连接创建连接
holderToUse.setConnection(con);
}
// 记录数据库连接
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(
new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
// 将ConnectionHolder保存至ThreadLocal中
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
return con;
}
真正实现的方法是 doGetConnection(DataSource dataSource)
这个方法,Spring获取数据库连接并没有那么的简单,例如只调用 dataSource.getConnection()
方法那么简单。书中的原话是==Spring主要考虑的是关于事务方面的处理,基于事务处理的特殊性,Spring需要保证线程中的数据库操作都是使用同一个事务连接。==关于这句话的理解,个人觉得先阅读一下探索多线程使用同一个数据库connection的后果这篇文章暴露的问题,以及ThreadLocal保证客户端同时拿到的是同一个连接,数据库多事务的处理这篇文章所提及的 ThreadLocal
类解决思路。在真实场景中,当多个用户同时请求一个服务的时候,Spring容器会给每一个用户请求分配一个线程,并发执行业务逻辑,业务逻辑中包含着对数据库数据的增删改查,也就意味着每一个线程(每一个请求)都包含着事务,也就都需要一个数据库连接。如果线程直接通过 dataSource.getConnection()
方法获得数据库连接,同时如果请求中包含了多个数据库增删改查的操作,那么每个操作很可能获得不同的数据库连接,那么就存在如果其中某个操作有问题而不能完整的回滚事务问题。
我们查看 TransactionSynchronizationManager
类。可以看到类中几个成员变量都是 ThreadLocal
类型,NamedThreadLocal
是Spring为了给每一个 ThreadLocal
设置一个标记性的名称而自定义的 ThreadLocal
子类,类中添加了一个成员变量 private final String name;
。
public abstract class TransactionSynchronizationManager {
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
// NamedThreadLocal继承自ThreadLocal,主要功能是给ThreadLocal设定一个名字
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
······
}
其中名为 Transactional resources 的 ThreadLocal
保存的类型为 Map<Object, Object>
,这是一个 HashMap,可以存储每个线程各自想要保存的键值对。每个线程各自的数据库连接对象也正是保存在这里。获取数据库连接时,在 doGetConnection(DataSource dataSource)
方法中,首先先尝试通过调用 TransactionSynchronizationManager.getResource(dataSource)
方法获取到 ThreadLocal 中保存的连接,dataSource
就是 Map 的 key,Value 就是包含了 Connection
实例的对象。
/**
* Retrieve a resource for the given key that is bound to the current thread.
* @param key the key to check (usually the resource factory)
* @return a value bound to the current thread (usually the active
* resource object), or {@code null} if none
* @see ResourceTransactionManager#getResourceFactory()
*/
@Nullable
public static Object getResource(Object key) {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Object value = doGetResource(actualKey);
if (value != null && logger.isTraceEnabled()) {
logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +
Thread.currentThread().getName() + "]");
}
return value;
}
/**
* 从ThreadLocal保存的Map中依据key取出value
* Actually check the value of the resource that is bound for the given key.
*/
@Nullable
private static Object doGetResource(Object actualKey) {
// resources(ThreadLocal)中获得对象
Map<Object, Object> map = resources.get();
if (map == null) {
return null;
}
Object value = map.get(actualKey);
// Transparently remove ResourceHolder that was marked as void...
if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
map.remove(actualKey);
// Remove entire ThreadLocal if empty...
if (map.isEmpty()) {
resources.remove();
}
value = null;
}
return value;
}
其实,从 ThreadLocal 的 Map 中获得到的对象是 ConnectionHolder
类型,我们先来看看它如何包装了 Connection
。除了 Connection
之外,还有关于事务方面的相关属性,这我们留到事务部分再分析。
public class ConnectionHolder extends ResourceHolderSupport {
/**
* Prefix for savepoint names.
*/
public static final String SAVEPOINT_NAME_PREFIX = "SAVEPOINT_";
@Nullable
private ConnectionHandle connectionHandle;
@Nullable
private Connection currentConnection;
private boolean transactionActive = false;
@Nullable
private Boolean savepointsSupported;
private int savepointCounter = 0;
当然,最开始 ThreadLocal 中肯定没有,所以在 doGetConnection(DataSource dataSource)
方法后面肯定会存在先从连接池取出一个然后再保存至 ThreadLocal 的操作。先来看看直接从连接池获取连接的方法:
/**
* 从线程池中获取一个连接,直接调用dataSource.getConnection()
* Actually fetch a {@link Connection} from the given {@link DataSource},
* defensively turning an unexpected {@code null} return value from
* {@link DataSource#getConnection()} into an {@link IllegalStateException}.
* @param dataSource the DataSource to obtain Connections from
* @return a JDBC Connection from the given DataSource (never {@code null})
* @throws SQLException if thrown by JDBC methods
* @throws IllegalStateException if the DataSource returned a null value
* @see DataSource#getConnection()
*/
private static Connection fetchConnection(DataSource dataSource) throws SQLException {
Connection con = dataSource.getConnection();
if (con == null) {
throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource);
}
return con;
}
很简单,就是正常的利用 dataSource
的 getConnection()
方法。我们进一步查看如何将这个连接保存至 ThreadLocal 中。
// 当前线程支持同步
if (TransactionSynchronizationManager.isSynchronizationActive()) {
logger.debug("Registering transaction synchronization for JDBC Connection");
// 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) {
// 没有holder和连接创建holder和连接
holderToUse = new ConnectionHolder(con);
}
else {
// 没有连接创建连接
holderToUse.setConnection(con);
}
// 记录数据库连接
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(
new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
// 将ConnectionHolder保存至ThreadLocal中
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
可以看到,当事务同步开启时,需要将连接通过 ConnectionHolder
进行封装,并且保存到 ThreadLocal 中,调用方法为 TransactionSynchronizationManager.bindResource(dataSource, holderToUse)
。
/**
* 保存对象之resources ThreadLocal的Map中
* Bind the given resource for the given key to the current thread.
* @param key the key to bind the value to (usually the resource factory)
* @param value the value to bind (usually the active resource object)
* @throws IllegalStateException if there is already a value bound to the thread
* @see ResourceTransactionManager#getResourceFactory()
*/
public static void bindResource(Object key, Object value) throws IllegalStateException {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Assert.notNull(value, "Value must not be null");
Map<Object, Object> map = resources.get();
// set ThreadLocal Map if none found
if (map == null) {
map = new HashMap<>();
resources.set(map);
}
Object oldValue = map.put(actualKey, value);
// Transparently suppress a ResourceHolder that was marked as void...
if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {
oldValue = null;
}
if (oldValue != null) {
throw new IllegalStateException("Already value [" + oldValue + "] for key [" +
actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
}
if (logger.isTraceEnabled()) {
logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" +
Thread.currentThread().getName() + "]");
}
}
② 应用用户设定的输入参数
我们回到 execute()
方法,上一步我们获得到了数据库连接,下一步我们就需要创建 PreparedStatement
对象并设置上一些参数。
/**
* 作为数据库操作的核心入口,将大多数数据库操作相同的步骤统一封装,而将个性化的操作使用参数PreparedStatementCallback进行回调
*/
@Override
@Nullable
public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
throws DataAccessException {
·····
// 获取数据库连接(Spring保证线程中的数据库操作都是使用同一个事务连接)
Connection con = DataSourceUtils.getConnection(obtainDataSource());
PreparedStatement ps = null;
try {
// 获取PreparedStatement
ps = psc.createPreparedStatement(con);
// 设置PreparedStatement属性(应用用户设定的输入参数)
applyStatementSettings(ps);
// 执行PreparedStatement,返回执行结果
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);
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());
}
}
首先,先利用 SimplePreparedStatementCreator
的 createPreparedStatement(con)
方法获取到一个 PreparedStatement
对象。实质也非常简单,就是常见的利用数据库连接创建 PreparedStatement
对象的方法。
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
return con.prepareStatement(this.sql);
}
Spring利用 applyStatementSettings(ps)
主要对结果集的 fetchSize、maxRows 以及超时时间进行了统一的设置。fetchSize 表示 resultSet 一次性从服务器上取得多少行数据回来,这样在下次调用 rs.next()
时直接从内存读,无需网络交互,提高效率,否则每次 resultSet 从网络上只读一行数据回来。这个设置可能会被某些 JDBC 驱动忽略,而且设置过大也会造成内存的上升。maxRows 将此 Statement 对象生成的所有 ResultSet 对象可包含的最大行数限制设置为定数。
/**
* 设置JDBC Statement,应用用户设定的输入参数
* Prepare the given JDBC Statement (or PreparedStatement or CallableStatement),
* applying statement settings such as fetch size, max rows, and query timeout.
* @param stmt the JDBC Statement to prepare
* @throws SQLException if thrown by JDBC API
* @see #setFetchSize
* @see #setMaxRows
* @see #setQueryTimeout
* @see org.springframework.jdbc.datasource.DataSourceUtils#applyTransactionTimeout
*/
protected void applyStatementSettings(Statement stmt) throws SQLException {
// fetchSize表示resultSet一次性从服务器上取得多少行数据回来,这样调用rs.next时直接从内存读,无需网络交互,提高效率
int fetchSize = getFetchSize();
if (fetchSize != -1) {
stmt.setFetchSize(fetchSize);
}
// maxRows将此Statement对象生成的所有ResultSet对象可包含的最大行数限制设置为定数
int maxRows = getMaxRows();
if (maxRows != -1) {
stmt.setMaxRows(maxRows);
}
DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout());
}
③ 调用回调函数
上面只是对 PreparedStatement
进行了一些基础属性的设置,而最重要的还是要将我们有关 SQL 的参数设置给它。所以,紧接着Spring调用了 action.doInPreparedStatement(ps)
方法进行设置。这里我将之前的函数式写法改成普通的写法看得更清晰。
T doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException {
try {
if (pss != null) {
// 设置PreparedStatement(ps)所需要的全部参数
pss.setValues(ps);
}
// 执行PreparedStatement
int rows = ps.executeUpdate();
if (logger.isTraceEnabled()) {
logger.trace("SQL update affected " + rows + " rows");
}
return rows;
}
finally {
if (pss instanceof ParameterDisposer) {
((ParameterDisposer) pss).cleanupParameters();
}
}
}
我们回顾一下,我们需要传进SQL语句的参数以及它们对应到数据库中的参数类型是封装在 ArgumentTypePreparedStatementSetter
类中的,也就是函数中的 pss 参数。所以首先先执行了 ArgumentTypePreparedStatementSetter
类中的 setValues(ps)
方法,ps 就是刚刚创建的 PreparedStatement
对象。this.args
和 this.argTypes
分别是我们自己传入的参数与参数类型。
从之前第一部分的示例来说,正常使用原生JDBC方式对 PreparedStatement
对象进行设置参数,需要这样。
preparedStatement = connection.prepareStatement("select * from actor where actor_id > ? and actor_id < ?");
// 设置PreparedStatement参数
preparedStatement.setInt(1, 5);
preparedStatement.setInt(2, 11);
而如果使用Spring,我们仅仅只需要传入参数数组以及类型数组即可,Spring会自动帮我们设置。
public void save(Actor actor) {
jdbcTemplate.update("insert into actor(first_name, last_name, last_update) values (?, ?, ?)",
new Object[]{actor.getFirstName(), actor.getLastName(), actor.getLastDate()},
new int[]{Types.VARCHAR, Types.VARCHAR, Types.DATE});
}
那么,Spring是如何将参数数组以及类型数组自动解析并设置给 PreparedStatement
对象的呢?主要就是通过这里的 setValues(ps)
方法。
/**
* 重写的setValues,对PreparedStatement的参数和参数类型进行封装
* @param ps the PreparedStatement to invoke setter methods on
* @throws SQLException
*/
@Override
public void setValues(PreparedStatement ps) throws SQLException {
int parameterPosition = 1;
if (this.args != null && this.argTypes != null) {
// 遍历每个参数以作类型匹配及转换
for (int i = 0; i < this.args.length; i++) {
Object arg = this.args[i];
// 如果参数是集合类型,且配置的参数类型不是数组,那么就需要进入集合内部递归解析集合内部属性
if (arg instanceof Collection && this.argTypes[i] != Types.ARRAY) {
Collection<?> entries = (Collection<?>) arg;
for (Object entry : entries) {
if (entry instanceof Object[]) {
Object[] valueArray = ((Object[]) entry);
for (Object argValue : valueArray) {
doSetValue(ps, parameterPosition, this.argTypes[i], argValue);
parameterPosition++;
}
}
else {
doSetValue(ps, parameterPosition, this.argTypes[i], entry);
parameterPosition++;
}
}
}
else {
// 解析当前属性
doSetValue(ps, parameterPosition, this.argTypes[i], arg);
parameterPosition++;
}
}
}
}
/**
* 对单个参数及类型的匹配处理
* Set the value for the prepared statement's specified parameter position using the passed in
* value and type. This method can be overridden by sub-classes if needed.
* @param ps the PreparedStatement
* @param parameterPosition index of the parameter position
* @param argType the argument type
* @param argValue the argument value
* @throws SQLException if thrown by PreparedStatement methods
*/
protected void doSetValue(PreparedStatement ps, int parameterPosition, int argType, Object argValue)
throws SQLException {
StatementCreatorUtils.setParameterValue(ps, parameterPosition, argType, argValue);
}
/**
* Set the value for a parameter. The method used is based on the SQL type
* of the parameter and we can handle complex types like arrays and LOBs.
* @param ps the prepared statement or callable statement
* @param paramIndex index of the parameter we are setting
* @param sqlType the SQL type of the parameter
* @param inValue the value to set (plain value or a SqlTypeValue)
* @throws SQLException if thrown by PreparedStatement methods
* @see SqlTypeValue
*/
public static void setParameterValue(PreparedStatement ps, int paramIndex, int sqlType,
@Nullable Object inValue) throws SQLException {
setParameterValueInternal(ps, paramIndex, sqlType, null, null, inValue);
}
/**
* Set the value for a parameter. The method used is based on the SQL type
* of the parameter and we can handle complex types like arrays and LOBs.
* @param ps the prepared statement or callable statement
* @param paramIndex index of the parameter we are setting
* @param sqlType the SQL type of the parameter
* @param typeName the type name of the parameter
* (optional, only used for SQL NULL and SqlTypeValue)
* @param scale the number of digits after the decimal point
* (for DECIMAL and NUMERIC types)
* @param inValue the value to set (plain value or a SqlTypeValue)
* @throws SQLException if thrown by PreparedStatement methods
* @see SqlTypeValue
*/
private static void setParameterValueInternal(PreparedStatement ps, int paramIndex, int sqlType,
@Nullable String typeName, @Nullable Integer scale, @Nullable Object inValue) throws SQLException {
String typeNameToUse = typeName;
int sqlTypeToUse = sqlType;
Object inValueToUse = inValue;
// override type info?
if (inValue instanceof SqlParameterValue) {
SqlParameterValue parameterValue = (SqlParameterValue) inValue;
if (logger.isDebugEnabled()) {
logger.debug("Overriding type info with runtime info from SqlParameterValue: column index " + paramIndex +
", SQL type " + parameterValue.getSqlType() + ", type name " + parameterValue.getTypeName());
}
if (parameterValue.getSqlType() != SqlTypeValue.TYPE_UNKNOWN) {
sqlTypeToUse = parameterValue.getSqlType();
}
if (parameterValue.getTypeName() != null) {
typeNameToUse = parameterValue.getTypeName();
}
inValueToUse = parameterValue.getValue();
}
if (logger.isTraceEnabled()) {
logger.trace("Setting SQL statement parameter value: column index " + paramIndex +
", parameter value [" + inValueToUse +
"], value class [" + (inValueToUse != null ? inValueToUse.getClass().getName() : "null") +
"], SQL type " + (sqlTypeToUse == SqlTypeValue.TYPE_UNKNOWN ? "unknown" : Integer.toString(sqlTypeToUse)));
}
if (inValueToUse == null) {
setNull(ps, paramIndex, sqlTypeToUse, typeNameToUse);
}
else {
// 最终调用方法
setValue(ps, paramIndex, sqlTypeToUse, typeNameToUse, scale, inValueToUse);
}
}
最终还是继续调用 setValue()
方法,该方法也非常粗暴,就是对所有的 SqlType
进行对应的设置,再往下就不需要继续看了,思路理清即可。
/**
* 对各种类型的参数进行设置
* @param ps
* @param paramIndex
* @param sqlType
* @param typeName
* @param scale
* @param inValue
* @throws SQLException
*/
private static void setValue(PreparedStatement ps, int paramIndex, int sqlType,
@Nullable String typeName, @Nullable Integer scale, Object inValue) throws SQLException {
if (inValue instanceof SqlTypeValue) {
((SqlTypeValue) inValue).setTypeValue(ps, paramIndex, sqlType, typeName);
}
else if (inValue instanceof SqlValue) {
((SqlValue) inValue).setValue(ps, paramIndex);
}
else if (sqlType == Types.VARCHAR || sqlType == Types.LONGVARCHAR ) {
ps.setString(paramIndex, inValue.toString());
}
else if (sqlType == Types.NVARCHAR || sqlType == Types.LONGNVARCHAR) {
ps.setNString(paramIndex, inValue.toString());
}
else if ((sqlType == Types.CLOB || sqlType == Types.NCLOB) && isStringValue(inValue.getClass())) {
String strVal = inValue.toString();
if (strVal.length() > 4000) {
// Necessary for older Oracle drivers, in particular when running against an Oracle 10 database.
// Should also work fine against other drivers/databases since it uses standard JDBC 4.0 API.
if (sqlType == Types.NCLOB) {
ps.setNClob(paramIndex, new StringReader(strVal), strVal.length());
}
else {
ps.setClob(paramIndex, new StringReader(strVal), strVal.length());
}
return;
}
else {
// Fallback: setString or setNString binding
if (sqlType == Types.NCLOB) {
ps.setNString(paramIndex, strVal);
}
else {
ps.setString(paramIndex, strVal);
}
}
}
else if (sqlType == Types.DECIMAL || sqlType == Types.NUMERIC) {
if (inValue instanceof BigDecimal) {
ps.setBigDecimal(paramIndex, (BigDecimal) inValue);
}
else if (scale != null) {
ps.setObject(paramIndex, inValue, sqlType, scale);
}
else {
ps.setObject(paramIndex, inValue, sqlType);
}
}
else if (sqlType == Types.BOOLEAN) {
if (inValue instanceof Boolean) {
ps.setBoolean(paramIndex, (Boolean) inValue);
}
else {
ps.setObject(paramIndex, inValue, Types.BOOLEAN);
}
}
else if (sqlType == Types.DATE) {
if (inValue instanceof java.util.Date) {
if (inValue instanceof java.sql.Date) {
ps.setDate(paramIndex, (java.sql.Date) inValue);
}
else {
ps.setDate(paramIndex, new java.sql.Date(((java.util.Date) inValue).getTime()));
}
}
else if (inValue instanceof Calendar) {
Calendar cal = (Calendar) inValue;
ps.setDate(paramIndex, new java.sql.Date(cal.getTime().getTime()), cal);
}
else {
ps.setObject(paramIndex, inValue, Types.DATE);
}
}
else if (sqlType == Types.TIME) {
if (inValue instanceof java.util.Date) {
if (inValue instanceof java.sql.Time) {
ps.setTime(paramIndex, (java.sql.Time) inValue);
}
else {
ps.setTime(paramIndex, new java.sql.Time(((java.util.Date) inValue).getTime()));
}
}
else if (inValue instanceof Calendar) {
Calendar cal = (Calendar) inValue;
ps.setTime(paramIndex, new java.sql.Time(cal.getTime().getTime()), cal);
}
else {
ps.setObject(paramIndex, inValue, Types.TIME);
}
}
else if (sqlType == Types.TIMESTAMP) {
if (inValue instanceof java.util.Date) {
if (inValue instanceof java.sql.Timestamp) {
ps.setTimestamp(paramIndex, (java.sql.Timestamp) inValue);
}
else {
ps.setTimestamp(paramIndex, new java.sql.Timestamp(((java.util.Date) inValue).getTime()));
}
}
else if (inValue instanceof Calendar) {
Calendar cal = (Calendar) inValue;
ps.setTimestamp(paramIndex, new java.sql.Timestamp(cal.getTime().getTime()), cal);
}
else {
ps.setObject(paramIndex, inValue, Types.TIMESTAMP);
}
}
else if (sqlType == SqlTypeValue.TYPE_UNKNOWN || (sqlType == Types.OTHER &&
"Oracle".equals(ps.getConnection().getMetaData().getDatabaseProductName()))) {
if (isStringValue(inValue.getClass())) {
ps.setString(paramIndex, inValue.toString());
}
else if (isDateValue(inValue.getClass())) {
ps.setTimestamp(paramIndex, new java.sql.Timestamp(((java.util.Date) inValue).getTime()));
}
else if (inValue instanceof Calendar) {
Calendar cal = (Calendar) inValue;
ps.setTimestamp(paramIndex, new java.sql.Timestamp(cal.getTime().getTime()), cal);
}
else {
// Fall back to generic setObject call without SQL type specified.
ps.setObject(paramIndex, inValue);
}
}
else {
// Fall back to generic setObject call with SQL type specified.
ps.setObject(paramIndex, inValue, sqlType);
}
}
参数设置好后,我们就可以直接执行 SQL 语句了,直接利用 ps.executeUpdate()
方法,执行后返回结果。
④ 警告处理
获得结果后,Spring并没有直接进行返回。在这之前还进行了警告处理,调用了 handleWarnings(ps)
方法。此方法中用到了一个 java.sql.SQLWarning
的类,SQLWarning
提供关于数据库访问警告信息的异常。这些警告直接连接到导致报告警告的方法所在的对象。警告可以从 Connection
、Statement
和 ResultSet
对象中获得。类似的,试图在已经关闭的连接或者已经关闭的结果集上获取警告也将导致异常抛出。注意,关闭语句时还会关闭它可能生成的结果集。
很多人不是很理解什么情况下会产生警告而不是异常,在这里给读者提示个最常见的警告 DataTruncationg:DataTruncation
直接继承 SQLWarning
,由于某种原因意外的截断数据值时会以 DataTruncation
警告形式报告异常。
对应警告的处理方式并不是直接抛出异常,出现警告很可能会出现数据错误,但是并不是一定会影响程序执行,所以用户可以自己设置处理警告的方式。Spring中当设置为忽略警告时只会尝试打印日志,不忽略警告就直接抛出异常。
/**
* 警告处理
* Throw an SQLWarningException if we're not ignoring warnings,
* else log the warnings (at debug level).
* @param stmt the current JDBC statement
* @throws SQLWarningException if not ignoring warnings
* @see org.springframework.jdbc.SQLWarningException
*/
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());
}
}
/**
* 直接抛出异常
* Throw an SQLWarningException if encountering an actual warning.
* @param warning the warnings object from the current statement.
* May be {@code null}, in which case this method does nothing.
* @throws SQLWarningException in case of an actual warning to be raised
*/
protected void handleWarnings(@Nullable SQLWarning warning) throws SQLWarningException {
if (warning != null) {
throw new SQLWarningException("Warning not ignored", warning);
}
}
⑤ 资源释放
当执行完 SQL,获得结果,处理完警告,在返回结果程序结束之前,一定要释放资源,将数据库连接返回给连接池等等。数据库的释放不是直接调用了 Connection
的API中的 close()
方法。考虑到存在事务的情况,如果当前线程存在事务,那么说明在当前线程中存在共用数据库连接,这种情况下直接使用 ConnectionHolder
中的 released()
方法进行数据库连接数减一,而不是真正的释放连接。
/**
* 释放连接归还连接池
* Close the given Connection, obtained from the given DataSource,
* if it is not managed externally (that is, not bound to the thread).
* @param con the Connection to close if necessary
* (if this is {@code null}, the call will be ignored)
* @param dataSource the DataSource that the Connection was obtained from
* (may be {@code null})
* @see #getConnection
*/
public static void releaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) {
try {
doReleaseConnection(con, dataSource);
}
catch (SQLException ex) {
logger.debug("Could not close JDBC Connection", ex);
}
catch (Throwable ex) {
logger.debug("Unexpected exception on closing JDBC Connection", ex);
}
}
/**
* 释放连接归还连接池
* Actually close the given Connection, obtained from the given DataSource.
* Same as {@link #releaseConnection}, but throwing the original SQLException.
* <p>Directly accessed by {@link TransactionAwareDataSourceProxy}.
* @param con the Connection to close if necessary
* (if this is {@code null}, the call will be ignored)
* @param dataSource the DataSource that the Connection was obtained from
* (may be {@code null})
* @throws SQLException if thrown by JDBC methods
* @see #doGetConnection
*/
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;
}
}
logger.debug("Returning JDBC Connection to DataSource");
doCloseConnection(con, dataSource);
}
/**
* Close the Connection, unless a {@link SmartDataSource} doesn't want us to.
* @param con the Connection to close if necessary
* @param dataSource the DataSource that the Connection was obtained from
* @throws SQLException if thrown by JDBC methods
* @see Connection#close()
* @see SmartDataSource#shouldClose(Connection)
*/
public static void doCloseConnection(Connection con, @Nullable DataSource dataSource) throws SQLException {
// 如果是SmartDataSource且SmartDataSource中没有要求连接关闭则不进行关闭
if (!(dataSource instanceof SmartDataSource) || ((SmartDataSource) dataSource).shouldClose(con)) {
con.close();
}
}
III. query功能实现
上一部分介绍的是有关 jdbcTemplate
中的 update()
方法的实现流程,现在再来看看 jdbcTemplate
中另一个主要的方法——查询 query()
。
示例中,我们使用 query()
查询指定 id 区间的所有 Actor,同样我们需要传入四个参数,第一个参数是 SQL 语句,第二个还是参数值数组,第三个还是每个参数对应的类型。相比 update()
方法多的是一个 RowMapper
参数,这是Spring提供的一个接口,让用户定义如何将查询得到的结果集进行封装成我们自己的POJO。
@Override
public List<Actor> getUsers() {
return jdbcTemplate.query("select * from actor where actor_id < ?",
new Object[]{10}, new int[]{Types.INTEGER}, new ActorRowMapper());
}
我们跟踪 jdbcTemplate
中的 query()
方法。同样,query()
的重载方法也非常的多,我们挑出示例中调用到的作为讲解。经过上一部分 update()
方法的研究,相信这个调用过程还是非常清晰,也是一样对我们输入的参数进行了封装。前三个参数的封装与 update()
方法一致,主要多了一个对于 RowMapper
参数的封装。
@Override
public <T> List<T> query(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper) throws DataAccessException {
return result(query(sql, args, argTypes, new RowMapperResultSetExtractor<>(rowMapper)));
}
private static <T> T result(@Nullable T result) {
Assert.state(result != null, "No result");
return result;
}
@Override
@Nullable
public <T> T query(String sql, Object[] args, int[] argTypes, ResultSetExtractor<T> rse) throws DataAccessException {
// 利用newArgTypePreparedStatementSetter包装参数和参数类型
return query(sql, newArgTypePreparedStatementSetter(args, argTypes), rse);
}
@Override
@Nullable
public <T> T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor<T> rse) throws DataAccessException {
// 利用SimplePreparedStatementCreator包装sql语句,能够获取PreparedStatement
return query(new SimplePreparedStatementCreator(sql), pss, rse);
}
对于 RowMapper
参数的封装,利用了 RowMapperResultSetExtractor
类。先看一下 RowMapper
接口,接口方法参数分别是结果集以及当前行行号,返回值为泛型,我们可以实现方法,定义如何从结果集取出数据,包装成指定POJO返回。
@FunctionalInterface
public interface RowMapper<T> {
/**
* Implementations must implement this method to map each row of data
* in the ResultSet. This method should not call {@code next()} on
* the ResultSet; it is only supposed to map values of the current row.
* @param rs the ResultSet to map (pre-initialized for the current row)
* @param rowNum the number of the current row
* @return the result object for the current row (may be {@code null})
* @throws SQLException if a SQLException is encountered getting
* column values (that is, there's no need to catch SQLException)
*/
@Nullable
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
再来看一下 RowMapperResultSetExtractor
类,其中包含了 RowMapper
属性。该类同时又实现了 ResultSetExtractor
接口,extractData(ResultSet rs)
方法中定义了如何利用 RowMapper
的 mapRow()
方法将结果进行封装转换至POJO并返回。
public class RowMapperResultSetExtractor<T> implements ResultSetExtractor<List<T>> {
private final RowMapper<T> rowMapper;
private final int rowsExpected;
/**
* Create a new RowMapperResultSetExtractor.
* @param rowMapper the RowMapper which creates an object for each row
*/
public RowMapperResultSetExtractor(RowMapper<T> rowMapper) {
this(rowMapper, 0);
}
/**
* Create a new RowMapperResultSetExtractor.
* @param rowMapper the RowMapper which creates an object for each row
* @param rowsExpected the number of expected rows
* (just used for optimized collection handling)
*/
public RowMapperResultSetExtractor(RowMapper<T> rowMapper, int rowsExpected) {
Assert.notNull(rowMapper, "RowMapper is required");
this.rowMapper = rowMapper;
this.rowsExpected = rowsExpected;
}
/**
* 实现ResultSetExtractor接口方法利用RowMapper的mapRow方法将结果进行封装转换至POJO并返回
* @param rs the ResultSet to extract data from. Implementations should
* not close this: it will be closed by the calling JdbcTemplate.
* @return
* @throws SQLException
*/
@Override
public List<T> extractData(ResultSet rs) throws SQLException {
List<T> results = (this.rowsExpected > 0 ? new ArrayList<>(this.rowsExpected) : new ArrayList<>());
int rowNum = 0;
while (rs.next()) {
results.add(this.rowMapper.mapRow(rs, rowNum++));
}
return results;
}
}
了解了 query()
方法的参数封装步骤,我们正式深入研究Spring是怎么完成查询功能的。
带参数的查询
/**
* 带参数的查询
* Query using a prepared statement, allowing for a PreparedStatementCreator
* and a PreparedStatementSetter. Most other query methods use this method,
* but application code will always work with either a creator or a setter.
* @param psc the Callback handler that can create a PreparedStatement given a
* Connection
* @param pss object that knows how to set values on the prepared statement.
* If this is null, the SQL will be assumed to contain no bind parameters.
* @param rse object that will extract results.
* @return an arbitrary result object, as returned by the ResultSetExtractor
* @throws DataAccessException if there is any problem
*/
@Nullable
public <T> T query(
PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor<T> rse)
throws DataAccessException {
Assert.notNull(rse, "ResultSetExtractor must not be null");
logger.debug("Executing prepared SQL query");
return execute(psc, new PreparedStatementCallback<T>() {
// 重写了doInPreparedStatement方法,定义执行PreparedStatement逻辑
@Override
@Nullable
public T doInPreparedStatement(PreparedStatement ps) throws SQLException {
ResultSet rs = null;
try {
if (pss != null) {
// 设置了PreparedStatement所需的全部参数
pss.setValues(ps);
}
// 执行查询
rs = ps.executeQuery();
// 将结果进行封装转换至POJO
return rse.extractData(rs);
}
finally {
JdbcUtils.closeResultSet(rs);
if (pss instanceof ParameterDisposer) {
((ParameterDisposer) pss).cleanupParameters();
}
}
}
});
}
好像一切都很眼熟,query()
方法中调用的还是 execute()
方法,不一样的只是回调函数 doInPreparedStatement()
的实现不同。这个回调函数的作用就是给 PreparedStatement
设置我们传入的参数,然后执行语句。这里不一样的就是执行的是查询 ps.executeQuery()
,同样返回的结果需要进行封装并转换成POJO。而调用的方法正是 RowMapperResultSetExtractor
的 extractData(rs)
方法。
无参数的查询
前面的查询示例中 SQL 语句包含了参数,Spring对于没有参数的 SQL 语句采取了更加高效的方式。例如示例中查询所有的 Actor。
@Override
public List<Actor> getAllUsers() {
return jdbcTemplate.query("select * from actor", new ActorRowMapper());
}
进入 query()
方法:
@Override
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
}
@Override
@Nullable
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());
}
与前面有参的查询方法对比,明显少了参数与参数类型的包装,在 query()
方法内部,也因为无参就不需要创建 PreparedStatement
了,直接用 Statement
即可。此外,无参调用的 execute()
也相应的变化。
/**
* 不带参数的sql执行
* @param action callback object that specifies the action
* @param <T>
* @return
* @throws DataAccessException
*/
@Override
@Nullable
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());
}
}
那么可以看出,基本流程如出一辙,唯一区别主要还是在 PreparedStatement
与 Statement
。
IV. queryObject
Spring中不仅仅为我们提供了 query()
方法,还在此基础上做了封装,提供了不同类型的 query()
方法,如下图。
以 queryForObject()
方法为例,来了解Spring自带的一些方法如何在返回结果的基础上进行封装的。对应第一部分的示例查询记录数目。
@Override
public Integer getActorsCount() {
return jdbcTemplate.queryForObject("select count(*) from actor", Integer.class);
}
进入 queryForObject()
方法:
@Override
@Nullable
public <T> T queryForObject(String sql, Class<T> requiredType) throws DataAccessException {
return queryForObject(sql, getSingleColumnRowMapper(requiredType));
}
@Override
@Nullable
public <T> T queryForObject(String sql, RowMapper<T> rowMapper) throws DataAccessException {
List<T> results = query(sql, rowMapper);
return DataAccessUtils.nullableSingleResult(results);
}
这里利用了 getSingleColumnRowMapper(requiredType)
方法进行目标类型的包装,本质上利用 SingleColumnRowMapper
进行封装。
/**
* Create a new RowMapper for reading result objects from a single column.
* @param requiredType the type that each result object is expected to match
* @return the RowMapper to use
* @see SingleColumnRowMapper
*/
protected <T> RowMapper<T> getSingleColumnRowMapper(Class<T> requiredType) {
return new SingleColumnRowMapper<>(requiredType);
}
我们查看一下该类,它实现了 RowMapper
接口,相当于Spring帮助写好了一个 RowMapper
的实现类。其中最关键的当然是实现方法 mapRow()
的内容。
public class SingleColumnRowMapper<T> implements RowMapper<T> {
······
@Nullable
private Class<?> requiredType;
public SingleColumnRowMapper(Class<T> requiredType) {
setRequiredType(requiredType);
}
public void setRequiredType(Class<T> requiredType) {
this.requiredType = ClassUtils.resolvePrimitiveIfNecessary(requiredType);
}
/**
* Extract a value for the single column in the current row.
* <p>Validates that there is only one column selected,
* then delegates to {@code getColumnValue()} and also
* {@code convertValueToRequiredType}, if necessary.
* @see java.sql.ResultSetMetaData#getColumnCount()
* @see #getColumnValue(java.sql.ResultSet, int, Class)
* @see #convertValueToRequiredType(Object, Class)
*/
@Override
@SuppressWarnings("unchecked")
@Nullable
public T mapRow(ResultSet rs, int rowNum) throws SQLException {
// 验证返回结果数
ResultSetMetaData rsmd = rs.getMetaData();
int nrOfColumns = rsmd.getColumnCount();
if (nrOfColumns != 1) {
throw new IncorrectResultSetColumnCountException(1, nrOfColumns);
}
// Extract column value from JDBC ResultSet.
// 抽取第一个结果进行处理
Object result = getColumnValue(rs, 1, this.requiredType);
if (result != null && this.requiredType != null && !this.requiredType.isInstance(result)) {
// Extracted value does not match already: try to convert it.
// 转换到对应的类型
try {
return (T) convertValueToRequiredType(result, this.requiredType);
}
catch (IllegalArgumentException ex) {
throw new TypeMismatchDataAccessException(
"Type mismatch affecting row number " + rowNum + " and column type '" +
rsmd.getColumnTypeName(1) + "': " + ex.getMessage());
}
}
return (T) result;
}
/**
* Retrieve a JDBC object value for the specified column.
* <p>The default implementation calls
* {@link JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)}.
* If no required type has been specified, this method delegates to
* {@code getColumnValue(rs, index)}, which basically calls
* {@code ResultSet.getObject(index)} but applies some additional
* default conversion to appropriate value types.
* @param rs is the ResultSet holding the data
* @param index is the column index
* @param requiredType the type that each result object is expected to match
* (or {@code null} if none specified)
* @return the Object value
* @throws SQLException in case of extraction failure
* @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)
* @see #getColumnValue(java.sql.ResultSet, int)
*/
@Nullable
protected Object getColumnValue(ResultSet rs, int index, @Nullable Class<?> requiredType) throws SQLException {
if (requiredType != null) {
return JdbcUtils.getResultSetValue(rs, index, requiredType);
}
else {
// No required type specified -> perform default extraction.
return getColumnValue(rs, index);
}
}
/**
* Retrieve a JDBC object value for the specified column, using the most
* appropriate value type. Called if no required type has been specified.
* <p>The default implementation delegates to {@code JdbcUtils.getResultSetValue()},
* which uses the {@code ResultSet.getObject(index)} method. Additionally,
* it includes a "hack" to get around Oracle returning a non-standard object for
* their TIMESTAMP datatype. See the {@code JdbcUtils#getResultSetValue()}
* javadoc for details.
* @param rs is the ResultSet holding the data
* @param index is the column index
* @return the Object value
* @throws SQLException in case of extraction failure
* @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int)
*/
@Nullable
protected Object getColumnValue(ResultSet rs, int index) throws SQLException {
return JdbcUtils.getResultSetValue(rs, index);
}
/**
* Convert the given column value to the specified required type.
* Only called if the extracted column value does not match already.
* <p>If the required type is String, the value will simply get stringified
* via {@code toString()}. In case of a Number, the value will be
* converted into a Number, either through number conversion or through
* String parsing (depending on the value type). Otherwise, the value will
* be converted to a required type using the {@link ConversionService}.
* @param value the column value as extracted from {@code getColumnValue()}
* (never {@code null})
* @param requiredType the type that each result object is expected to match
* (never {@code null})
* @return the converted value
* @see #getColumnValue(java.sql.ResultSet, int, Class)
*/
@SuppressWarnings("unchecked")
@Nullable
protected Object convertValueToRequiredType(Object value, Class<?> requiredType) {
if (String.class == requiredType) {
return value.toString();
}
else if (Number.class.isAssignableFrom(requiredType)) {
if (value instanceof Number) {
// Convert original Number to target Number class.
// 转换原始Number类型的实体到Number类
return NumberUtils.convertNumberToTargetClass(((Number) value), (Class<Number>) requiredType);
}
else {
// Convert stringified value to target Number class.
// 转换String类型的值到Number类
return NumberUtils.parseNumber(value.toString(),(Class<Number>) requiredType);
}
}
else if (this.conversionService != null && this.conversionService.canConvert(value.getClass(), requiredType)) {
return this.conversionService.convert(value, requiredType);
}
else {
throw new IllegalArgumentException(
"Value [" + value + "] is of type [" + value.getClass().getName() +
"] and cannot be converted to required type [" + requiredType.getName() + "]");
}
}
······
}
还是继续回到 queryForObject(String sql, RowMapper<T> rowMapper)
方法,首先调用了之前分析过的 query
重载方法,返回最终的返回结果。
@Override
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
}
然后调用 DataAccessUtils
的 nullableSingleResult()
方法返回查询得到的集合类型结果的第一个也应仅有的一个元素,否则抛出异常。
/**
* Return a single result object from the given Collection.
* <p>Throws an exception if 0 or more than 1 element found.
* @param results the result Collection (can be {@code null}
* and is also expected to contain {@code null} elements)
* @return the single result object
* @throws IncorrectResultSizeDataAccessException if more than one
* element has been found in the given Collection
* @throws EmptyResultDataAccessException if no element at all
* has been found in the given Collection
* @since 5.0.2
*/
@Nullable
public static <T> T nullableSingleResult(@Nullable Collection<T> results) throws IncorrectResultSizeDataAccessException {
// This is identical to the requiredSingleResult implementation but differs in the
// semantics of the incoming Collection (which we currently can't formally express)
if (CollectionUtils.isEmpty(results)) {
throw new EmptyResultDataAccessException(1);
}
if (results.size() > 1) {
throw new IncorrectResultSizeDataAccessException(1, results.size());
}
return results.iterator().next();
}
V. 总结
Spring JDBC内容相对来说还较少,但是其主要还是基于Spring的IOC容器,以及后面文章提及的事务会依赖AOP。Spring JDBC主要利用模板模式加上回调接口的形式,对JDBC的固定流程代码很好的包装起来,个性化的代码通过让用户在实现接口的时候进行定制,关于这一点,在Android开发中还是很常见的。
不过我们依然任重道远,可以看到我们在分析获取连接池那一块的代码对于事务的分析都基本略过,我们留到后面再继续分解。