前言
内容主要参考自《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 in