大家好,我是折腾派程序员。
如果让我说 Spring 源码里出现频率最高的设计模式是哪个,代理算一个,观察者算一个,但模板方法可能才是真正的第一——JdbcTemplate、RedisTemplate、RestTemplate、AbstractApplicationContext、AbstractBeanFactory,随手翻几个核心类都是它的影子。
但这个模式在面试题里的出镜率,远不如单例和代理。很多人对它的理解停留在"父类定骨架子类填实现"这一句话,细节上经不住追问:钩子方法是什么?和策略模式到底有什么本质区别?Spring 里具体是怎么用的?
今天把这些都说清楚。
从一段真实的糟糕代码说起
这是一个很典型的场景:系统需要支持报表多格式导出,同一份数据,用户可以下载 Excel、CSV、PDF 三种格式。
第一个同学接了这个需求,先做 Excel:
publicclassExcelExportService{
publicvoidexport(Long reportId){
// 第一步:查询数据
ReportData data = reportDao.findById(reportId);
// 第二步:权限校验
permissionService.checkExportPermission(data);
// 第三步:数据预处理(金额保留两位、日期格式化)
dataProcessor.preprocess(data);
// 第四步:写入 Excel 格式(核心差异点)
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("报表");
fillSheet(sheet, data);
workbook.write(outputStream);
// 第五步:记录导出日志
exportLogDao.save(new ExportLog(reportId, "EXCEL", LocalDateTime.now()));
}
}
然后 CSV 需求来了,新建一个 CsvExportService,把上面的代码复制一遍,改改第四步的写入逻辑。PDF 来了,再来一遍。
结果是三个类,四个步骤是完全一样的,只有第四步不同。
三个月后产品说权限校验规则要改,你需要找到三个地方分别修改。改了两个漏了一个,上线发现 CSV 导出没有走新的权限逻辑,这就是一个线上 Bug。更难受的是,这种错误在测试阶段很难发现,因为功能本身是正常的,只是权限判断逻辑跑的是旧的。
模板方法:把不变的放进父类,把变的留给子类
模板方法要解决的问题就是这个:一批对象的处理流程高度相似,只有其中某几个步骤的具体实现不同。
核心结构有三个东西:
模板方法(Template Method):定义算法的整体流程,用 final 锁死,子类无法改变顺序。
抽象步骤(Abstract Methods):流程里那些"变化点",强制子类实现,父类不知道也不应该知道具体怎么做。
钩子方法(Hook Methods):流程里那些"可选扩展点",父类给一个空实现或默认实现,子类可以选择覆盖,也可以不管。
先看基础结构:
// 抽象基类:定义导出流程的骨架
publicabstractclassReportExporter{
// 模板方法:final 锁死,流程顺序不允许子类改变
publicfinalvoidexport(Long reportId){
ReportData data = reportDao.findById(reportId);
permissionService.checkExportPermission(data);
dataProcessor.preprocess(data);
// 唯一的变化点:交给子类实现
doExport(data);
exportLogDao.save(new ExportLog(reportId, getFormatName(), LocalDateTime.now()));
}
// 抽象步骤1:怎么写入,子类决定
protectedabstractvoiddoExport(ReportData data);
// 抽象步骤2:叫什么格式名,子类决定(写日志用)
protectedabstract String getFormatName();
}
子类只需要关心自己那部分,其余的一行不用写:
@Service
publicclassExcelExporterextendsReportExporter{
@Override
protectedvoiddoExport(ReportData data){
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("报表");
// Excel 特有的写入逻辑
fillSheet(sheet, data);
workbook.write(outputStream);
}
@Override
protected String getFormatName(){
return"EXCEL";
}
}
@Service
publicclassCsvExporterextendsReportExporter{
@Override
protectedvoiddoExport(ReportData data){
CsvWriter writer = new CsvWriter(outputStream, StandardCharsets.UTF_8);
// CSV 特有的写入逻辑
for (ReportRow row : data.getRows()) {
writer.writeRecord(row.toArray());
}
writer.close();
}
@Override
protected String getFormatName(){
return"CSV";
}
}
权限校验逻辑改了?ReportExporter 里改一次,所有格式的导出全部生效。新增 PDF 格式?加一个子类,实现两个方法,其余都是现成的。
钩子方法:给骨架留弹性
abstract 方法是强制子类实现的,但有些步骤是"可选的"——多数情况用默认行为,少数情况需要定制。这时候用钩子方法,父类给一个空实现或者返回默认值,子类可以选择性覆盖。
还是导出的场景,比如有些格式导出时需要加水印(比如"内部资料,禁止外传"),有些不需要:
publicabstractclassReportExporter{
publicfinalvoidexport(Long reportId){
ReportData data = reportDao.findById(reportId);
permissionService.checkExportPermission(data);
dataProcessor.preprocess(data);
// 钩子方法控制流程分支
if (needWatermark()) {
watermarkService.apply(data);
}
doExport(data);
exportLogDao.save(new ExportLog(reportId, getFormatName(), LocalDateTime.now()));
}
protectedabstractvoiddoExport(ReportData data);
protectedabstract String getFormatName();
// 钩子方法:默认不加水印,子类可以覆盖
protectedbooleanneedWatermark(){
returnfalse;
}
}
// Excel 不需要水印,什么都不用写
publicclassExcelExporterextendsReportExporter{
// needWatermark() 用父类默认的 false
// ...
}
// PDF 需要水印
publicclassPdfExporterextendsReportExporter{
@Override
protectedbooleanneedWatermark(){
returntrue; // 覆盖钩子,开启水印
}
// ...
}
钩子方法让骨架有了弹性。子类不用改变流程结构,只需要覆盖钩子,就能影响某个分支的走向。
这是很多人不知道的细节:模板方法模式里不只有 abstract 方法,钩子方法同样重要,Spring 源码里用得很多。
Spring 源码里的模板方法,随手就是
JdbcTemplate:把 JDBC 的套路流程封死
不用框架手写 JDBC,固定要做的事是这些:
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection(); // 获取连接
pstmt = conn.prepareStatement(sql); // 创建语句
setParameters(pstmt, params); // 设置参数 ← 变化点
rs = pstmt.executeQuery();
return mapResult(rs); // 处理结果 ← 变化点
} catch (SQLException e) {
// 异常处理
} finally {
// 关闭 rs, pstmt, conn(顺序不能乱,每个关闭都可能抛异常)
}
整段代码里真正因业务而不同的只有两个地方:SQL 参数怎么设置、结果集怎么转成对象。剩下的全是固定的、繁琐的、容易出错的样板代码。
JdbcTemplate 就是把这个骨架封装进去,把两个变化点暴露出来:
// 你只需要告诉它:怎么把结果集里的一行映射成你的对象
List<User> users = jdbcTemplate.query(
"SELECT * FROM users WHERE age > ?",
new Object[]{18},
(rs, rowNum) -> { // ← 这个 lambda 就是在实现抽象步骤
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
return user;
}
);
你传进去的那个 RowMapper lambda,就是模板骨架里留出来的"抽象步骤"。获取连接、管理事务、关闭资源、处理 SQLException 全是框架干的,你一行也不用写。
AbstractApplicationContext.refresh():Spring 容器启动的总指挥
这是 Spring 里最核心的一个方法,每次容器启动必走,简化版如下:
publicabstractclassAbstractApplicationContext{
@Override
publicvoidrefresh()throws BeansException {
// 12 个步骤,顺序固定,这就是模板方法
prepareRefresh();
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // ← 抽象方法
prepareBeanFactory(beanFactory);
postProcessBeanFactory(beanFactory); // ← 钩子方法
invokeBeanFactoryPostProcessors(beanFactory);
registerBeanPostProcessors(beanFactory);
initMessageSource();
initApplicationEventMulticaster();
onRefresh(); // ← 钩子方法,非常关键
registerListeners();
finishBeanFactoryInitialization(beanFactory);
finishRefresh();
}
// 抽象方法:如何获取/刷新 BeanFactory,不同子类实现不同
protectedabstract ConfigurableListableBeanFactory obtainFreshBeanFactory();
// 钩子方法:默认空实现,子类可以扩展
protectedvoidonRefresh()throws BeansException {}
}
两个地方值得特别注意:
obtainFreshBeanFactory() 是抽象方法——ClassPathXmlApplicationContext 的实现是解析 XML 配置文件,AnnotationConfigApplicationContext 的实现是扫描注解。两种配置方式,容器启动的 12 个步骤完全一样,只有这一步的具体实现不同。
onRefresh() 是钩子方法——AbstractApplicationContext 里这个方法是空的。SpringBoot 的 ServletWebServerApplicationContext 覆盖了它,在这一步启动内嵌 Tomcat。就这一个钩子,让 Spring 容器和 Web 服务器的启动顺序被无缝串联在一起。
这就是 Spring 能同时支持 XML 配置和注解配置、能既跑普通 Java 应用又跑 Web 应用,但核心启动流程保持统一的底层原因。
和策略模式的区别,这个问题必须想清楚
这两个模式的表面目标很像,都是处理"变化的部分",面试里很容易被拿来对比。
| 实现方式 | ||
| 变化粒度 | ||
| 扩展方式 | ||
| 运行时替换 | ||
| 代码耦合 |
选哪个,看场景:
流程步骤固定,只有某几个环节的具体实现不同 → 模板方法。就像上面的报表导出,查询数据 → 权限校验 → 预处理 → 导出 → 记日志,这个顺序是业务固定的,不允许子类随便改。
整个处理逻辑都可能不同,没有固定骨架 → 策略模式。比如不同促销活动的计算逻辑:满减、折扣、秒杀,三种算法从头到尾完全独立,没有共同的"骨架"可以提取。
还有一个角度:谁决定用哪个实现?
模板方法是父类在骨架里调用子类的方法,是自上而下的"父类驱动"。策略模式是上下文在运行时持有一个策略对象,是"客户端注入"。前者更适合流程编排,后者更适合算法切换。
《设计模式》那本书里有一句话:"优先使用组合而非继承。"策略模式是组合,可以运行时替换,理论上更灵活。但灵活性不是免费的——策略模式需要额外定义接口和上下文类,代码量更多。模板方法在"流程固定、细节可变"这个场景里,代码量更少,逻辑更直观。不存在哪个更好,只有哪个更合适。
总结
模板方法的本质只有一句话:把不变的放进父类,把变的留给子类。
这不只是一个设计模式,而是面向对象里"抽象"最直接的体现。你在写一批流程相似的类时,都应该问自己:这些类里,哪些步骤是固定的,哪些步骤是变化的?固定的提到父类,变化的留给子类——模板方法就自然用对了。
最后说一句:看 Spring 源码时,遇到带 Abstract 前缀的类,大概率就是模板方法模式。带 Template 后缀的类(JdbcTemplate、RedisTemplate),名字里都明确告诉你了。以后看到这些类,知道从这个角度去理解它们的设计意图,代码会清晰很多。
👇 聊几个问题:
你的项目里有没有用过模板方法?还是遇到类似场景时直接选择了策略模式?两者各有什么感受,评论区交流。 AbstractApplicationContext.refresh()那 12 个步骤,你之前了解多少?这是读 Spring 源码必须过的一关,有兴趣可以留言,我后面单独写一篇 Spring 容器启动流程的深度解析。设计模式系列到这里就完整了。后续想看什么方向?Spring 源码深度解读、JVM 调优实战、分布式系统设计,还是 AI 工程落地?留言告诉我,票数最多的我优先写。点个在看 👍
折腾派程序员:专注 Java 后端、AI 工程、架构设计。 更多干货,关注公众号持续更新 ↓
夜雨聆风