Spring源码——JDBC

本文详细探讨了Spring框架中JdbcTemplate对JDBC的封装,从JDBC连接MySQL的基本流程开始,逐步解析了Spring如何简化数据库操作。通过JdbcTemplate的save/update功能和query功能实现,展示了Spring如何利用模板模式将固定流程与个性化操作分离,提供更便捷的数据库操作接口。文章还介绍了执行SQL的execute方法,分析了参数设置、警告处理和资源释放的细节,以及Spring如何处理查询结果并将其转换为自定义对象。
摘要由CSDN通过智能技术生成

前言

内容主要参考自《Spring源码深度解析》一书,算是读书笔记或是原书的补充。进入正文后可能会引来各种不适,毕竟阅读源码是件极其痛苦的事情。

本文主要涉及书中第八章的部分,依照书中内容以及个人理解对Spring源码进行了注释,详见Github仓库:https://github.com/MrSorrow/spring-framework

本章内容基于之前所说的Spring IOC 和 AOP功能,开始下一阶段有关应用方面的一些研究。首先介绍Spring中关于JDBC给我们带来怎样的封装与支持。

I. JDBC连接MySQL

JDBC连接数据库的流程

  1. 在开发环境中加载指定数据库的驱动程序。以数据库 MySQL 为例,我们需要下载 MySQL 支持 JDBC 的驱动程序包(mysql-connector-java-xxx.jar);
  2. 在 Java 代码中利用反射加载驱动程序,例如加载 MySQL 数据库驱动程序的代码为 Class.forName("com.mysql.jdbc.Driver")
  3. 创建数据库连接对象。通过 DriverManager 类来创建数据库连接对象 ConnectionDriverManager 类作用于 Java 程序和 JDBC 驱动程序之间,用于检测所加载的驱动程序是否可以建立连接,然后通过 getConnection() 方法根据数据库的 URL、用户名、密码 创建一个 JDBC Connection 对象,如:Connection connection = DriverManager.getConnection(“URL”, ”用户名”, ”密码”)。其中,URL = 协议名 + IP地址(域名) + 端口 + 数据库名称
  4. 创建 Statement 对象。Statement 对象主要用于执行静态的 SQL 语句并返回它所生成结果的对象。通过数据库连接 Connection 对象的 createStatement() 可以创建一个 Statement 对象。例如:Statement statement = connection.createStatement()
  5. 调用 Statement 对象的相关方法执行相对应的 SQL 语句。例如对数据更新、插入和删除操作的 executeUpdate() 函数以及对数据库进行查询的 executeQuery() 函数。执行这些函数相当于执行了我们定义的 SQL 语句,自然会获得结果。查询操作返回的结果是 ResultSet 对象,其中包含了我们需要的数据。通过 ResultSet 对象的 next() 方法,使得指针指向下一行,然后将数据以列号或者字段名取出。 当 next() 方法返回为 false 时,表示下一行没有数据存在。
  6. ==关闭数据库连接。==使用完数据库或者不需要访问数据库时,通过数据库连接 Connection 对象的 close() 方法及时关闭数据库连接。

JDBC连接MySQL示例

按照上述6个流程,我们搬出最常见 JDBC 流程代码。在此之前,我们需要在我们的 spring-mytest 模块中添加上 MySQL 数据库驱动。

compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.18'

我们使用的数据库表如下。表中有四个字段,idfirst_namelast_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 在固定区间中的所有演员信息。关于 PreparedStatementStatement 的区别可以参考阅读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();
				}
			}
		}
	}
}

运行结果:
jdbc测试结果

使用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容器会帮我们实例化这个对象。此外,我们还需要在容器中注册一个 ActorServicebean,需要注入依赖的连接池对象 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 in
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值