MiniSpring框架学习-JDBC 访问框架:如何抽取专门的部件完成专门的任务?
- [14. 增强模板:如何抽取专门的部件完成专门的任务?](#14. 增强模板:如何抽取专门的部件完成专门的任务?)
-
- 一、先看最终使用方式
- [二、参数绑定交给 ArgumentPreparedStatementSetter](#二、参数绑定交给 ArgumentPreparedStatementSetter)
- 三、结果处理拆成两层
-
- [1. RowMapper:只管当前行](#1. RowMapper:只管当前行)
- [2. ResultSetExtractor:处理整个结果集](#2. ResultSetExtractor:处理整个结果集)
- [3. RowMapperResultSetExtractor:把两者接起来](#3. RowMapperResultSetExtractor:把两者接起来)
- [四、JdbcTemplate 现在怎样串起来](#四、JdbcTemplate 现在怎样串起来)
-
- [1. 执行 PreparedStatement 回调](#1. 执行 PreparedStatement 回调)
- [2. 用 ResultSetExtractor 处理整个结果集](#2. 用 ResultSetExtractor 处理整个结果集)
- [3. 用 RowMapper 做最常见的列表查询](#3. 用 RowMapper 做最常见的列表查询)
- [4. 单行查询和更新](#4. 单行查询和更新)
- 五、再把连接创建换成连接池
-
- [1. PooledConnection:逻辑连接](#1. PooledConnection:逻辑连接)
- [2. PooledDataSource:池化 DataSource](#2. PooledDataSource:池化 DataSource)
- [六、交给 IoC 容器装配](#六、交给 IoC 容器装配)
- 七、完整调用链
- 八、当前实现边界
教程: https://github.com/YaleGuo/minis
极客时间: 手把手带你写一个 MiniSpring
前言:这节内容比较简单,是对上一节内容的完善,针对专门功能的抽象以及增强。
14. 增强模板:如何抽取专门的部件完成专门的任务?
上一节我们已经有了一个能用的 JdbcTemplate:业务代码传入 SQL、参数和回调,模板负责获取连接、创建 PreparedStatement、关闭资源和转换异常。
这一节继续往前走一步:把 JdbcTemplate 里还能拆开的专门任务,交给专门的部件。
JDBC 执行 SQL 大体还是三件事:
text
准备参数
↓
执行语句
↓
处理返回结果
上一节这些逻辑已经被收进 JdbcTemplate,但其中还有一些小职责可以继续拆出来:
| 职责 | 专门部件 |
|---|---|
给 PreparedStatement 绑定参数 |
ArgumentPreparedStatementSetter |
把一行 ResultSet 转成对象 |
RowMapper |
把整个 ResultSet 提取成结果 |
ResultSetExtractor |
用 RowMapper 处理整个结果集 |
RowMapperResultSetExtractor |
| 复用数据库连接 | PooledDataSource、PooledConnection |
这不是为了"类越多越好",而是为了让每个类只负责一件清楚的事。
一、先看最终使用方式
业务层最理想的样子,是只关心业务本身:执行什么 SQL,传什么参数,结果怎么变成 User。
项目里的 UserService 已经变成这样:
java
package com.chenhai.jdbc.example;
import com.chenhai.beans.factory.annotation.Autowired;
import com.chenhai.jdbc.core.JdbcTemplate;
import java.sql.Date;
import java.util.List;
/**
* 演示业务层怎样使用 JdbcTemplate。
*
* UserService 不再负责加载驱动、创建连接或关闭资源,只保留 SQL 和结果映射逻辑。
*/
public class UserService {
/**
* 当前 MiniSpring 的 @Autowired 按字段名查找 Bean,因此 XML 中的 id 必须是 jdbcTemplate。
*/
@Autowired
private JdbcTemplate jdbcTemplate;
public User getUserInfo(int userId) {
final String sql = "select id, name, birthday from users where id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{userId},
(resultSet, rowNum) -> mapUser(resultSet));
}
public List<User> getUsers(int minUserId) {
final String sql = "select id, name, birthday from users where id > ?";
return jdbcTemplate.query(sql, new Object[]{minUserId},
(resultSet, rowNum) -> mapUser(resultSet));
}
public int updateUserName(int userId, String name) {
final String sql = "update users set name = ? where id = ?";
return jdbcTemplate.update(sql, new Object[]{name, userId});
}
private User mapUser(java.sql.ResultSet resultSet)
throws java.sql.SQLException {
User user = new User();
user.setId(resultSet.getInt("id"));
user.setName(resultSet.getString("name"));
Date birthday = resultSet.getDate("birthday");
if (birthday != null) {
user.setBirthday(new java.util.Date(birthday.getTime()));
}
return user;
}
}
从这个用法往下看,JdbcTemplate 至少要解决三件事:
new Object[]{userId}里的参数怎么绑定到 SQL 的?上;ResultSet怎么一行一行变成User;- 每次查询时,连接怎么创建、复用和归还。
二、参数绑定交给 ArgumentPreparedStatementSetter
上一节参数绑定逻辑还在 JdbcTemplate 里。它能工作,但模板类会越来越杂。
java
private void bindParameters(PreparedStatement statement, Object[] args) throws SQLException {
Object[] parameters = args == null ? new Object[0] : args;
for (int i = 0; i < parameters.length; i++) {
Object value = parameters[i];
int parameterIndex = i + 1;
// JDBC 参数下标从 1 开始。常用类型显式设置,其余类型交给驱动的 setObject 处理。
if (value instanceof String) {
statement.setString(parameterIndex, (String) value);
} else if (value instanceof Integer) {
statement.setInt(parameterIndex, (Integer) value);
} else if (value instanceof Long) {
statement.setLong(parameterIndex, (Long) value);
} else if (value instanceof Boolean) {
statement.setBoolean(parameterIndex, (Boolean) value);
} else if (value instanceof java.sql.Date) {
statement.setDate(parameterIndex, (java.sql.Date) value);
} else if (value instanceof java.util.Date) {
statement.setTimestamp(parameterIndex, new Timestamp(((java.util.Date) value).getTime()));
} else {
statement.setObject(parameterIndex, value);
}
}
}
参数绑定本身是一件独立的事:拿到 Object[] args,按顺序调用 JDBC 的 setXxx(...) 方法。于是项目里把它抽成了 ArgumentPreparedStatementSetter。
java
package com.chenhai.jdbc.core;
import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
/**
* 按顺序给 PreparedStatement 绑定参数。
*
* 业务代码只需要传入 Object[],类型判断和 JDBC 下标从 1 开始这些细节
* 统一收口到这里。
*/
public class ArgumentPreparedStatementSetter {
private final Object[] args;
public ArgumentPreparedStatementSetter(Object[] args) {
this.args = args;
}
public void setValues(PreparedStatement statement) throws SQLException {
if (this.args == null) {
return;
}
for (int i = 0; i < this.args.length; i++) {
// Java 数组从 0 开始,JDBC 参数位置从 1 开始。
doSetValue(statement, i + 1, this.args[i]);
}
}
protected void doSetValue(PreparedStatement statement,
int parameterPosition,
Object argValue) throws SQLException {
if (argValue == null) {
statement.setNull(parameterPosition, Types.NULL);
} else if (argValue instanceof String) {
statement.setString(parameterPosition, (String) argValue);
} else if (argValue instanceof Integer) {
statement.setInt(parameterPosition, (Integer) argValue);
} else if (argValue instanceof Long) {
statement.setLong(parameterPosition, (Long) argValue);
} else if (argValue instanceof Boolean) {
statement.setBoolean(parameterPosition, (Boolean) argValue);
} else if (argValue instanceof Double) {
statement.setDouble(parameterPosition, (Double) argValue);
} else if (argValue instanceof Float) {
statement.setFloat(parameterPosition, (Float) argValue);
} else if (argValue instanceof BigDecimal) {
statement.setBigDecimal(parameterPosition, (BigDecimal) argValue);
} else if (argValue instanceof java.sql.Date) {
statement.setDate(parameterPosition, (java.sql.Date) argValue);
} else if (argValue instanceof Timestamp) {
statement.setTimestamp(parameterPosition, (Timestamp) argValue);
} else if (argValue instanceof java.util.Date) {
statement.setTimestamp(parameterPosition,
new Timestamp(((java.util.Date) argValue).getTime()));
} else {
statement.setObject(parameterPosition, argValue);
}
}
}
注意这里不是"自动拼 SQL"。PreparedStatement 的重点是把 SQL 结构和参数值分开:
java
select id, name from users where id = ?
参数值通过 setXxx(...) 交给 JDBC 驱动处理,而不是直接拼成字符串。
三、结果处理拆成两层
查询结果处理也可以拆开看。
text
ResultSetExtractor
关注整个 ResultSet 怎么变成最终结果
RowMapper
关注当前这一行怎么变成一个对象
RowMapperResultSetExtractor
负责循环 ResultSet,每一行调用 RowMapper
它们的关系可以画成这样:
text
JdbcTemplate
|
| executeQuery()
v
ResultSet
|
| ResultSetExtractor.extractData(rs)
v
RowMapperResultSetExtractor
|
| while (rs.next())
v
RowMapper.mapRow(rs, rowNum)
1. RowMapper:只管当前行
java
package com.chenhai.jdbc.core;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 把 ResultSet 当前行转换为领域对象。
*
* rowNum 从 0 开始,可用于需要区分首行和后续行的映射场景。
*/
@FunctionalInterface
public interface RowMapper<T> {
T mapRow(ResultSet resultSet, int rowNum) throws SQLException;
}
RowMapper 不调用 resultSet.next()。模板已经把游标移动到当前行了,它只负责读字段、组装对象。
2. ResultSetExtractor:处理整个结果集
java
package com.chenhai.jdbc.core;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 把整个 ResultSet 转换成目标结果。
*
* RowMapper 关注"一行怎么映射",ResultSetExtractor 关注"整个结果集怎么提取"。
*/
@FunctionalInterface
public interface ResultSetExtractor<T> {
T extractData(ResultSet resultSet) throws SQLException;
}
ResultSetExtractor 的粒度更大。它可以返回 List<User>,也可以返回统计值、分组结果、树形结构。
3. RowMapperResultSetExtractor:把两者接起来
java
package com.chenhai.jdbc.core;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* 把 RowMapper 适配成 ResultSetExtractor。
*
* 遍历 ResultSet 的固定流程只写一次,每一行的对象映射交给 RowMapper。
*/
public class RowMapperResultSetExtractor<T>
implements ResultSetExtractor<List<T>> {
private final RowMapper<T> rowMapper;
public RowMapperResultSetExtractor(RowMapper<T> rowMapper) {
if (rowMapper == null) {
throw new IllegalArgumentException("RowMapper must not be null");
}
this.rowMapper = rowMapper;
}
@Override
public List<T> extractData(ResultSet resultSet) throws SQLException {
List<T> results = new ArrayList<>();
int rowNum = 0;
while (resultSet.next()) {
results.add(this.rowMapper.mapRow(resultSet, rowNum++));
}
return results;
}
}
这样业务代码就不需要反复写:
java
while (resultSet.next()) {
...
}
它只写一行怎么转对象:
java
jdbcTemplate.query(sql, new Object[]{minUserId},
(resultSet, rowNum) -> mapUser(resultSet));
四、JdbcTemplate 现在怎样串起来
有了这些部件,JdbcTemplate 的代码反而更清楚。
1. 执行 PreparedStatement 回调
java
public <T> T execute(String sql,
Object[] args,
PreparedStatementCallback<T> callback) {
assertSql(sql);
if (callback == null) {
throw new IllegalArgumentException(
"PreparedStatementCallback must not be null");
}
try (Connection connection = obtainConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
// 参数绑定交给专门部件。
new ArgumentPreparedStatementSetter(args).setValues(statement);
// 执行 SQL 的变化点仍然交给回调。
return callback.doInPreparedStatement(statement);
} catch (SQLException e) {
throw new JdbcException("JDBC operation failed for SQL: " + sql, e);
}
}
2. 用 ResultSetExtractor 处理整个结果集
java
public <T> T query(String sql,
Object[] args,
ResultSetExtractor<T> resultSetExtractor) {
if (resultSetExtractor == null) {
throw new IllegalArgumentException(
"ResultSetExtractor must not be null");
}
return execute(sql, args, statement -> {
try (ResultSet resultSet = statement.executeQuery()) {
return resultSetExtractor.extractData(resultSet);
}
});
}
这里 ResultSet 由 JdbcTemplate 关闭。业务代码不要把 ResultSet 保存到方法外面继续用。
3. 用 RowMapper 做最常见的列表查询
java
public <T> List<T> query(String sql,
Object[] args,
RowMapper<T> rowMapper) {
if (rowMapper == null) {
throw new IllegalArgumentException("RowMapper must not be null");
}
return query(sql, args, new RowMapperResultSetExtractor<>(rowMapper));
}
这就是"组合"的味道:JdbcTemplate 不再自己写循环,而是把 RowMapper 包装成 ResultSetExtractor。
4. 单行查询和更新
java
public <T> T queryForObject(String sql,
Object[] args,
RowMapper<T> rowMapper) {
List<T> rows = query(sql, args, rowMapper);
return rows.isEmpty() ? null : rows.get(0);
}
public int update(String sql, Object[] args) {
return execute(sql, args, PreparedStatement::executeUpdate);
}
当前教学版的 queryForObject(...) 仍然是简化语义:没有结果返回 null,多行时取第一行。它还没有做 Spring JDBC 那种严格的单行校验。
现在 JdbcTemplate 的结构可以概括成:
text
JdbcTemplate
|
+-- ArgumentPreparedStatementSetter
| 负责参数绑定
|
+-- ResultSetExtractor
| 负责整个结果集提取
|
+-- RowMapperResultSetExtractor
负责把 RowMapper 应用到每一行
五、再把连接创建换成连接池
前面解决的是模板内部的职责拆分。接下来处理连接复用。
如果每次 SQL 都新建物理连接、用完就断开,成本很高。连接池的想法很朴素:
text
启动或第一次使用时创建几个物理连接
↓
getConnection() 借出一个空闲连接
↓
业务代码照常使用 Connection
↓
connection.close() 不断开数据库,只把连接还给池
这一节的连接池是教学版,不是生产级连接池。它用来说明 DataSource 为什么有价值:JdbcTemplate 只依赖 DataSource,所以底层从普通连接换成池化连接时,模板和业务层都不用改。
1. PooledConnection:逻辑连接
PooledConnection 实现了 java.sql.Connection。它内部包着一个真实物理连接,并用 active 标记当前是否被借出。
关键点是 close():
java
/**
* close() 不关闭真实数据库连接,只把连接归还给池。
*/
@Override
public void close() throws SQLException {
if (!this.active) {
return;
}
try {
if (!this.connection.getAutoCommit()) {
this.connection.rollback();
this.connection.setAutoCommit(true);
}
this.connection.clearWarnings();
} finally {
this.active = false;
if (this.closeCallback != null) {
this.closeCallback.run();
}
}
}
所以这里不是"逻辑删除连接"。更准确的说法是:
text
active = true
表示连接已借出
active = false
表示连接已归还,可以再次借出
真正关闭物理连接,只能由连接池销毁时调用:
java
public void closePhysicalConnection() throws SQLException {
this.active = false;
if (this.connection != null && !this.connection.isClosed()) {
this.connection.close();
}
}
其他 Connection 方法大多是委托给真实连接。为了防止连接归还后继续使用,内部会先检查状态:
java
private Connection target() throws SQLException {
if (this.connection == null) {
throw new SQLException("No physical connection available");
}
if (!this.active) {
throw new SQLException("Connection has been returned to the pool");
}
return this.connection;
}
2. PooledDataSource:池化 DataSource
PooledDataSource 仍然实现标准的 DataSource:
text
JdbcTemplate
|
v
javax.sql.DataSource
|
+-- PooledDataSource
|
+-- PooledConnection
|
+-- 真实数据库 Connection
初始化连接池:
java
public void initPool() throws SQLException {
synchronized (this.poolMonitor) {
if (this.initialized) {
return;
}
assertConfigured();
loadDriver();
this.connections = new ArrayList<>(this.initialSize);
for (int i = 0; i < this.initialSize; i++) {
PooledConnection pooledConnection =
new PooledConnection(
createPhysicalConnection(this.username, this.password),
false);
pooledConnection.setCloseCallback(this::notifyConnectionReturned);
this.connections.add(pooledConnection);
}
this.initialized = true;
}
}
借连接时,如果池里没有空闲连接,就等待一小段时间;超过 maxWaitMillis 还拿不到,就抛出异常:
java
@Override
public Connection getConnection() throws SQLException {
initPool();
long deadline = System.currentTimeMillis() + this.maxWaitMillis;
synchronized (this.poolMonitor) {
while (true) {
PooledConnection pooledConnection = getAvailableConnection();
if (pooledConnection != null) {
return pooledConnection;
}
long waitMillis = deadline - System.currentTimeMillis();
if (waitMillis <= 0) {
throw new SQLException("No available connection in pool within "
+ this.maxWaitMillis + " ms");
}
try {
this.poolMonitor.wait(Math.min(waitMillis, 30));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new SQLException(
"Interrupted while waiting for a pooled connection", e);
}
}
}
}
查找空闲连接:
java
private PooledConnection getAvailableConnection() throws SQLException {
for (PooledConnection pooledConnection : this.connections) {
if (!pooledConnection.isActive()) {
Connection physicalConnection = pooledConnection.getConnection();
if (physicalConnection == null || physicalConnection.isClosed()) {
pooledConnection.setConnection(
createPhysicalConnection(this.username, this.password));
}
pooledConnection.setActive(true);
return pooledConnection;
}
}
return null;
}
归还连接时会唤醒等待线程:
java
private void notifyConnectionReturned() {
synchronized (this.poolMonitor) {
this.poolMonitor.notifyAll();
}
}
关闭容器时,连接池才真正关闭物理连接:
java
@Override
public void close() throws SQLException {
synchronized (this.poolMonitor) {
if (this.connections != null) {
for (PooledConnection connection : this.connections) {
connection.closePhysicalConnection();
}
this.connections.clear();
this.connections = null;
this.initialized = false;
this.poolMonitor.notifyAll();
}
}
}
项目里的完整实现还处理了多个关闭异常的收集,这里只保留主线。
六、交给 IoC 容器装配
applicationContext.xml 中把原来的数据源换成 PooledDataSource:
xml
<bean id="dataSource" class="com.chenhai.jdbc.pool.PooledDataSource">
<property type="String"
name="driverClassName"
value="com.mysql.cj.jdbc.Driver"/>
<property type="String"
name="url"
value="jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:}?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true"/>
<property type="String" name="username" value="${MYSQL_USER:root}"/>
<property type="String" name="password" value="${MYSQL_PASSWORD:}"/>
<property type="int"
name="initialSize"
value="${MYSQL_POOL_INITIAL_SIZE:3}"/>
</bean>
<bean id="jdbcTemplate" class="com.chenhai.jdbc.core.JdbcTemplate">
<property type="javax.sql.DataSource"
name="dataSource"
ref="dataSource"/>
</bean>
<bean id="userService"
class="com.chenhai.jdbc.example.UserService"/>
因为 PooledDataSource 实现了 DataSource,所以 JdbcTemplate 只需要这一行:
java
Connection connection = dataSource.getConnection();
它不需要知道底层是普通连接,还是连接池里的逻辑连接。
容器关闭时,AbstractApplicationContext.close() 会销毁单例 Bean。如果单例实现了 AutoCloseable,就调用它的 close():
java
protected void destroySingletons() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
String[] singletonNames = beanFactory.getSingletonNames();
for (int i = singletonNames.length - 1; i >= 0; i--) {
Object singleton = beanFactory.getSingleton(singletonNames[i]);
if (singleton instanceof AutoCloseable) {
try {
((AutoCloseable) singleton).close();
} catch (Exception e) {
throw new IllegalStateException(
"Destroy bean failed: " + singletonNames[i], e);
}
}
}
}
这一步很重要。连接池创建的物理连接不能只借不还,也不能在 JVM 退出前一直没人管。既然连接池是容器创建的,销毁也应该由容器负责。
七、完整调用链
以 userService.getUsers(1) 为例,调用链现在是:
text
UserService.getUsers(1)
↓
准备 SQL、参数、RowMapper
↓
JdbcTemplate.query(sql, args, RowMapper)
↓
new RowMapperResultSetExtractor(rowMapper)
↓
JdbcTemplate.query(sql, args, ResultSetExtractor)
↓
JdbcTemplate.execute(sql, args, PreparedStatementCallback)
↓
PooledDataSource.getConnection()
↓
借出 PooledConnection
↓
ArgumentPreparedStatementSetter.setValues(...)
↓
PreparedStatement.executeQuery()
↓
RowMapperResultSetExtractor.extractData(...)
↓
循环调用 RowMapper.mapRow(...)
↓
try-with-resources 调用 connection.close()
↓
PooledConnection.close() 把连接归还给池
到这里,几个角色的职责就比较清楚了:
| 组件 | 职责 |
|---|---|
UserService |
提供 SQL、参数和行映射逻辑 |
JdbcTemplate |
管理 JDBC 固定流程和异常边界 |
ArgumentPreparedStatementSetter |
绑定 SQL 参数 |
RowMapper |
映射当前行 |
ResultSetExtractor |
提取整个结果集 |
PooledDataSource |
管理连接池 |
PooledConnection |
包装物理连接,让 close() 变成归还连接 |
八、当前实现边界
这一章的代码是教学实现,重点是看清抽象过程。它已经能说明核心思想,但还不是生产级 JDBC 框架。
当前还没有包含:
- 事务传播;
- 连接泄漏检测;
- 空闲连接保活;
- 最大连接数扩容;
- 连接有效性定期检查;
- Spring JDBC 那样细分的异常体系;
queryForObject(...)的严格单行结果校验。
对应的测试主要验证:
JdbcTemplate能通过ArgumentPreparedStatementSetter绑定参数;RowMapperResultSetExtractor能把多行结果转成列表;PooledConnection.close()会把连接归还给池;- 池耗尽时,
PooledDataSource会等待并在超时后抛出异常; - XML 能把
PooledDataSource注入到JdbcTemplate。
这一节的核心思路可以收成一句话:
JdbcTemplate继续负责总流程,但参数绑定、结果集提取和连接复用,都交给更专门的部件完成。