模板方法模式中异常、事务和资源释放应该放在哪里?
简化版
异常、事务和资源释放通常应该由模板父类统一管理,因为它们属于流程骨架的一部分。子类负责具体业务步骤,不能随意吞异常、提前提交事务或忘记释放资源。父类可用 try-finally、事务边界、统一日志和失败回调保证流程完整;子类只抛出明确异常或返回受控结果。这样才能让所有子类共享一致的可靠性语义。
详细版
模板方法模式常用于批处理、导入导出、任务执行和框架生命周期,这些场景都绕不开异常和资源:
public final void execute(JobContext context) {
before(context);
try {
doExecute(context);
onSuccess(context);
} catch (Exception ex) {
onFailure(context, ex);
throw ex;
} finally {
cleanup(context);
}
}
父类统一决定成功、失败、清理和异常传播,子类只实现 doExecute。如果每个子类各自处理事务和释放资源,很容易出现行为不一致。
记忆钩子:模板父类管“必然发生的可靠性动作”,子类管“具体业务怎么做”。
完整版教学
面试提示:模板方法里的异常、事务和资源释放要一起看,因为它们共同决定流程骨架是否可靠。
| 关注点 | 模板方法职责 | 子类职责 |
|---|---|---|
| 异常处理 | 统一捕获、分类和转换 | 抛出有语义的业务异常 |
| 事务边界 | 决定整体提交或回滚范围 | 避免私自开启冲突事务 |
| 资源释放 | 保证 finally 或自动关闭 | 不绕过模板直接占用资源 |
一、异常处理为什么适合放在父类
如果每个子类自己写异常处理:
class CsvImport extends ImportTemplate {
protected void doImport() {
try {
...
} catch (Exception e) {
log.error(...);
}
}
}
有的子类吞异常,有的子类重抛,有的子类漏日志,整体行为会不一致。
父类统一处理能保证所有子类遵守同一失败语义。
二、资源释放要用 finally 保证
模板方法常见结构:
public final void process(File file) {
Resource resource = open(file);
try {
validate(resource);
doProcess(resource);
} finally {
close(resource);
}
}
无论子类成功还是失败,资源都能释放。资源打开和关闭通常属于父类流程骨架,不应交给子类自由处理。
三、事务边界通常也属于模板
如果每个子类自己决定事务:
子类 A 开事务
子类 B 不开事务
子类 C 中途提交
同一模板下的执行语义就乱了。更稳的是父类或外层服务定义事务边界,子类只执行业务步骤。
@Transactional
public final void importData(File file) {
List<Row> rows = parse(file);
save(rows);
}
不过要注意 Spring AOP 对 final 方法和自调用的限制,实际项目可能把事务放在外层 service。
四、子类不应该随意吞异常
子类吞异常会让父类误以为成功:
protected void doExecute() {
try {
remoteCall();
} catch (Exception ignored) {
}
}
这会导致 onSuccess 被调用、事务提交、状态标记成功。子类应抛出明确异常,让父类统一决策。
五、失败回调是受控扩展点
父类可以提供钩子:
protected void onFailure(JobContext context, Exception ex) {}
protected void onSuccess(JobContext context) {}
但这些钩子要有明确约束:不能吞掉主异常,不能做长耗时阻塞,不能改变事务一致性。必要时父类捕获钩子异常并记录。
六、外部副作用要考虑事务提交时机
如果模板方法在事务中执行,子类不要在事务提交前直接发送短信、MQ 或外部 HTTP 请求。否则主事务回滚时,外部副作用无法回滚。
可以让父类在成功后发布提交后事件,或者记录 outbox:
doExecute -> 保存业务数据
afterCommit -> 发送通知
这类回答能体现你理解模板方法和事务一致性的结合。
七、统一可靠性语义便于监控
父类统一记录:
jobName=CsvImport
status=FAILED
duration=2300ms
error=ParseException
cleanup=SUCCESS
所有子类都有一致日志和指标,线上排查会简单很多。
八、常见误区与追问
- 误区:异常处理应该由每个子类自己决定。 共同流程的失败语义应由父类统一管理。
- 误区:子类捕获异常不抛更稳。 这可能让父类误判成功,导致事务、状态和通知错误。
- 误区:资源释放交给子类更灵活。 资源生命周期属于流程骨架,应由父类 finally 兜底。
- 误区:模板方法加
@Transactional一定生效。 Spring 代理、自调用和 final 方法都可能影响事务增强。 - 追问:事务放父类还是外层 service? 看框架限制;原则是边界统一,不让子类各自随意提交。
- 追问:失败钩子异常怎么办? 父类应捕获并记录,避免覆盖主异常或破坏清理流程。
- 追问:外部通知放在哪里? 通常放事务提交后事件、outbox 或可靠消息里,不放在未提交事务中。
九、加强记忆
- 异常、事务、资源释放属于流程可靠性。
- 父类统一管理可靠性动作,子类只做业务变化。
- 资源释放用 finally 保证。
- 子类不要吞异常让父类误判成功。
- Spring 事务要注意代理和 final 限制。
- 外部副作用尽量放事务提交后。