模板方法模式:JdbcTemplate/RestTemplate的钩子设计与扩展点
模板方法模式:JdbcTemplate/RestTemplate的钩子设计与扩展点
适读人群:中高级Java开发者 | 阅读时长:约22分钟 | 模式类型:行为型
开篇故事
接触 Spring 框架之前,我写过大量"裸 JDBC"代码:
// 每次数据库操作都要重复这套流程
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
ps = conn.prepareStatement("SELECT * FROM orders WHERE id = ?");
ps.setString(1, orderId);
rs = ps.executeQuery();
if (rs.next()) {
order = mapRow(rs); // 这行是真正的业务逻辑
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
// 必须关闭,顺序还不能错
if (rs != null) try { rs.close(); } catch (SQLException e) {}
if (ps != null) try { ps.close(); } catch (SQLException e) {}
if (conn != null) try { conn.close(); } catch (SQLException e) {}
}这段代码中,真正的业务逻辑只有 mapRow(rs) 那一行,其余都是固定的样板代码。每个数据库操作方法都要把这套骨架重复一遍,烦不胜烦,而且容易漏关 ResultSet 导致资源泄漏。
用了 JdbcTemplate 之后,这一切样板代码都消失了:
// 只需要写业务逻辑
Order order = jdbcTemplate.queryForObject(
"SELECT * FROM orders WHERE id = ?",
(rs, rowNum) -> mapRow(rs), // 这就是钩子方法!
orderId
);JdbcTemplate 把"获取连接 → 创建语句 → 执行 → 处理结果 → 关闭资源"这个固定骨架管理起来,只留下"如何处理结果"这个变化点交给你来实现。这正是模板方法模式的精髓。
一、模式动机:固化骨架,开放扩展点
模板方法模式(Template Method Pattern)的核心:在父类中定义算法的骨架(模板方法),将某些步骤的实现延迟到子类,使得子类可以在不改变算法结构的情况下重定义算法的某些步骤。
两类方法:
- 模板方法(Template Method):定义算法骨架,通常是
final的,不允许子类覆写。 - 钩子方法(Hook Method):在算法骨架中的关键点被调用,子类可以(可选)覆写来影响算法行为;或者是抽象方法,子类必须实现。
二、模式结构
三、Spring 模板类的钩子设计分析
3.1 JdbcTemplate 的模板方法
JdbcTemplate.execute() 是核心模板方法,管理连接生命周期,只把"如何使用连接"这个关键点留给调用者:
// JdbcTemplate.execute() — 模板方法的典型实现
public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(obtainDataSource()); // 获取连接(固定步骤)
try {
Connection conToUse = createConnectionProxy(con);
return action.doInConnection(conToUse); // 调用钩子:让调用者决定怎么用连接
} catch (SQLException ex) {
String sql = getSql(action);
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw translateException("ConnectionCallback", sql, ex); // 异常转换(固定步骤)
} finally {
DataSourceUtils.releaseConnection(con, getDataSource()); // 释放连接(固定步骤)
}
}
// query() 方法内部也是模板方法
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
return execute(new QueryStatementCallback(), false); // 委托给execute
}
// 三个函数式钩子接口(Callback)
@FunctionalInterface
public interface ConnectionCallback<T> {
T doInConnection(Connection con) throws SQLException, DataAccessException;
}@FunctionalInterface
public interface StatementCallback<T> {
T doInStatement(Statement stmt) throws SQLException, DataAccessException;
}@FunctionalInterface
public interface PreparedStatementCallback<T> {
T doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException;
}3.2 RestTemplate 的拦截器钩子
RestTemplate 通过拦截器机制提供扩展点:
// RestTemplate.doExecute() — 模板方法
protected <T> T doExecute(URI url, @Nullable String uriTemplate, @Nullable HttpMethod method,
RequestCallback requestCallback, ResponseExtractor<T> responseExtractor)
throws RestClientException {
ClientHttpResponse response = null;
try {
ClientHttpRequest request = createRequest(url, method); // 创建请求(固定步骤)
if (requestCallback != null) {
requestCallback.doWithRequest(request); // 钩子A:允许调用者设置请求头等
}
response = request.execute(); // 发送请求(固定步骤)
handleResponse(url, method, response); // 错误处理(固定步骤)
return (responseExtractor != null ? responseExtractor.extractData(response) : null); // 钩子B:允许调用者自定义响应解析
} catch (IOException ex) {
// 异常处理(固定步骤)
throw new ResourceAccessException("I/O error on " + method.name() + " request for \"" + url + "\": " + ex.getMessage(), ex);
} finally {
if (response != null) {
response.close(); // 关闭响应(固定步骤)
}
}
}四、生产级代码实现
4.1 批量数据处理模板
/**
* 批量数据处理模板(抽象类)
* 定义批量处理的骨架:分批读取 → 处理 → 写入 → 统计
*/
@Slf4j
public abstract class BatchProcessTemplate<T, R> {
private static final int DEFAULT_BATCH_SIZE = 1000;
protected int batchSize = DEFAULT_BATCH_SIZE;
/**
* 模板方法:定义批量处理的完整骨架
* 不允许子类覆写(final)
*/
public final BatchProcessResult execute(BatchProcessContext context) {
log.info("Starting batch process: {}", getTaskName());
long startTime = System.currentTimeMillis();
BatchProcessResult result = new BatchProcessResult();
result.setTaskName(getTaskName());
result.setStartTime(LocalDateTime.now());
// 步骤1:初始化(钩子,子类可选实现)
beforeProcess(context);
int pageNum = 0;
boolean hasMore = true;
try {
while (hasMore) {
// 步骤2:分批读取数据(抽象方法,子类必须实现)
List<T> batch = fetchBatch(pageNum, batchSize, context);
if (batch == null || batch.isEmpty()) {
hasMore = false;
break;
}
log.debug("Processing batch {}, size: {}", pageNum, batch.size());
// 步骤3:过滤(钩子,子类可选覆写)
List<T> filtered = filterBatch(batch, context);
// 步骤4:处理每批数据(抽象方法,子类必须实现)
List<R> processedItems = new ArrayList<>();
int successCount = 0, failCount = 0;
for (T item : filtered) {
try {
R processed = processItem(item, context);
if (processed != null) {
processedItems.add(processed);
successCount++;
}
} catch (Exception e) {
failCount++;
log.warn("Failed to process item {}: {}", item, e.getMessage());
// 步骤5:单项处理失败的处理(钩子,子类可选覆写)
handleItemError(item, e, context);
}
}
// 步骤6:批量写入(抽象方法,子类必须实现)
if (!processedItems.isEmpty()) {
writeBatch(processedItems, context);
}
// 步骤7:每批结束后的处理(钩子,子类可选实现)
afterBatch(pageNum, batch.size(), successCount, failCount, context);
result.addBatchStats(batch.size(), successCount, failCount);
if (batch.size() < batchSize) {
hasMore = false; // 读取到最后一批
}
pageNum++;
}
} catch (Exception e) {
log.error("Batch process failed at page {}: {}", pageNum, e.getMessage(), e);
result.setFailed(true);
result.setErrorMessage(e.getMessage());
// 步骤8:全局错误处理(钩子,子类可选实现)
onError(e, context);
} finally {
// 步骤9:清理资源(钩子,子类可选实现)
afterProcess(context);
}
long elapsed = System.currentTimeMillis() - startTime;
result.setEndTime(LocalDateTime.now());
result.setElapsedMs(elapsed);
log.info("Batch process completed: {}, total: {}, success: {}, fail: {}, elapsed: {}ms",
getTaskName(), result.getTotalCount(), result.getSuccessCount(),
result.getFailCount(), elapsed);
return result;
}
// ====== 抽象方法:子类必须实现 ======
/**
* 任务名称
*/
protected abstract String getTaskName();
/**
* 分批读取数据
*/
protected abstract List<T> fetchBatch(int pageNum, int pageSize, BatchProcessContext context);
/**
* 处理单个数据项
*/
protected abstract R processItem(T item, BatchProcessContext context) throws Exception;
/**
* 批量写入处理结果
*/
protected abstract void writeBatch(List<R> items, BatchProcessContext context);
// ====== 钩子方法:子类可选实现 ======
protected void beforeProcess(BatchProcessContext context) { /* 默认空实现 */ }
protected List<T> filterBatch(List<T> batch, BatchProcessContext context) { return batch; }
protected void handleItemError(T item, Exception e, BatchProcessContext context) { /* 默认忽略 */ }
protected void afterBatch(int pageNum, int total, int success, int fail, BatchProcessContext context) { /* 默认空实现 */ }
protected void onError(Exception e, BatchProcessContext context) { /* 默认空实现 */ }
protected void afterProcess(BatchProcessContext context) { /* 默认空实现 */ }
}
/**
* 用户积分过期处理任务(ConcreteClass A)
*/
@Component
@Slf4j
public class ExpiredPointsCleanupTask extends BatchProcessTemplate<UserPoints, Void> {
@Autowired
private UserPointsRepository pointsRepository;
@Autowired
private PointsExpiredEventPublisher eventPublisher;
public ExpiredPointsCleanupTask() {
this.batchSize = 500; // 覆写批次大小
}
@Override
protected String getTaskName() {
return "EXPIRED_POINTS_CLEANUP";
}
@Override
protected List<UserPoints> fetchBatch(int pageNum, int pageSize, BatchProcessContext context) {
LocalDate expiryDate = (LocalDate) context.getParam("expiryDate");
return pointsRepository.findExpiredPoints(expiryDate, PageRequest.of(pageNum, pageSize));
}
@Override
protected Void processItem(UserPoints points, BatchProcessContext context) throws Exception {
points.setStatus(PointsStatus.EXPIRED);
points.setExpiredAt(LocalDateTime.now());
return null;
}
@Override
protected void writeBatch(List<Void> items, BatchProcessContext context) {
// items为null的情况已在父类过滤,这里直接批量保存
}
// 覆写钩子:每批处理完后发送积分过期事件
@Override
protected void afterBatch(int pageNum, int total, int success, int fail,
BatchProcessContext context) {
log.info("Points cleanup batch {} done: success={}, fail={}", pageNum, success, fail);
// 发布事件,通知用户积分即将过期
eventPublisher.publishBatchExpiredEvent(pageNum, success);
}
// 覆写钩子:单项处理失败时记录错误
@Override
protected void handleItemError(UserPoints item, Exception e, BatchProcessContext context) {
log.error("Failed to expire points for user {}: {}", item.getUserId(), e.getMessage());
context.incrementErrorCount();
}
}
/**
* 报表数据同步任务(ConcreteClass B)
*/
@Component
@Slf4j
public class ReportDataSyncTask extends BatchProcessTemplate<Order, ReportRecord> {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ReportRepository reportRepository;
@Override
protected String getTaskName() { return "REPORT_DATA_SYNC"; }
@Override
protected List<Order> fetchBatch(int pageNum, int pageSize, BatchProcessContext context) {
LocalDate syncDate = (LocalDate) context.getParam("syncDate");
return orderRepository.findByDate(syncDate, PageRequest.of(pageNum, pageSize));
}
@Override
protected ReportRecord processItem(Order order, BatchProcessContext context) {
// 将订单转换为报表记录
return ReportRecord.builder()
.orderId(order.getId())
.userId(order.getUserId())
.amount(order.getFinalAmount())
.orderDate(order.getCreatedAt().toLocalDate())
.region(order.getShippingRegion())
.channel(order.getOrderChannel())
.build();
}
@Override
protected void writeBatch(List<ReportRecord> items, BatchProcessContext context) {
reportRepository.batchInsert(items); // 批量插入报表数据库
}
// 覆写过滤钩子:只处理已完成的订单
@Override
protected List<Order> filterBatch(List<Order> batch, BatchProcessContext context) {
return batch.stream()
.filter(order -> order.getStatus() == OrderStatus.COMPLETED)
.collect(Collectors.toList());
}
// 覆写初始化钩子:开始前清除当天已有的报表数据
@Override
protected void beforeProcess(BatchProcessContext context) {
LocalDate syncDate = (LocalDate) context.getParam("syncDate");
log.info("Clearing existing report data for date: {}", syncDate);
reportRepository.deleteByDate(syncDate);
}
}五、踩坑实录
坑一:模板方法不加 final 被子类覆写
有同事在继承我的批量处理模板时,覆写了 execute() 方法(以为可以自定义执行流程),结果把整个批处理骨架绕过去了,资源清理和统计都失效了。
教训:模板方法必须加 final,明确告知使用者"这个方法不应该被覆写"。
坑二:钩子方法中的异常处理
父类的钩子方法如果没有处理异常,子类实现中抛出异常可能会导致模板方法中的资源没有被正确释放(因为异常发生在 finally 之前的步骤)。
模板方法中的每个钩子调用都应该有适当的 try-catch,保证即使某个钩子失败,后续的清理步骤(afterProcess)仍然能够执行。
坑三:JdbcTemplate 的 RowMapper 回调异常
在使用 JdbcTemplate.query() 时,如果 RowMapper 内部抛出了 RuntimeException,JdbcTemplate 会正常关闭 ResultSet 和连接,但异常会向上传播。有一次我们的 RowMapper 里做了 JSON 反序列化,因为数据库中有一条脏数据(JSON 格式不合法),导致整个查询失败,而不是跳过那条数据。
解决方案:RowMapper 中对可能出错的操作加 try-catch,记录日志后返回 null,然后在外层过滤 null。
六、总结
模板方法模式是框架设计的基石:
- 固化骨架:把稳定的流程(获取连接、释放资源、异常处理)放在父类模板方法中。
- 开放扩展:把变化的点(如何使用连接、如何处理结果、如何映射对象)留作抽象方法或钩子方法。
JdbcTemplate、RestTemplate、AbstractBatchProcessTemplate 等都是这个模式的典型实践。
核心设计原则:Hollywood Principle——"别调用我们,我们会调用你"。框架(父类)控制整体流程,在合适的时机调用业务代码(子类/回调),业务代码不需要关心框架级别的资源管理。
