模板方法模式

一个 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 方法内部就是模板方法:

  1. 获取连接
  2. 创建 PreparedStatement
  3. 设置参数
  4. 执行查询
  5. 处理结果集(回调函数)—— 变化点
  6. 关闭资源

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 回调

维度模板方法(继承)回调(委托)
实现方式继承抽象类,重写方法传入接口实现(Lambda)
代码耦合继承层次接口引用
灵活性较低(受继承限制)高(任何时候可传入不同实现)
可读性算法骨架清晰调用点代码简洁
Java 8+需要继承类支持 Lambda 表达式
Spring 推荐抽象类@Bean + Lambda

【架构权衡】 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 核心追问

  1. "模板方法模式和策略模式有什么区别?" —— 策略模式替换整个算法,模板方法模式复用算法骨架
  2. "Spring 中哪些地方用到了模板方法模式?" —— JdbcTemplate、RestTemplate、TransactionTemplate
  3. "为什么 Spring 5 之后更推荐回调而不是继承?" —— Lambda 表达式更简洁,耦合更低
  4. "钩子方法和抽象方法的区别是什么?" —— 抽象方法必须重写,钩子方法可选重写

7.2 级别差异

级别期望回答
P5能写出模板方法的基本结构,知道 Spring JdbcTemplate
P6能说出模板方法与回调的区别,能识别 AQS 中的模板方法
P7能从框架源码(JDK AQS、Spring)分析模板方法的应用,能对比继承与回调的权衡