模板方法模式:JdbcTemplate 为什么让你只写 SQL 不用管连接?
一句话结论(30s)
模板方法模式的本质是「父类固定流程骨架、子类只实现可变步骤」,体现好莱坞原则「别调我,我调你」。JdbcTemplate 把它用到极致,是因为它把「获取连接、创建 Statement、异常翻译、关闭资源」这些 100 个 DAO 方法里重复出现的样板代码抽成模板,只把纯 SQL 逻辑留给回调 doInStatement,让业务代码不写 try-catch-finally、不手动 close()。
核心原理(2min)
主流程:JdbcTemplate.execute(StatementCallback) 作为模板方法,先 DataSourceUtils.getConnection() 拿连接、createStatement() 建 Statement,然后回调 action.doInStatement(stmt) 执行你写的纯 SQL 逻辑,catch (SQLException) 时统一翻译成 Spring 运行时异常 DataAccessException,finally 里关闭 Statement 并释放连接。关键机制是「控制权颠倒」——父类 execute 控制整个流程的固定顺序并主动回调子类实现,子类从不主动调父类、只知道自己被调用时做什么;Spring 里 AbstractApplicationContext.refresh() 也是同样套路,14 个步骤固定顺序、子类只需重写个别步骤。
底层深入(5-10min)
模板方法的定义
父类定义算法骨架(final 方法固定步骤顺序),子类实现可变步骤:
abstract class DataExporter {
public final void export() { // final: 子类不能改流程
List<Data> data = fetchData(); // 步骤1: 子类实现
String formatted = format(data); // 步骤2: 子类实现
write(formatted); // 步骤3: 子类实现
}
abstract List<Data> fetchData();
abstract String format(List<Data> data);
abstract void write(String output);
}
💭 思考穿插:模板方法和策略模式有什么区别?——两者都用了多态,但方向相反:模板方法是「父类定骨架、子类填空」,通过继承复用流程;策略是「调用方持有一个接口、运行时换实现」,通过组合替换算法。一句话:模板方法用继承固化流程,策略用组合替换行为。所以模板方法的可变点是「流程里的某一步」,策略的可变点是「整个算法」。
JdbcTemplate:模板方法的巅峰应用
// org.springframework.jdbc.core.JdbcTemplate —— 真正的 execute 模板方法
private <T extends @Nullable Object> T execute(StatementCallback<T> action, boolean closeResources) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(obtainDataSource());
Statement stmt = null;
try {
stmt = con.createStatement();
applyStatementSettings(stmt);
T result = action.doInStatement(stmt);
handleWarnings(stmt);
return result;
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
if (stmt != null) {
handleWarnings(stmt, ex);
}
String sql = getSql(action);
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw translateException("StatementCallback", sql, ex);
}
finally {
if (closeResources) {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
}
@Override
public <T extends @Nullable Object> T execute(StatementCallback<T> action) throws DataAccessException {
return execute(action, true);
}
这一段是 JdbcTemplate 里真正的模板方法骨架:拿连接 → 建 Statement → applyStatementSettings 应用超时/抓取大小 → 回调 action.doInStatement(stmt) 执行可变 SQL → handleWarnings 处理警告,异常分支和 finally 都负责释放资源。唯一可变的一行是 action.doInStatement(stmt),其余全部复用。注意 catch 里先 closeStatement + releaseConnection、再抛 translateException,是为了在异常翻译器尚未初始化时也能及时归还连接、避免连接池死锁。
模板做了什么:获取连接、创建 Statement、异常翻译(将 JDBC 受检异常转为 Spring 运行时异常)、关闭资源。
回调(action.doInStatement)做了什么:纯 SQL 逻辑。不写 try-catch-finally,不手动 close()。
💭 思考穿插:
execute为什么能把 100 份样板代码合并成 1 份?——因为它把「顺序固定」的部分(拿连接 → 建 Statement → 异常翻译 → 关资源)留在模板里,只把「顺序可变」的那一行(doInStatement)作为洞留给回调。这就是控制权颠倒:不是业务代码去调 JDBC,而是模板反过来调你的回调。
query 也复用同一个 execute 模板
query(String sql, ResultSetExtractor) 没有重写一套连接管理,而是构造一个内部回调类,最后仍调用 execute(new QueryStatementCallback(), true):
@Override
public <T extends @Nullable Object> T query(String sql, ResultSetExtractor<T> rse) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
Assert.notNull(rse, "ResultSetExtractor must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL query [" + sql + "]");
}
// Callback to execute the query.
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
@Override
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
try {
rs = stmt.executeQuery(sql);
return rse.extractData(rs);
}
finally {
JdbcUtils.closeResultSet(rs);
}
}
@Override
public String getSql() {
return sql;
}
}
return execute(new QueryStatementCallback(), true);
}
这正是模板方法的最大收益:query、queryForStream、execute(String sql) 这些公开 API 各自只写「纯 SQL + 结果提取」,获取连接、异常翻译、关闭资源全部复用同一个 execute 模板。回调类同时实现 SqlProvider,让模板在异常时能通过 getSql() 拿到 SQL 拼进异常信息,异常上下文更完整。
💭 思考穿插:为什么
query不自己写一套连接管理,而是构造个内部回调再调execute?——因为「连接管理」是每个 SQL 操作都一样的固定骨架,重复写一遍就是回到「100 份样板代码」的老路。复用同一个execute,query就只剩「纯 SQL + 结果提取」这一件可变的事。
如果不用模板方法
每个 DAO 方法都要写:
Connection con = null;
Statement stmt = null;
try {
con = dataSource.getConnection();
stmt = con.createStatement();
stmt.executeQuery(sql);
} catch (SQLException e) {
// 翻译异常(每个方法重复)
throw new DataAccessException(...);
} finally {
if (stmt != null) try { stmt.close(); } catch (SQLException ignored) {}
if (con != null) try { con.close(); } catch (SQLException ignored) {}
}
100 个 DAO 方法 → 100 份重复的”获取连接-异常翻译-关闭”代码。模板方法把这 100 份合并为 1 份,回调只剩纯业务。
Spring 中的其他模板方法
AbstractApplicationContext.refresh() 是容器启动的模板方法——14 个步骤固定顺序(obtainFreshBeanFactory → invokeBeanFactoryPostProcessors → registerBeanPostProcessors → finishBeanFactoryInitialization 等),子类 AnnotationConfigApplicationContext 只需重写个别步骤。
好莱坞原则
“别调我,我调你”(Don’t call us, we’ll call you)。父类控制流程主动回调子类实现,子类从不主动调父类。控制权颠倒——子类不知道什么时候被调用,只知道被调用时做什么。
💭 思考穿插:「别调我,我调你」到底解决了什么?——解决的是「谁控制流程」的问题。传统写法是业务代码自己控制连接的生命周期,每个方法都要记住先开后关;好莱坞原则把控制权交给框架,业务代码只写「被调用时做什么」,流程的正确性由框架一次性保证。
总结
| 框架 | 模板方法 | 回调 |
|---|---|---|
| JdbcTemplate | execute(连接+异常+关闭) | doInStatement(SQL) |
| HibernateTemplate | execute(Session管理) | doInHibernate(HQL) |
| RestTemplate | execute(HTTP连接) | RequestCallback |
章末提问
Q1:模板方法模式和策略模式有什么区别?
结论先行:模板方法用继承固化流程骨架、子类填空;策略用组合替换整个算法。 因为:模板方法父类定死步骤顺序、只留可变步骤给子类重写,是「复用流程」;策略把算法抽成接口、调用方运行时换实现,是「替换行为」。前者继承、后者组合,可变粒度也不同——模板方法变的是流程里的某一步,策略变的是整个算法。
Q2:JdbcTemplate 里模板和回调分别做了什么?为什么这么分?
结论先行:模板做「获取连接、建 Statement、异常翻译、关资源」的固定骨架,回调只写纯 SQL。 因为:这四步在 100 个 DAO 方法里完全重复,属于不变部分;SQL 逻辑每个方法都不同,属于可变部分。把不变抽成模板、可变留给回调,100 份样板代码就合并成 1 份。
Q3:好莱坞原则是什么?在 JdbcTemplate 里怎么体现?
结论先行:好莱坞原则是「别调我,我调你」——框架控制流程、主动回调业务实现,业务代码不主动调框架。 因为:
execute模板方法主动回调action.doInStatement(stmt),业务代码从不自己拿连接、开 Statement、关资源,只写被调用时做什么。控制权颠倒,流程正确性由框架一次性保证。
Q4:模板方法的骨架方法为什么要 final?
结论先行:为了不让子类破坏流程的固定顺序。 因为:模板方法的灵魂是「步骤顺序不可变」,一旦子类能重写骨架方法,就可能打乱流程、跳过关键步骤(比如漏掉关资源)。
final锁死骨架,只开放 abstract 的可变步骤给子类实现。
Q5:query 为什么也复用 execute,而不是自己写一套连接管理?
结论先行:因为连接管理是所有 SQL 操作的固定骨架,复用能避免样板代码回归。 因为:如果
query自己再写一遍「拿连接-异常翻译-关资源」,就回到了 100 份重复代码的老路。复用execute,query只剩「纯 SQL + 结果提取」这一件可变的事。