Spring源码深度解析之数据库连接JDBC

Spring源码深度解析之数据库连接JDBC

  JDBC(Java Data Base Connectivity,Java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。JDBC为数据库开发人员提供了一个标准的API,据此可以构建更高级的工具和接口,是数据库开发人员能够用纯Java API编写数据库应用程序,并且可跨平台运行,并且不受数据库供应商的限制。

  JDBC连接数据库流程及原理如下:

  (1)在开发环境中加载指定数据库的驱动程序。接下来的试验中,使用的数据库是MySQL,所以需要去下载MySQl支持JDBC的驱动程序,将下载得到的驱动程序加载进开发环境中。

  (2)在Java程序中加载驱动程序。在Java程序中,可以通过Class.forName(“指定数据库的驱动程序”)的方式来嘉爱添加到开发环境中的驱动程序,例如加载MySQL的数据驱动程序的代码为Class.forName(“com.mysql.jdbc.Driver”)。

  (3)创建数据连接对象。通过DriverManager类创建数据库连接对象Connection。DriverManager类作用于Java程序和JDBC驱动程序之间,用于检查所加载的驱动程序是否可以建立连接,然后通过它的getConnection方法根据数据库的URL、用户名和密码,创建一个JDBC Connection对象。例如:Connection connection = DriverManager.getConnection(“连接数据库的URL”,”用户名”,”密码”)。其中URL=协议名+IP地址(域名)+端口+数据库名称;用户名和密码是指登录数据库时所使用的用户名和密码。具体示例创建MySQL的数据库连接代码如下:

Connection connectMySQL = DriverManager.getConnection(“jdbc:mysql://localhost:3306/myuser”,”root”,”root”);

  (4)创建Statement对象。Statement类的主要是用于执行静态SQL语句并返回它所生产结果的对象。通过Conncetion对象的createStatement()方法可以创建一个Statement对象。例如:Statement statement = connection.createStatement()。具体示例创建Statement对象代码如下:

Statemetn statementMySQL = connectMySQL.createStatement();

  (5)调用Statement对象的相关方法执行相应的SQL语句。通过executeUpdate()方法来对数据更新,包括插入和删除等操作,例如向staff表中插入一条数据的代码:

statement.excuteUpdate(“INSERT INTO staff(name, age, sex, address, depart, worklen, wage)”+ “VALUES(‘Tom1’, 321, ‘M’,’China’,’Personnel’,’3’,’3000’)”);

  通过调用Statement对象的executeQuery()方法进行数据的查询,而查询的结果会得到ResultSet对象,ResultSet表示执行查询数据库后返回的数据的集合,ResultSet对象具有科研指向当前数据行的指针。通过该对象的next()方法,使得指针指向下一行,然后将数据以列号或者字段名取出。如果当next()方法返回null,则表示下一行中没有数据存在。使用示例代码如下:

ResultSet resultSet = statement.executeQuery(“select * from staff”);

  (6)关闭数据库连接。使用完数据库或者不需要访问数据库时,通过Connection的close()方法及时关闭数据库。

一、Spring连接数据库程序实现(JDBC)

Spring中的JDBC连接与直接使用JDBC去连接还是有所差别的,Spring对JDBC做了大量的封装,消除了冗余代码,使得开发量大大减小。下面通过一个小例子让大家简单认识Spring中的JDBC操作。

  (1)创建数据表结构

1 CREATE table 'user'(
2 'id' int(11) NOT NULL auto_increment,
3 'name' varchar(255) default null,
4 'age' int(11) default null,
5 'sex' varchar(255) default null,
6 primary key ('id')
7 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

  (2)创建对应数据表的PO。

1 public class User{
2 private int id;
3 private String name;
4 private int age;
5 private String set;
6 //省略get/set方法
7 }

  (3)创建表与实体间的映射

1 public class UserRowMapper implements RowMapper{
2 @Override
3 public Object mapRow(ResultSet set, int index) throws SQLException {
4 User person = new User(set.getInt("id"), set.getString("name"), set.getInt("age"), set.getString("sex"));
5 return person;
6 }
7 }

  (4)创建数据操作接口

1 public interface UserService{
2 public void save(User user);
3 public List<User> getUsers();
4 }

  (5)创建数据操作接口实现类

 1 public class UserServiceImpl implements UserService{
2 private JdbcTemplate jdbcTemplate;
3
4 //设置数据源
5 public void setDataSource(DataSource dataSource){
6 this.jdbcTemplate = new JdbcTemplate(dataSource);
7 }
8
9 public void save(User user){
10 jdbcTemplate.update("insert into user(name, age, sex) value(?, ?, ?)",
11 new Object[] {user.getName(), user.getAge(), user.getSex()},
12 new int[] {java.sql.Types.VARCHAR, java.sql.Types,INTEGER, java.sql.Types.VARCHAR});
13 }
14
15 @SuppressWarnings("unchecked")
16 public List<User> getUser() {
17 List<User> list = jdbcTemplate.query("select * from user", new UserRowMapper());
18 return list;
19 }
20 }

  (6)创建Spring配置文件

 1 <?xml version="1.0" encoding="UTF-8" ?>
2 <beans xmlns="http://www.Springframework.org/schema/beans"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://www.Springframework.org/schema/beans
5 http://www.Springframework.org/schema/beans/Spring-beans-2.5.xsd">
6 <!--配置数据源-->
7 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
8 <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
9 <property name="uri" value="jdbc:mysql://localhost:3306/lexueba"/>
10 <property name="username" value="root"/>
11 <property name="password" value="haojia0421xixi" />
12 <!--连接池启动时的初始值-->
13 <property name="initialSize" value="1"/>
14 <!--连接池的最大值-->
15 <property name="maxActive" value="300"/>
16 <!--最大空闲值,当经过一个最高峰时间后,连接池可以慢慢将已经用不到的连接慢慢释放一部分,一直减少到maxIdle为止-->
17 <property name="maxIdle" value="2"/>
18 <!--最小空闲值,当空闲的连接数少于阀值时,连接池就会预申请去一些连接,以免洪峰来时来不及申请-->
19 <property name="minIdle" value="1"/>
20 </bean>
21
22 <!--配置业务bean:PersonServiceBean-->
23 <bean id="userService" class="service.UserServiceImpl">
24 <!--向属性DataSource注入数据源-->
25 <property name="dataSource" ref="dataSource"/>
26 </bean>
27 </beans>

  (7)测试

 1 public class SpringJDBCTest{
2 public static void main(String[] args) {
3 ApplicationContext act = new ClassPathXmlApplicationContext("bean.xml");
4 UserService userService = (UserService) act.getBean("userService");
5 User user = new User();
6 user.setName("张三");
7 user.setAge(20);
8 user.setSex("男");
9 //保存一条记录
10 userService.save(user);
11
12 List<User> person1 = userService.getUser();
13 Systemout.out.println("得到所有的User");
14 for (User person2:person1) {
15 System.out.println(person2.getId() + " " + person2.getName() + " " + person2.getAge() + " " + person2.getSex());
16 }
17 }
18 }

二、sava/update功能的实现

  我们以上面的例子为基础开始分析Spring中对JDBC的支持,首先寻找整个功能的切入点,在示例中我们可以看到所有的数据库操作都封装在了UserServiceImpl中,而UserServiceImple中的所有数据库操作又以其内部属性jdbcTemplate为基础。这个jdbcTemplate可以作为源码分析的切入点,我们一起看看它是如何实现定义又是如何被初始化的。

  在UserServiceImple中jdbcTemplate的初始化是从setDataSource函数开始的,DataSource实例通过参数注入,DataSource的创建过程是引入第三方的连接池,这里不做过多的介绍。DataSource是整个数据库操作的基础,里面封装了整个数据库的连接信息。我们首先以保存实体类为例进行代码跟踪。

1 public void save(User user){
2 jdbcTemplate.update("insert into user(name, age, sex) value(?, ?, ?)",
3 new Object[] {user.getName(), user.getAge(), user.getSex()},
4 new int[] {java.sql.Types.VARCHAR, java.sql.Types,INTEGER, java.sql.Types.VARCHAR});
5 }

  对于保存一个实体类来讲,在操作中我们只需要提供SQL语句及语句中对应的参数和参数类型,其他操作便可以交由Spring来完成了,这些工作到底包括什么呢?进入jdbcTemplate中的update方法:

1     @Override
2 public int update(String sql, @Nullable PreparedStatementSetter pss) throws DataAccessException {
3 return update(new SimplePreparedStatementCreator(sql), pss);
4 }
5
6 @Override
7 public int update(String sql, Object[] args, int[] argTypes) throws DataAccessException {
8 return update(sql, newArgTypePreparedStatementSetter(args, argTypes));
9 }

  进入update方法后,Spring并不是急于进入核心处理操作,而是做足了准备工作,使用ArgPreparedStatementSetter对参数与参数类型进行封装,同时又使用Simple PreparedStatementCreator对SQL语句进行封装。

  经过数据封装后便可以进入了核心的数据处理代码了。

 1     protected int update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss)
2 throws DataAccessException {
3
4 logger.debug("Executing prepared SQL update");
5
6 return updateCount(execute(psc, ps -> {
7 try {
8 if (pss != null) {
9 //设置PreparedStatement所需的全部参数
10 pss.setValues(ps);
11 }
12 int rows = ps.executeUpdate();
13 if (logger.isTraceEnabled()) {
14 logger.trace("SQL update affected " + rows + " rows");
15 }
16 return rows;
17 }
18 finally {
19 if (pss instanceof ParameterDisposer) {
20 ((ParameterDisposer) pss).cleanupParameters();
21 }
22 }
23 }));
24 }

  如果读者了解过其他操作方法,可以知道execute方法是最基础的操作。而其他操作比如update、query等方法则是传入不同的PreparedStatementCallback参数来执行不同的逻辑。

(一)基础方法execute

  execute作为数据库操作的核心入口,将大多数数据库操作相同的步骤统一封装,而将个性化的操作使用参数PreparedStatementCallback进行回调。

 1     public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
2 throws DataAccessException {
3
4 Assert.notNull(psc, "PreparedStatementCreator must not be null");
5 Assert.notNull(action, "Callback object must not be null");
6 if (logger.isDebugEnabled()) {
7 String sql = getSql(psc);
8 logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
9 }
10
11 //获取数据库连接
12 Connection con = DataSourceUtils.getConnection(obtainDataSource());
13 PreparedStatement ps = null;
14 try {
15 ps = psc.createPreparedStatement(con);
16 //应用用户设定的输入参数
17 applyStatementSettings(ps);
18 //调用回调函数
19 T result = action.doInPreparedStatement(ps);
20 handleWarnings(ps);
21 return result;
22 }
23 catch (SQLException ex) {
24 // Release Connection early, to avoid potential connection pool deadlock
25 // in the case when the exception translator hasn't been initialized yet.
26 //释放数据库连接避免当异常转换器没有被初始化的时候出现潜在的连接池死锁
27 if (psc instanceof ParameterDisposer) {
28 ((ParameterDisposer) psc).cleanupParameters();
29 }
30 String sql = getSql(psc);
31 psc = null;
32 JdbcUtils.closeStatement(ps);
33 ps = null;
34 DataSourceUtils.releaseConnection(con, getDataSource());
35 con = null;
36 throw translateException("PreparedStatementCallback", sql, ex);
37 }
38 finally {
39 if (psc instanceof ParameterDisposer) {
40 ((ParameterDisposer) psc).cleanupParameters();
41 }
42 JdbcUtils.closeStatement(ps);
43 DataSourceUtils.releaseConnection(con, getDataSource());
44 }
45 }

  以上方法对常用操作进行了封装,包括如下几项内容。

  1、获取数据库连接

  获取数据库连接池也并非直接使用dataSource.getConnection()方法那么简单,同样也考虑了诸多情况。

 1     public static Connection doGetConnection(DataSource dataSource) throws SQLException {
2 Assert.notNull(dataSource, "No DataSource specified");
3
4 ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
5 if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
6 conHolder.requested();
7 if (!conHolder.hasConnection()) {
8 logger.debug("Fetching resumed JDBC Connection from DataSource");
9 conHolder.setConnection(fetchConnection(dataSource));
10 }
11 return conHolder.getConnection();
12 }
13 // Else we either got no holder or an empty thread-bound holder here.
14
15 logger.debug("Fetching JDBC Connection from DataSource");
16 Connection con = fetchConnection(dataSource);
17
18 //当前线程支持同步
19 if (TransactionSynchronizationManager.isSynchronizationActive()) {
20 try {
21 // Use same Connection for further JDBC actions within the transaction.
22 // Thread-bound object will get removed by synchronization at transaction completion.
23 //在事务中使用同一数据库连接
24 ConnectionHolder holderToUse = conHolder;
25 if (holderToUse == null) {
26 holderToUse = new ConnectionHolder(con);
27 }
28 else {
29 holderToUse.setConnection(con);
30 }
31 //记录数据库连接
32 holderToUse.requested();
33 TransactionSynchronizationManager.registerSynchronization(
34 new ConnectionSynchronization(holderToUse, dataSource));
35 holderToUse.setSynchronizedWithTransaction(true);
36 if (holderToUse != conHolder) {
37 TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
38 }
39 }
40 catch (RuntimeException ex) {
41 // Unexpected exception from external delegation call -> close Connection and rethrow.
42 releaseConnection(con, dataSource);
43 throw ex;
44 }
45 }
46
47 return con;
48 }

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

  2、应用用户设定的输入参数。

 1     protected void applyStatementSettings(Statement stmt) throws SQLException {
2 int fetchSize = getFetchSize();
3 if (fetchSize != -1) {
4 stmt.setFetchSize(fetchSize);
5 }
6 int maxRows = getMaxRows();
7 if (maxRows != -1) {
8 stmt.setMaxRows(maxRows);
9 }
10 DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout());
11 }

  setFetchSize最主要是为了减少网络交互次数设计的。访问ResultSet时,如果它每次只从服务器上读取一行数据,则会产生大量的开销。setFetchSize的意思是当调用rs.next时,ResultSet会一次性从服务器上取得多少行数据回来,这样在下次rs.next时,它可以直接从内存中获取数据而不需要网络交互,提高效率。这个设置可能会被某些JDBC驱动忽略,而且设置过大会造成内存的上升。

  setMaxRows将此Statement对象生成的所有ResultSet对象可以包含的最大行数限制设置为给定数。

  3、调用回调函数

  处理一些通用方法外的个性化处理,也就是PreparedStatementCallback类型的参数的doInPreparedStatement方法的回调。

  4、警告处理

 1     protected void handleWarnings(Statement stmt) throws SQLException {
2 //当设置为忽略警告时只尝试打印日志
3 if (isIgnoreWarnings()) {
4 if (logger.isDebugEnabled()) {
5 //如果日志开启的情况下打印日志
6 SQLWarning warningToLog = stmt.getWarnings();
7 while (warningToLog != null) {
8 logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() + "', error code '" +
9 warningToLog.getErrorCode() + "', message [" + warningToLog.getMessage() + "]");
10 warningToLog = warningToLog.getNextWarning();
11 }
12 }
13 }
14 else {
15 handleWarnings(stmt.getWarnings());
16 }
17 }

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

  很多人不是很理解什么情况下会产生警告而不是异常,在这里给读者提示个最常见的警告:DataTruncation,DataTruncation直接继承SQLWaring,由于某种原因意外地截断数据值时会以DataTruncation警告形式报告异常。

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

  5、资源释放

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

 1     public static void releaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) {
2 try {
3 doReleaseConnection(con, dataSource);
4 }
5 catch (SQLException ex) {
6 logger.debug("Could not close JDBC Connection", ex);
7 }
8 catch (Throwable ex) {
9 logger.debug("Unexpected exception on closing JDBC Connection", ex);
10 }
11 }

  上面函数又调用了本类中的doReleaseConnection函数:

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

(二)Update函数

 1     protected int update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss)
2 throws DataAccessException {
3
4 logger.debug("Executing prepared SQL update");
5
6 return updateCount(execute(psc, ps -> {
7 try {
8 if (pss != null) {
9 //设置PreparedStatement所需的全部参数
10 pss.setValues(ps);
11 }
12 int rows = ps.executeUpdate();
13 if (logger.isTraceEnabled()) {
14 logger.trace("SQL update affected " + rows + " rows");
15 }
16 return rows;
17 }
18 finally {
19 if (pss instanceof ParameterDisposer) {
20 ((ParameterDisposer) pss).cleanupParameters();
21 }
22 }
23 }));
24 }

  其中用于真正执行SQL的int rows = ps.executeUpdate();没有太多需要讲解的,因为我们平时在直接使用JDBC方式进行调用的时候经常使用此方法。但是,对于设置输入参数的函数pss.setValues(ps);,我们有必要去深入研究一下。在没有分析代码之前,我们至少可以知道其功能,不妨再回顾下Spring中使用SQL的执行过程,直接使用:

1 jdbcTemplate.update("insert into user(name, age, sex) value(?, ?, ?)",
2 new Object[] {user.getName(), user.getAge(), user.getSex()},
3 new int[] {java.sql.Types.VARCHAR, java.sql.Types,INTEGER, java.sql.Types.VARCHAR});

  SQL语句对应的参数的类型清晰明了,这都归功于Spring为我们做了封装,而真正的JDBC调用其实非常繁琐,你需要这么做:

1 PreparedStatement updateSales = con.prepareStatement("insert into user(name, age, sex) values(?, ?, ?)");
2 updateSales.setString(1, user.getName());
3 updateSales.setInt(2, user.getAge());
4 updateSales.setString(3, user.getSex());

  那么看看Spring是如何做到封装上面的操作呢?
  首先,所有的操作都是以pss.setValues(ps)为入口的。还记得我们之前的分析路程吗?这个pss所代表的当前类正是ArgumentTypePreparedStatementSetter。其中的setValues方法如下:

 1     public void setValues(PreparedStatement ps) throws SQLException {
2 int parameterPosition = 1;
3 if (this.args != null && this.argTypes != null) {
4 //遍历每个参数以作类型匹配和转换
5 for (int i = 0; i < this.args.length; i++) {
6 Object arg = this.args[i];
7 //如果是集合类型则需要进入集合类内部递归解析集合内部属性
8 if (arg instanceof Collection && this.argTypes[i] != Types.ARRAY) {
9 Collection<?> entries = (Collection<?>) arg;
10 for (Object entry : entries) {
11 if (entry instanceof Object[]) {
12 Object[] valueArray = ((Object[]) entry);
13 for (Object argValue : valueArray) {
14 doSetValue(ps, parameterPosition, this.argTypes[i], argValue);
15 parameterPosition++;
16 }
17 }
18 else {
19 //解析当前属性
20 doSetValue(ps, parameterPosition, this.argTypes[i], entry);
21 parameterPosition++;
22 }
23 }
24 }
25 else {
26 doSetValue(ps, parameterPosition, this.argTypes[i], arg);
27 parameterPosition++;
28 }
29 }
30 }
31 }

  对单个参数及类型的匹配处理:

1     protected void doSetValue(PreparedStatement ps, int parameterPosition, int argType, Object argValue)
2 throws SQLException {
3
4 StatementCreatorUtils.setParameterValue(ps, parameterPosition, argType, argValue);
5 }

  上述函数调用了StatementCreatorUtils类的setParameterValue方法,进入:

1     public static void setParameterValue(PreparedStatement ps, int paramIndex, SqlParameter param,
2 @Nullable Object inValue) throws SQLException {
3
4 setParameterValueInternal(ps, paramIndex, param.getSqlType(), param.getTypeName(), param.getScale(), inValue);
5 }

  调用了本类的setParameterValueInternal函数,继续进入:

 1     private static void setParameterValueInternal(PreparedStatement ps, int paramIndex, int sqlType,
2 @Nullable String typeName, @Nullable Integer scale, @Nullable Object inValue) throws SQLException {
3
4 String typeNameToUse = typeName;
5 int sqlTypeToUse = sqlType;
6 Object inValueToUse = inValue;
7
8 // override type info?
9 if (inValue instanceof SqlParameterValue) {
10 SqlParameterValue parameterValue = (SqlParameterValue) inValue;
11 if (logger.isDebugEnabled()) {
12 logger.debug("Overriding type info with runtime info from SqlParameterValue: column index " + paramIndex +
13 ", SQL type " + parameterValue.getSqlType() + ", type name " + parameterValue.getTypeName());
14 }
15 if (parameterValue.getSqlType() != SqlTypeValue.TYPE_UNKNOWN) {
16 sqlTypeToUse = parameterValue.getSqlType();
17 }
18 if (parameterValue.getTypeName() != null) {
19 typeNameToUse = parameterValue.getTypeName();
20 }
21 inValueToUse = parameterValue.getValue();
22 }
23
24 if (logger.isTraceEnabled()) {
25 logger.trace("Setting SQL statement parameter value: column index " + paramIndex +
26 ", parameter value [" + inValueToUse +
27 "], value class [" + (inValueToUse != null ? inValueToUse.getClass().getName() : "null") +
28 "], SQL type " + (sqlTypeToUse == SqlTypeValue.TYPE_UNKNOWN ? "unknown" : Integer.toString(sqlTypeToUse)));
29 }
30
31 if (inValueToUse == null) {
32 setNull(ps, paramIndex, sqlTypeToUse, typeNameToUse);
33 }
34 else {
35 setValue(ps, paramIndex, sqlTypeToUse, typeNameToUse, scale, inValueToUse);
36 }
37 }

三、query功能的实现

  在之前的章节中我们介绍了update方法的功能实现。那么在数据库操作中查找操作也是使用非常高的函数,同样我们也需要了解它的实现过程。使用方法如下:

1 List<User> list = jdbcTemplate.query("select * from user where age=?",
2 new Object[][20], new int[]{java.sql.Types.INTEGER}, new UserRowMapper());

  跟踪jdbcTemplate的query方法:

1     @Override
2 public <T> List<T> query(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper) throws DataAccessException {
3 return result(query(sql, args, argTypes, new RowMapperResultSetExtractor<>(rowMapper)));
4 }
1     @Override
2 @Nullable
3 public <T> T query(String sql, Object[] args, int[] argTypes, ResultSetExtractor<T> rse) throws DataAccessException {
4 return query(sql, newArgTypePreparedStatementSetter(args, argTypes), rse);
5 }

  上面的函数中和update方法中同样使用了 newArgTypePreparedStatementSetter。

1     public <T> T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor<T> rse) throws DataAccessException {
2 return query(new SimplePreparedStatementCreator(sql), pss, rse);
3 }
 1     public <T> T query(
2 PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor<T> rse)
3 throws DataAccessException {
4
5 Assert.notNull(rse, "ResultSetExtractor must not be null");
6 logger.debug("Executing prepared SQL query");
7
8 return execute(psc, new PreparedStatementCallback<T>() {
9 @Override
10 @Nullable
11 public T doInPreparedStatement(PreparedStatement ps) throws SQLException {
12 ResultSet rs = null;
13 try {
14 if (pss != null) {
15 //设置PreparedStatement所需的全部参数
16 pss.setValues(ps);
17 }
18 rs = ps.executeQuery();
19 return rse.extractData(rs);
20 }
21 finally {
22 JdbcUtils.closeResultSet(rs);
23 if (pss instanceof ParameterDisposer) {
24 ((ParameterDisposer) pss).cleanupParameters();
25 }
26 }
27 }
28 });
29 }

  可以看到整体套路和update差不多,只不过在回调类PreparedStatementCallback的实现中使用的是ps.executeQuery()执行查询操作,而且在返回方法上也做了一些额外的处理。

  rse.extractData(rs)方法负责将结果进行封装并转换到POJO,rse当前代表的类为RowMapperResultSetExtractor,而在构造RowMapperResultSetExtractor的时候我们又将自定义的rowMapper设置了进去。调用代码如下:

1     public List<T> extractData(ResultSet rs) throws SQLException {
2 List<T> results = (this.rowsExpected > 0 ? new ArrayList<>(this.rowsExpected) : new ArrayList<>());
3 int rowNum = 0;
4 while (rs.next()) {
5 results.add(this.rowMapper.mapRow(rs, rowNum++));
6 }
7 return results;
8 }

  上面的代码并没有什么负责的逻辑,只是对返回的结果遍历并以此使用rowMapper进行转换。

  之前降了update方法以及query方法,使用这两个函数示例的SQL都是带参数值的,也就是带有“?”的,那么还有另一种情况是不带有“?”的,Spring中使用的是另一种处理方式,例如:

List<User> list = jdbcTemplate.query("select * from user", new UserRowMapper());

  跟踪进入:

 1     public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
2 Assert.notNull(sql, "SQL must not be null");
3 Assert.notNull(rse, "ResultSetExtractor must not be null");
4 if (logger.isDebugEnabled()) {
5 logger.debug("Executing SQL query [" + sql + "]");
6 }
7
8 /**
9 * Callback to execute the query.
10 */
11 class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
12 @Override
13 @Nullable
14 public T doInStatement(Statement stmt) throws SQLException {
15 ResultSet rs = null;
16 try {
17 rs = stmt.executeQuery(sql);
18 return rse.extractData(rs);
19 }
20 finally {
21 JdbcUtils.closeResultSet(rs);
22 }
23 }
24 @Override
25 public String getSql() {
26 return sql;
27 }
28 }
29
30 return execute(new QueryStatementCallback());
31 }

  与之前的query方法最大的不同是少了参数及参数类型的传递,自然也少了PreparedStatementSetter类型的封装。既然少了PreparedStatementSetter类型的传入,调用的execute方法自然也会有所改变了。

 1     public <T> T execute(StatementCallback<T> action) throws DataAccessException {
2 Assert.notNull(action, "Callback object must not be null");
3
4 Connection con = DataSourceUtils.getConnection(obtainDataSource());
5 Statement stmt = null;
6 try {
7 stmt = con.createStatement();
8 applyStatementSettings(stmt);
9 T result = action.doInStatement(stmt);
10 handleWarnings(stmt);
11 return result;
12 }
13 catch (SQLException ex) {
14 // Release Connection early, to avoid potential connection pool deadlock
15 // in the case when the exception translator hasn't been initialized yet.
16 String sql = getSql(action);
17 JdbcUtils.closeStatement(stmt);
18 stmt = null;
19 DataSourceUtils.releaseConnection(con, getDataSource());
20 con = null;
21 throw translateException("StatementCallback", sql, ex);
22 }
23 finally {
24 JdbcUtils.closeStatement(stmt);
25 DataSourceUtils.releaseConnection(con, getDataSource());
26 }
27 }

  这个execute与之前的execute并无太大的差别,都是做一些常规的处理,诸如获取连接、释放连接等,但是,有一个地方是不一样的,就是statement的创建。这里直接使用connection创建,而带有参数的SQL使用的是PreparedStatementCreator类来创建的。一个是普通的Statement,另一个是PreparedStatement,两者究竟是何区别呢?

  PreparedStatement接口继承Statement,并与之在两方面有所不同。

  1、PreparedStatement实例包含已经编译的SQL语句。这就是使语句“准备好”。包含于PreparedStatement对象中的SQL语句可具有一个或者多个IN参数。IN参数的值在SQL语句创建时未被指定。相反的,该语句为每个IN参数保留一个问号(“?”)作为占位符。每个问号的值必须在该语句执行之前,通过适当的setXXX方法来提供。

  2、由于PreparedStatement对象已预编译过,所以其执行速度要快于Statement对象。因此,多次执行的SQL语句经常创建为PreparedStatement对象,以提高效率。

  作为Statement的子类,PreparedStatement继承了Statement的所有功能。另外,它还添加了一整套方法,用于设置发送给数据库以取代IN参数占位符的值。同时,三种方法execute、executeQuery和executeUpdate已被更改以使之不再需要参数。这些方法的Statement形式(接收SQL语句参数的形式)不应该用于PreparedStatement对象。

四、queryForObject

  Spring中不仅为我们提供了query方法,还在此基础上做了封装,提供了不同类型的query方法。

  我们以queryForObject为例,来讨论一下Spring是如何在返回结果的基础上进行封装的。

1     public <T> T queryForObject(String sql, Class<T> requiredType) throws DataAccessException {
2 return queryForObject(sql, getSingleColumnRowMapper(requiredType));
3 }
1     public <T> T queryForObject(String sql, RowMapper<T> rowMapper) throws DataAccessException {
2 List<T> results = query(sql, rowMapper);
3 return DataAccessUtils.nullableSingleResult(results);
4 }

  其实最大的不同还是对于RowMapper的使用,SingleColumnRowMapper类中的mapRow:

 1     public T mapRow(ResultSet rs, int rowNum) throws SQLException {
2 // Validate column count.
3 //验证返回结果
4 ResultSetMetaData rsmd = rs.getMetaData();
5 int nrOfColumns = rsmd.getColumnCount();
6 if (nrOfColumns != 1) {
7 throw new IncorrectResultSetColumnCountException(1, nrOfColumns);
8 }
9
10 // Extract column value from JDBC ResultSet.
11 //抽取第一个结果进行处理
12 Object result = getColumnValue(rs, 1, this.requiredType);
13 if (result != null && this.requiredType != null && !this.requiredType.isInstance(result)) {
14 // Extracted value does not match already: try to convert it.
15 //转换到对象的类型
16 try {
17 return (T) convertValueToRequiredType(result, this.requiredType);
18 }
19 catch (IllegalArgumentException ex) {
20 throw new TypeMismatchDataAccessException(
21 "Type mismatch affecting row number " + rowNum + " and column type '" +
22 rsmd.getColumnTypeName(1) + "': " + ex.getMessage());
23 }
24 }
25 return (T) result;
26 }

  对应的类型转换函数:

 1     protected Object convertValueToRequiredType(Object value, Class<?> requiredType) {
2 if (String.class == requiredType) {
3 return value.toString();
4 }
5 else if (Number.class.isAssignableFrom(requiredType)) {
6 if (value instanceof Number) {
7 // Convert original Number to target Number class.
8 //转换原始的Number类型的实体到Number类
9 return NumberUtils.convertNumberToTargetClass(((Number) value), (Class<Number>) requiredType);
10 }
11 else {
12 // Convert stringified value to target Number class.
13 //转换String类型的值到Number类
14 return NumberUtils.parseNumber(value.toString(),(Class<Number>) requiredType);
15 }
16 }
17 else if (this.conversionService != null && this.conversionService.canConvert(value.getClass(), requiredType)) {
18 return this.conversionService.convert(value, requiredType);
19 }
20 else {
21 throw new IllegalArgumentException(
22 "Value [" + value + "] is of type [" + value.getClass().getName() +
23 "] and cannot be converted to required type [" + requiredType.getName() + "]");
24 }
25 }

  本文摘自《Spring源码深度解析》数据库连接JDBC,作者:郝佳。本文代码基于的Spring版本为5.2.4.BUILD-SNAPSHOT,和原书代码部分会略有不同。

拓展阅读:
  Spring框架之beans源码完全解析
  Spring框架之AOP源码完全解析
  Spring框架之jms源码完全解析
  Spring框架之spring-web http源码完全解析
  Spring框架之spring-web web源码完全解析
       Spring框架之spring-webmvc源码完全解析
  Spring源码深度解析之Spring MVC

上一篇:设置Session的超时时间


下一篇:构造函数语义学之Copy Constructor构建操作(1)