← 返回题目列表

模板方法模式中异常、事务和资源释放应该放在哪里?

高频 困难 第 15 / 26 题 更新于 2026/08/02
模板方法模式异常处理事务资源释放

简化版

异常、事务和资源释放通常应该由模板父类统一管理,因为它们属于流程骨架的一部分。子类负责具体业务步骤,不能随意吞异常、提前提交事务或忘记释放资源。父类可用 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 或可靠消息里,不放在未提交事务中。

九、加强记忆

  1. 异常、事务、资源释放属于流程可靠性。
  2. 父类统一管理可靠性动作,子类只做业务变化。
  3. 资源释放用 finally 保证。
  4. 子类不要吞异常让父类误判成功。
  5. Spring 事务要注意代理和 final 限制。
  6. 外部副作用尽量放事务提交后。