你好,我是郭屹,今天我们继续手写MiniSpring。
上节课,我们从JDBC这些套路性的程序流程中抽取出了一个通用模板。然后进行了拆解,将SQL语句当作参数传入,而SQL语句执行之后的结果处理逻辑也作为一个匿名类传入,又抽取出了数据源的概念。下面我们接着上节课的思路,继续拆解JDBC程序。
我们现在观察应用程序怎么使用的JdbcTemplate,看这些代码,还是会发现几个问题。
这些问题,我们都需要在这节课上一个个解决。
先看SQL语句参数的传入问题,我们注意到现在往PreparedStatement中传入参数是这样实现的。
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if (arg instanceof String) {
pstmt.setString(i+1, (String)arg);
}
else if (arg instanceof Integer) {
pstmt.setInt(i+1, (int)arg);
}
else if (arg instanceof java.util.Date) {
pstmt.setDate(i+1, new java.sql.Date(((java.util.Date)arg).getTime()));
}
}
简单地说,这些参数都是一个个手工传入进去的。但我们想让参数传入的过程自动化一点,所以现在我们来修改一下,把JDBC里传参数的代码进行包装,用一个专门的部件专门做这件事情,于是我们引入 ArgumentPreparedStatementSetter,通过里面的setValues()方法把参数传进PreparedStatement。
package com.minis.jdbc.core;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class ArgumentPreparedStatementSetter {
private final Object[] args; //参数数组
public ArgumentPreparedStatementSetter(Object[] args) {
this.args = args;
}
//设置SQL参数
public void setValues(PreparedStatement pstmt) throws SQLException {
if (this.args != null) {
for (int i = 0; i < this.args.length; i++) {
Object arg = this.args[i];
doSetValue(pstmt, i + 1, arg);
}
}
}
//对某个参数,设置参数值
protected void doSetValue(PreparedStatement pstmt, int parameterPosition, Object argValue) throws SQLException {
Object arg = argValue;
//判断参数类型,调用相应的JDBC set方法
if (arg instanceof String) {
pstmt.setString(parameterPosition, (String)arg);
}
else if (arg instanceof Integer) {
pstmt.setInt(parameterPosition, (int)arg);
}
else if (arg instanceof java.util.Date) {
pstmt.setDate(parameterPosition, new java.sql.Date(((java.util.Date)arg).getTime()));
}
}
}
从代码中可以看到,核心仍然是JDBC的set方法,但是包装成了一个独立部件。现在的示例程序只是针对了String、Int和Date三种数据类型,更多的数据类型我们留到后面再扩展。
有了这个专门负责参数传入的setter之后,query()就修改成这个样子。
public Object query(String sql, Object[] args, PreparedStatementCallback pstmtcallback) {
Connection con = null;
PreparedStatement pstmt = null;
try {
//通过data source拿数据库连接
con = dataSource.getConnection();
pstmt = con.prepareStatement(sql);
//通过argumentSetter统一设置参数值
ArgumentPreparedStatementSetter argumentSetter = new ArgumentPreparedStatementSetter(args);
argumentSetter.setValues(pstmt);
return pstmtcallback.doInPreparedStatement(pstmt);
}
catch (Exception e) {
e.printStackTrace();
}
finally {
try {
pstmt.close();
con.close();
} catch (Exception e) {
}
}
return null;
}
我们可以看到,代码简化了很多,手工写的一大堆设置参数的代码不见了,这就体现了专门的部件做专门的事情的优点。
JDBC来执行SQL语句,说起来很简单,就三步,一准备参数,二执行语句,三处理返回结果。准备参数和执行语句这两步我们上面都已经抽取了。接下来我们再优化一下处理返回值的代码,看看能不能提供更多便捷的方法。
我们先看一下现在是怎么处理的,程序体现在pstmtcallback.doInPreparedStatement(pstmt)这个方法里,这是一个callback类,由用户程序自己给定,一般会这么做。
return (User)jdbcTemplate.query(sql, new Object[]{new Integer(userid)},
(pstmt)->{
ResultSet rs = pstmt.executeQuery();
User rtnUser = null;
if (rs.next()) {
rtnUser = new User();
rtnUser.setId(userid);
rtnUser.setName(rs.getString("name"));
rtnUser.setBirthday(new java.util.Date(rs.getDate("birthday").getTime()));
} else {
}
return rtnUser;
}
);
这个本身没有什么问题,这部分逻辑实际上已经剥离出去了。只不过,它限定了用户只能用这么一种方式进行。有时候很不便利,我们还应该考虑给用户程序提供多种方式。比如说,我们想返回的不是一个对象(对应数据库中一条记录),而是对象列表(对应数据库中多条记录)。这种场景很常见,需要我们再单独提供一个便利的工具。
所以我们设计一个接口RowMapper,把JDBC返回的ResultSet里的某一行数据映射成一个对象。
package com.minis.jdbc.core;
import java.sql.ResultSet;
import java.sql.SQLException;
public interface RowMapper<T> {
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
再提供一个接口ResultSetExtractor,把JDBC返回的ResultSet数据集映射为一个集合对象。
package com.minis.jdbc.core;
import java.sql.ResultSet;
import java.sql.SQLException;
public interface ResultSetExtractor<T> {
T extractData(ResultSet rs) throws SQLException;
}
利用上面的两个接口,我们来实现一个RowMapperResultSetExtractor。
package com.minis.jdbc.core;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class RowMapperResultSetExtractor<T> implements ResultSetExtractor<List<T>> {
private final RowMapper<T> rowMapper;
public RowMapperResultSetExtractor(RowMapper<T> rowMapper) {
this.rowMapper = rowMapper;
}
@Override
public List<T> extractData(ResultSet rs) throws SQLException {
List<T> results = new ArrayList<>();
int rowNum = 0;
//对结果集,循环调用mapRow进行数据记录映射
while (rs.next()) {
results.add(this.rowMapper.mapRow(rs, rowNum++));
}
return results;
}
}
这样,SQL语句返回的数据集就自动映射成对象列表了。我们看到,实际的数据映射工作其实不是我们实现的,而是由RowMapper实现的,这个RowMapper既是作为一个参数又是作为一个用户程序传进去的。这很合理,因为确实只有用户程序自己知道自己的数据要如何映射。
好,有了这个工具,我们可以提供一个新的query()方法来返回SQL语句的结果集,代码如下:
public <T> List<T> query(String sql, Object[] args, RowMapper<T> rowMapper) {
RowMapperResultSetExtractor<T> resultExtractor = new RowMapperResultSetExtractor<>(rowMapper);
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
//建立数据库连接
con = dataSource.getConnection();
//准备SQL命令语句
pstmt = con.prepareStatement(sql);
//设置参数
ArgumentPreparedStatementSetter argumentSetter = new ArgumentPreparedStatementSetter(args);
argumentSetter.setValues(pstmt);
//执行语句
rs = pstmt.executeQuery();
//数据库结果集映射为对象列表,返回
return resultExtractor.extractData(rs);
}
catch (Exception e) {
e.printStackTrace();
}
finally {
try {
pstmt.close();
con.close();
} catch (Exception e) {
}
}
return null;
}
那么上层应用程序的service层要改成这样:
public List<User> getUsers(int userid) {
final String sql = "select id, name,birthday from users where id>?";
return (List<User>)jdbcTemplate.query(sql, new Object[]{new Integer(userid)},
new RowMapper<User>(){
public User mapRow(ResultSet rs, int i) throws SQLException {
User rtnUser = new User();
rtnUser.setId(rs.getInt("id"));
rtnUser.setName(rs.getString("name"));
rtnUser.setBirthday(new java.util.Date(rs.getDate("birthday").getTime()));
return rtnUser;
}
}
);
}
service程序里面执行SQL语句,直接按照数据记录的字段的mapping关系,返回一个对象列表。这样,到此为止,MiniSpring的JdbcTemplate就可以提供3种query()方法了。
实际上我们还可以提供更多的工具,你可以举一反三思考一下应该怎么做,这里我就不多说了。
到现在这一步,我们的MiniSpring仍然是在执行SQL语句的时候,去新建数据库连接,使用完之后就释放掉了。我们知道,数据库连接的建立和释放,是很费资源和时间的。所以这个方案不是最优的,那怎样才能解决这个问题呢?有一个方案可以试一试,那就是 池化技术。提前在一个池子里预制多个数据库连接,在应用程序来访问的时候,就给它一个,用完之后再收回到池子中,整个过程中数据库连接一直保持不关闭,这样就大大提升了性能。
所以我们需要改造一下原有的数据库连接,不把它真正关闭,而是设置一个可用不可用的标志。我们用一个新的类,叫PooledConnection,来实现Connetion接口,里面包含了一个普通的Connection,然后用一个标志Active表示是否可用,并且永不关闭。
package com.minis.jdbc.pool;
public class PooledConnection implements Connection{
private Connection connection;
private boolean active;
public PooledConnection() {
}
public PooledConnection(Connection connection, boolean active) {
this.connection = connection;
this.active = active;
}
public Connection getConnection() {
return connection;
}
public void setConnection(Connection connection) {
this.connection = connection;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public void close() throws SQLException {
this.active = false;
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return this.connection.prepareStatement(sql);
}
}
实际代码很长,因为要实现JDBC Connection接口里所有的方法,你可以参考上面的示例代码,别的可以都留空。
最主要的,我们要注意close()方法,它其实不会关闭连接,只是把这个标志设置为false。
基于上面的PooledConnection,我们把原有的DataSource改成PooledDataSource。首先在初始化的时候,就激活所有的数据库连接。
package com.minis.jdbc.pool;
public class PooledDataSource implements DataSource{
private List<PooledConnection> connections = null;
private String driverClassName;
private String url;
private String username;
private String password;
private int initialSize = 2;
private Properties connectionProperties;
private void initPool() {
this.connections = new ArrayList<>(initialSize);
for(int i = 0; i < initialSize; i++){
Connection connect = DriverManager.getConnection(url, username, password);
PooledConnection pooledConnection = new PooledConnection(connect, false);
this.connections.add(pooledConnection);
}
}
}
获取数据库连接的代码如下:
PooledConnection pooledConnection= getAvailableConnection();
while(pooledConnection == null){
pooledConnection = getAvailableConnection();
if(pooledConnection == null){
try {
TimeUnit.MILLISECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return pooledConnection;
可以看出,我们的策略是死等这一个有效的连接。而获取有效连接的代码如下:
private PooledConnection getAvailableConnection() throws SQLException{
for(PooledConnection pooledConnection : this.connections){
if (!pooledConnection.isActive()){
pooledConnection.setActive(true);
return pooledConnection;
}
}
return null;
}
通过代码可以知道,其实它就是拿一个空闲标志的数据库连接来返回。逻辑上这样是可以的,但是,这段代码就会有一个并发问题,多线程的时候不好用,需要改造一下才能适应多线程环境。我们注意到这个池子用的是一个简单的ArrayList,这个默认是不同步的,我们需要手工来做同步,比如使用Collections.synchronizedList(),或者用两个LinkedBlockingQueue,一个用于active连接,一个用于inactive连接。
同样,对DataSource里数据库的相关信息,可以通过配置来注入的。
<bean id="dataSource" class="com.minis.jdbc.pool.PooledDataSource">
<property name="url" value="jdbc:sqlserver://localhost:1433;databasename=DEMO"/>
<property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
<property name="username" value="sa"/>
<property name="password" value="Sql2016"/>
<property type="int" name="initialSize" value="3"/>
</bean>
整个程序的结构实际上没有什么改动,只是将DataSource的实现变成了支持连接池的实现。从这里也可以看出,独立抽取部件、解耦这些手段给程序结构带来了极大的灵活性。
我们这节课,在已有的JdbcTemplate基础之上,仍然按照专门的事情交给专门的部件来做的思路,一步步拆解。
我们把SQL语句参数的处理独立成一个ArgumentPreparedStatementSetter,由它来负责参数的传入。之后对返回结果,我们提供了RowMapper和RowMapperResultSetExtractor,将数据库记录集转换成一个对象的列表,便利了上层应用程序。最后考虑到性能,我们还引入了一个简单的数据库连接池。在这一步步地拆解过程中,JdbcTemplate这个工具越来越完整、便利了。
完整源代码参见 https://github.com/YaleGuo/minis。
学完这节课的内容,我也给你留一道思考题。你想一想我们应该怎么改造数据库连接池,保证多线程安全?欢迎你在留言区与我交流讨论,也欢迎你把这节课分享给需要的朋友。我们下节课见!