模板方法模式
一个 JDBC 工具类的演进
2019年,我接手了一个老项目,里面有几十个类似这样的方法:
public class UserDAO {
public User findById(Long id) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
User user = null;
try {
conn = dataSource.getConnection();
ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?");
ps.setLong(1, id);
rs = ps.executeQuery();
if (rs.next()) {
user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
// ... 10 个字段
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
close(rs, ps, conn);
}
return user;
}
public User findByName(String name) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
User user = null;
try {
conn = dataSource.getConnection();
ps = conn.prepareStatement("SELECT * FROM user WHERE name = ?"); // 只有 SQL 不同
ps.setString(1, name);
rs = ps.executeQuery();
if (rs.next()) {
user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
// ... 完全相同的 20 行
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
close(rs, ps, conn);
}
return user;
}
}
除了 SQL 和参数绑定不同,剩下 40 行完全重复。
模板方法模式解决的就是这个问题:定义一个算法的骨架,把某些步骤延迟到子类中实现。
二、模板方法核心结构🔴
2.1 标准写法
// 抽象模板类
abstract class JdbcTemplate {
// 模板方法:定义算法骨架
public final Object execute(String sql) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = getConnection();
ps = conn.prepareStatement(sql);
setParameters(ps);
rs = ps.executeQuery();
return extractData(rs); // 子类实现
} catch (Exception e) {
handleException(e);
} finally {
close(rs, ps, conn);
}
return null;
}
// 通用方法:子类不需要重写
protected Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
// 抽象方法:子类必须实现
protected abstract void setParameters(PreparedStatement ps) throws SQLException;
protected abstract Object extractData(ResultSet rs) throws SQLException;
// 钩子方法:子类可以选择重写
protected void handleException(Exception e) {
throw new RuntimeException(e);
}
// 通用关闭
private void close(ResultSet rs, Statement ps, Connection conn) {
// 关闭资源
}
}
2.2 具体实现
class UserDao extends JdbcTemplate {
@Override
protected void setParameters(PreparedStatement ps) throws SQLException {
// 不同的实现
}
@Override
protected Object extractData(ResultSet rs) throws SQLException {
User user = null;
if (rs.next()) {
user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
}
return user;
}
}
现在新增一个 DAO,只需要继承 JdbcTemplate,重写两个抽象方法:
class OrderDao extends JdbcTemplate {
@Override
protected void setParameters(PreparedStatement ps) throws SQLException {
// Order 特有的参数绑定
}
@Override
protected Object extractData(ResultSet rs) throws SQLException {
Order order = null;
if (rs.next()) {
order = new Order();
order.setId(rs.getLong("id"));
order.setAmount(rs.getDouble("amount"));
}
return order;
}
}
三、Spring 中的模板方法🟡
3.1 JdbcTemplate
Spring 的 JdbcTemplate 就是模板方法模式的经典应用:
// Spring JdbcTemplate 的使用方式
List<User> users = jdbcTemplate.query(
"SELECT * FROM user WHERE age > ?",
(rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name")),
18
);
query 方法内部就是模板方法:
- 获取连接
- 创建 PreparedStatement
- 设置参数
- 执行查询
- 处理结果集(回调函数)—— 变化点
- 关闭资源
3.2 RestTemplate
// RestTemplate 也有类似的模板结构
restTemplate.execute(url, HttpMethod.GET,
request -> { /* 设置请求头 */ },
response -> { /* 处理响应 */ });
3.3 TransactionTemplate
transactionTemplate.execute(status -> {
// 业务逻辑:这是"回调"部分
userDao.save(user);
orderDao.create(order);
return null;
});
四、模板方法 vs 回调🔴
4.1 回调的实现方式
Java 中,模板方法模式可以用回调(Callback)来实现:
// 使用 JdbcTemplate + 回调
List<User> users = jdbcTemplate.query(
"SELECT * FROM user WHERE age > ?",
new Object[]{18},
(rs, rowNum) -> {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
return user;
}
);
这里 RowMapper<User> 就是一个回调接口:
@FunctionalInterface
public interface RowMapper<T> {
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
4.2 模板方法 vs 回调
【架构权衡】
Spring 5 之后,大量模板方法模式被替换为回调模式。因为 Java 8 支持 Lambda 表达式,回调写法更简洁:
// 模板方法(继承)
class MyDao extends JdbcTemplate {
@Override
protected Object extractData(ResultSet rs) { ... }
}
// 回调(Lambda)
jdbcTemplate.query(sql, (rs, rowNum) -> { ... });
Spring 的设计选择:优先用回调,当回调写起来复杂时(如需要多个方法),才用模板方法。
五、钩子方法(Hook)🟡
5.1 什么是钩子方法
钩子方法是模板方法中可选重写的方法,它提供了"钩子"让子类可以在特定时机介入算法的执行。
abstract class AbstractClass {
// 模板方法
public final void templateMethod() {
step1();
if (hook()) { // 钩子:条件执行
step2();
}
step3();
}
protected void step1() { /* ... */ }
protected abstract void step2();
protected void step3() { /* ... */ }
// 钩子方法:默认返回 true,子类可以选择重写
protected boolean hook() {
return true;
}
}
5.2 实战:JUnit 中的钩子
JUnit 的 setUp() 和 tearDown() 就是典型的钩子方法:
class MyTest {
@BeforeEach
void setUp() { // 钩子:可选重写
// 每个测试方法前执行
}
@Test
void test1() { /* 测试逻辑 */ }
@Test
void test2() { /* 测试逻辑 */ }
@AfterEach
void tearDown() { // 钩子:可选重写
// 每个测试方法后执行
}
}
5.3 JDK 中的钩子:AQS
JDK 的 AbstractQueuedSynchronizer(AQS)大量使用模板方法和钩子:
public abstract class AbstractQueuedSynchronizer {
// 模板方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 钩子:尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
// 钩子方法:子类必须实现
protected abstract boolean tryAcquire(int arg);
// 钩子方法:可选重写
protected boolean tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
}
ReentrantLock 的公平锁和非公平锁就通过重写 tryAcquire 钩子实现:
// 非公平锁
class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// 公平锁
class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return fairTryAcquire(acquires);
}
}
六、生产避坑清单
6.1 模板方法 + 策略模式
// ❌ 错误:用模板方法实现不同算法
abstract class SortTemplate {
public void sort(List<?> list) { // 固定流程
compare(); // 抽象,不同实现
swap(); // 抽象,不同实现
}
}
// ✅ 正确:算法变化用策略,流程固定用模板
class Sorter {
private SortStrategy strategy; // 策略
public void sort(List<?> list) {
strategy.sort(list);
}
}
6.2 抽象方法的遗漏
// ❌ 危险:忘记重写抽象方法
class MyDao extends JdbcTemplate {
@Override
protected void setParameters(PreparedStatement ps) {
// 忘记实现
}
// extractData 没重写 → 编译错误(抽象方法)
}
编译器会强制要求重写所有抽象方法,但钩子方法容易被忽略。
6.3 final 关键字的使用
abstract class JdbcTemplate {
// ✅ 正确:模板方法用 final 防止被子类修改算法骨架
public final Object execute(String sql) {
// 固定流程,不允许修改
}
// ❌ 错误:把核心步骤也设为 final
protected final Connection getConnection() {
// 如果子类需要 mock 数据源,这就会很麻烦
}
}
⚠️
模板方法的 final 只应该修饰模板方法本身(算法骨架),不应该修饰可能被重写的步骤。过度使用 final 会降低子类的灵活性。
七、面试总结
7.1 核心追问
- "模板方法模式和策略模式有什么区别?" —— 策略模式替换整个算法,模板方法模式复用算法骨架
- "Spring 中哪些地方用到了模板方法模式?" —— JdbcTemplate、RestTemplate、TransactionTemplate
- "为什么 Spring 5 之后更推荐回调而不是继承?" —— Lambda 表达式更简洁,耦合更低
- "钩子方法和抽象方法的区别是什么?" —— 抽象方法必须重写,钩子方法可选重写
7.2 级别差异