SpringBoot使用设计模式一模板方法模式
一、前言
在日常开发中,我们经常会遇到这样的场景:多个业务流程包含相同的核心步骤,仅在部分细节逻辑上存在差异。例如:数据导入功能(Excel导入、CSV导入)、支付流程(微信支付、支付宝支付)、接口调用流程(参数校验、业务处理、结果封装)等。
如果每个业务流程都单独实现完整逻辑,会导致大量重复代码,且后续修改公共步骤时需要改动所有相关类,维护成本极高。例如:
public class ExcelDataImporter {
public void importData(String filePath) {
// 公共步骤1:校验文件合法性
System.out.println("校验Excel文件合法性:" + filePath);
if (!filePath.endsWith(".xlsx")) {
throw new IllegalArgumentException("文件格式错误");
}
// 特有步骤:解析Excel文件(不同导入方式的核心差异)
System.out.println("解析Excel文件数据");
List<String> data = parseExcel(filePath);
// 公共步骤2:数据校验
System.out.println("校验导入数据完整性");
validateData(data);
// 公共步骤3:数据入库
System.out.println("将Excel数据写入数据库");
saveData(data);
// 公共步骤4:记录操作日志
System.out.println("Excel导入完成,记录操作日志");
}
private List<String> parseExcel(String filePath) {
// Excel解析逻辑
return Collections.singletonList("Excel解析的数据");
}
private void validateData(List<String> data) {
// 数据校验逻辑
}
private void saveData(List<String> data) {
// 数据入库逻辑
}
}
public class CsvDataImporter {
public void importData(String filePath) {
// 公共步骤1:校验文件合法性(重复代码)
System.out.println("校验CSV文件合法性:" + filePath);
if (!filePath.endsWith(".csv")) {
throw new IllegalArgumentException("文件格式错误");
}
// 特有步骤:解析CSV文件(不同导入方式的核心差异)
System.out.println("解析CSV文件数据");
List<String> data = parseCsv(filePath);
// 公共步骤2:数据校验(重复代码)
System.out.println("校验导入数据完整性");
validateData(data);
// 公共步骤3:数据入库(重复代码)
System.out.println("将CSV数据写入数据库");
saveData(data);
// 公共步骤4:记录操作日志(重复代码)
System.out.println("CSV导入完成,记录操作日志");
}
private List<String> parseCsv(String filePath) {
// CSV解析逻辑
return Collections.singletonList("CSV解析的数据");
}
private void validateData(List<String> data) {
// 数据校验逻辑(重复代码)
}
private void saveData(List<String> data) {
// 数据入库逻辑(重复代码)
}
}
这种写法中,文件校验、数据校验、入库、日志记录等公共步骤在每个实现类中重复出现,代码冗余严重。针对这种场景,我们可以使用模板方法模式来解决,将公共逻辑抽象到父类,子类仅实现差异化步骤。
二、模板方法模式
模板方法模式是一种行为型设计模式,它定义一个操作中的算法骨架,将算法的一些步骤延迟到子类中实现,使得子类可以在不改变算法结构的情况下重定义该算法的某些特定步骤。
核心概念
模板方法模式包含两个核心角色:
- 抽象模板(Abstract Template):定义算法的骨架流程(模板方法),包含公共步骤的实现和抽象的差异化步骤(钩子方法/抽象方法);
- 具体实现(Concrete Implementation):继承抽象模板,实现父类中定义的抽象方法,完成差异化逻辑的实现。
工作原理
- 抽象模板类定义算法的整体流程(模板方法),该方法通常被声明为
final,防止子类修改流程结构; - 模板方法中调用一系列步骤方法,包括:
- 公共方法(Concrete Method):所有子类共享的固定逻辑,父类中直接实现;
- 抽象方法(Abstract Method):子类必须实现的差异化逻辑,父类仅声明;
- 钩子方法(Hook Method):可选的扩展点,父类提供默认实现,子类可根据需要重写。
- 客户端调用抽象模板的模板方法,由模板方法按预定流程调用各步骤,最终完成业务逻辑。
适用场景
- 多个业务流程具有相同的核心步骤,仅部分细节不同;
- 希望统一算法结构,避免子类修改流程逻辑;
- 需提取公共代码,减少重复冗余。
优点
- 封装不变部分,提取公共逻辑,减少代码冗余;
- 固定算法骨架,确保流程一致性,便于维护;
- 子类仅需关注差异化逻辑,降低开发复杂度;
- 符合开闭原则,扩展新业务时仅需新增子类,无需修改父类模板。
缺点
- 抽象模板类与子类存在强耦合,父类修改流程会影响所有子类;
- 若算法步骤过多,抽象模板类会变得复杂,不易维护;
- 子类数量可能增多,增加系统复杂度(可结合工厂模式优化)。
三、实现案例
下面以Spring Boot项目为例,通过模板方法模式实现多格式数据导入功能(Excel导入、CSV导入),统一导入流程,分离差异化解析逻辑。
3.1 定义抽象模板类(Abstract Template)
创建数据导入抽象类,定义导入流程的模板方法和步骤:
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 数据导入抽象模板类:定义导入流程的骨架
*/
@Slf4j
@Component
public abstract class AbstractDataImporter {
/**
* 模板方法:定义数据导入的完整流程(不可修改)
* @param filePath 文件路径
*/
public final void importData(String filePath) {
try {
// 步骤1:校验文件合法性(公共方法)
validateFile(filePath);
// 步骤2:解析文件(抽象方法,子类实现)
List<String> data = parseFile(filePath);
// 步骤3:数据校验(公共方法)
validateData(data);
// 步骤4:数据入库(公共方法)
saveData(data);
// 步骤5:后置处理(钩子方法,可选重写)
postProcess(filePath, data);
log.info("【{}】数据导入成功", getImporterName());
} catch (Exception e) {
log.error("【{}】数据导入失败,文件路径:{},异常信息:{}",
getImporterName(), filePath, e.getMessage(), e);
throw new RuntimeException("数据导入失败", e);
}
}
/**
* 公共方法:校验文件合法性(所有导入方式共享)
*/
protected void validateFile(String filePath) {
log.info("校验文件合法性:{}", filePath);
// 基础校验:文件路径非空
if (filePath == null || filePath.trim().isEmpty()) {
throw new IllegalArgumentException("文件路径不能为空");
}
// 格式校验:由子类实现的抽象方法完成
if (!filePath.endsWith(getFileSuffix())) {
throw new IllegalArgumentException("文件格式错误,支持格式:" + getFileSuffix());
}
}
/**
* 抽象方法:获取文件后缀(子类必须指定)
*/
protected abstract String getFileSuffix();
/**
* 抽象方法:解析文件(子类实现差异化解析逻辑)
*/
protected abstract List<String> parseFile(String filePath);
/**
* 公共方法:数据校验(所有导入方式共享)
*/
protected void validateData(List<String> data) {
log.info("校验导入数据,数据条数:{}", data.size());
if (data == null || data.isEmpty()) {
throw new IllegalArgumentException("导入数据不能为空");
}
// 可添加更多通用校验规则(如字段长度、格式等)
}
/**
* 公共方法:数据入库(所有导入方式共享)
*/
protected void saveData(List<String> data) {
log.info("将{}条数据写入数据库", data.size());
// 实际项目中可通过Spring Data JPA、MyBatis等框架实现入库逻辑
}
/**
* 钩子方法:后置处理(可选重写,默认空实现)
*/
protected void postProcess(String filePath, List<String> data) {
// 默认无操作,子类可重写(如记录详细日志、发送通知等)
}
/**
* 抽象方法:获取导入器名称(用于日志打印)
*/
protected abstract String getImporterName();
}
3.2 实现具体子类(Concrete Implementation)
分别创建Excel导入和CSV导入的实现类,继承抽象模板并实现抽象方法:
Excel导入实现类
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* Excel数据导入实现类
*/
@Component
public class ExcelDataImporter extends AbstractDataImporter {
@Override
protected String getFileSuffix() {
// Excel支持的文件后缀
return ".xlsx";
}
@Override
protected List<String> parseFile(String filePath) {
log.info("解析Excel文件:{}", filePath);
// 模拟Excel解析逻辑(实际项目中可使用EasyExcel、POI等框架)
return Collections.singletonList("Excel解析的数据-用户名:张三,年龄:25");
}
@Override
protected String getImporterName() {
return "Excel导入器";
}
/**
* 重写钩子方法:Excel导入后的后置处理
*/
@Override
protected void postProcess(String filePath, List<String> data) {
log.info("Excel导入后置处理:备份文件到指定目录,文件路径:{}", filePath);
// 实际项目中可实现文件备份、发送导入成功通知等逻辑
}
}
CSV导入实现类
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* CSV数据导入实现类
*/
@Component
public class CsvDataImporter extends AbstractDataImporter {
@Override
protected String getFileSuffix() {
// CSV支持的文件后缀
return ".csv";
}
@Override
protected List<String> parseFile(String filePath) {
log.info("解析CSV文件:{}", filePath);
// 模拟CSV解析逻辑(实际项目中可使用OpenCSV等框架)
return Collections.singletonList("CSV解析的数据-用户名:李四,年龄:30");
}
@Override
protected String getImporterName() {
return "CSV导入器";
}
// 不重写postProcess方法,使用父类默认实现
}
3.3 SpringBoot配置与使用
通过Spring容器管理模板类和实现类,在业务层直接注入使用:
服务层调用(业务逻辑)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* 数据导入服务:统一调度不同导入器
*/
@Service
public class DataImportService {
// 注入所有AbstractDataImporter的实现类(Spring自动封装为Map,key为bean名称)
private final Map<String, AbstractDataImporter> importerMap;
@Autowired
public DataImportService(Map<String, AbstractDataImporter> importerMap) {
this.importerMap = importerMap;
}
/**
* 统一导入入口
* @param fileType 文件类型(excel/csv)
* @param filePath 文件路径
*/
public void doImport(String fileType, String filePath) {
// 根据文件类型获取对应的导入器
AbstractDataImporter importer = getImporterByFileType(fileType);
// 调用模板方法,执行完整导入流程
importer.importData(filePath);
}
/**
* 根据文件类型获取导入器
*/
private AbstractDataImporter getImporterByFileType(String fileType) {
switch (fileType.toLowerCase()) {
case "excel":
return importerMap.get("excelDataImporter");
case "csv":
return importerMap.get("csvDataImporter");
default:
throw new IllegalArgumentException("不支持的文件类型:" + fileType);
}
}
}
控制器层调用(客户端)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 数据导入控制器
*/
@RestController
@RequestMapping("/import")
public class DataImportController {
@Autowired
private DataImportService importService;
/**
* 数据导入接口
* @param fileType 文件类型(excel/csv)
* @param filePath 文件路径
* @return 导入结果
*/
@PostMapping("/data")
public String importData(
@RequestParam String fileType,
@RequestParam String filePath) {
importService.doImport(fileType, filePath);
return "数据导入请求已接收,正在处理";
}
}
3.4 测试结果
启动Spring Boot项目,调用接口测试不同导入方式:
测试Excel导入
访问 http://localhost:8080/import/data?fileType=excel&filePath=/data/user.xlsx
控制台输出日志(流程完整执行,包含后置处理):
校验文件合法性:/data/user.xlsx
解析Excel文件:/data/user.xlsx
校验导入数据,数据条数:1
将1条数据写入数据库
Excel导入后置处理:备份文件到指定目录,文件路径:/data/user.xlsx
【Excel导入器】数据导入成功
测试CSV导入
访问 http://localhost:8080/import/data?fileType=csv&filePath=/data/user.csv
控制台输出日志(使用父类默认后置处理):
校验文件合法性:/data/user.csv
解析CSV文件:/data/user.csv
校验导入数据,数据条数:1
将1条数据写入数据库
【CSV导入器】数据导入成功
测试异常场景(文件格式错误)
访问 http://localhost:8080/import/data?fileType=excel&filePath=/data/user.txt
控制台输出日志(异常被捕获并记录):
校验文件合法性:/data/user.txt
【Excel导入器】数据导入失败,文件路径:/data/user.txt,异常信息:文件格式错误,支持格式:.xlsx
四、模板方法模式与Spring的整合进阶
在实际开发中,可结合Spring的特性进一步优化模板方法模式的使用,提升灵活性和可维护性。
4.1 结合配置文件动态切换实现类
通过配置文件指定默认导入器,实现动态切换,无需修改代码:
1. 配置文件(application.yml)
data-import:
default-type: excel # 默认导入类型(excel/csv)
2. 改造DataImportService
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class DataImportService {
private final Map<String, AbstractDataImporter> importerMap;
private final String defaultFileType;
@Autowired
public DataImportService(
Map<String, AbstractDataImporter> importerMap,
@Value("${data-import.default-type}") String defaultFileType) {
this.importerMap = importerMap;
this.defaultFileType = defaultFileType;
}
/**
* 无参导入:使用默认文件类型
* @param filePath 文件路径
*/
public void doImport(String filePath) {
doImport(defaultFileType, filePath);
}
/**
* 有参导入:指定文件类型
* @param fileType 文件类型
* @param filePath 文件路径
*/
public void doImport(String fileType, String filePath) {
AbstractDataImporter importer = getImporterByFileType(fileType);
importer.importData(filePath);
}
// 省略getImporterByFileType方法...
}
4.2 结合策略模式优化选择逻辑
当导入类型较多时,可通过策略模式优化导入器选择逻辑,避免switch语句冗余:
1. 定义导入器策略接口(继承抽象模板)
public interface DataImportStrategy extends AbstractDataImporter {
/**
* 获取支持的文件类型标识(与前端传入的fileType对应)
*/
String getSupportFileType();
}
2. 改造子类实现策略接口
// Excel导入器
@Component
public class ExcelDataImporter extends AbstractDataImporter implements DataImportStrategy {
@Override
public String getSupportFileType() {
return "excel";
}
// 其他方法不变...
}
// CSV导入器
@Component
public class CsvDataImporter extends AbstractDataImporter implements DataImportStrategy {
@Override
public String getSupportFileType() {
return "csv";
}
// 其他方法不变...
}
3. 优化DataImportService
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class DataImportService {
private final Map<String, DataImportStrategy> strategyMap;
private final String defaultFileType;
@Autowired
public DataImportService(
List<DataImportStrategy> strategyList,
@Value("${data-import.default-type}") String defaultFileType) {
// 构建策略映射(key:支持的文件类型,value:对应策略)
this.strategyMap = strategyList.stream()
.collect(Collectors.toMap(
DataImportStrategy::getSupportFileType,
strategy -> strategy
));
this.defaultFileType = defaultFileType;
}
public void doImport(String filePath) {
doImport(defaultFileType, filePath);
}
public void doImport(String fileType, String filePath) {
DataImportStrategy strategy = strategyMap.get(fileType.toLowerCase());
if (strategy == null) {
throw new IllegalArgumentException("不支持的文件类型:" + fileType);
}
strategy.importData(filePath);
}
}
4.3 基于注解实现步骤扩展
通过自定义注解标记需要增强的步骤,结合Spring的AOP实现步骤级别的动态增强,无需修改模板类:
1. 定义自定义注解
import java.lang.annotation.*;
/**
* 标记需要增强的模板步骤
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnhanceStep {
}
2. 在模板类中标记注解
public abstract class AbstractDataImporter {
public final void importData(String filePath) {
try {
validateFile(filePath); // 公共步骤
List<String> data = parseFile(filePath); // 抽象步骤
validateData(data); // 公共步骤
saveData(data); // 公共步骤
postProcess(filePath, data); // 钩子步骤
} catch (Exception e) {
// 异常处理
}
}
@EnhanceStep // 标记需要增强的步骤
protected void saveData(List<String> data) {
log.info("将{}条数据写入数据库", data.size());
}
// 其他方法不变...
}
3. 实现AOP增强
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* 模板步骤增强AOP
*/
@Slf4j
@Aspect
@Component
public class StepEnhanceAop {
@Around("@annotation(com.example.demo.annotation.EnhanceStep)")
public Object enhanceStep(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("【步骤增强】开始执行:{}", joinPoint.getSignature().getName());
long startTime = System.currentTimeMillis();
// 执行原步骤逻辑
Object result = joinPoint.proceed();
long costTime = System.currentTimeMillis() - startTime;
log.info("【步骤增强】执行完成,耗时:{}ms", costTime);
return result;
}
}
五、常见问题与注意事项
5.1 避免模板方法过多导致的类膨胀
若算法步骤过多,抽象模板类会变得庞大复杂。建议:
- 拆分模板类:将步骤按职责拆分到多个辅助类(如校验类、入库类),模板类仅负责流程调度;
- 合并相似步骤:将逻辑相近的步骤合并为一个公共方法,减少步骤数量。
5.2 合理使用钩子方法
钩子方法是模板的扩展点,但过多的钩子会增加子类的复杂性。建议:
- 仅在必要时提供钩子方法,默认实现保持简洁;
- 钩子方法命名明确,如
needValidateData()(返回布尔值,控制是否执行数据校验)。
5.3 处理子类依赖注入问题
若子类需要依赖Spring Bean(如@Autowired注入其他服务),需注意:
- 抽象模板类需标注
@Component或其派生注解,确保子类被Spring扫描; - 子类中的依赖注入需使用
@Autowired,且确保依赖Bean已初始化(可通过@DependsOn控制顺序)。
5.4 与其他设计模式的区别
| 对比维度 | 模板方法模式 | 策略模式 | 工厂方法模式 |
|---|---|---|---|
| 核心目的 | 固定流程,灵活替换步骤 | 灵活替换算法/逻辑 | 灵活创建对象 |
| 关系类型 | 继承关系(子类依赖父类) | 组合关系(策略容器依赖策略接口) | 继承关系(子类创建对象) |
| 适用场景 | 流程固定、步骤差异化 | 算法多样化、无固定流程 | 对象创建逻辑复杂 |
| 耦合程度 | 较高(父类与子类强耦合) | 较低(基于接口组合) | 中等(子类依赖父类工厂) |
六、总结
模板方法模式是解决“流程固定、步骤差异化”问题的优秀方案,尤其适用于Spring Boot项目中的数据导入、接口调用、业务流程编排等场景。
通过本文的案例实现,我们可以看到:
- 模板方法模式将公共逻辑抽象到父类,子类仅关注差异化实现,极大减少了代码冗余;
- 与Spring框架的整合简单高效,可通过配置文件、
AOP、策略模式等方式优化灵活性和可维护性; - 核心优势在于固定算法骨架,确保流程一致性,同时通过抽象方法和钩子方法保留扩展空间,符合开闭原则。
在实际开发中,模板方法模式常与策略模式、工厂模式、AOP等结合使用,例如Spring的JdbcTemplate、RestTemplate等核心组件都采用了模板方法模式的设计思想。掌握模板方法模式的原理与使用,能帮助我们设计出更具扩展性、可维护性的代码架构。

浙公网安备 33010602011771号