策略模式的实际应用:从单一数据源到多数据源架构
引言
策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,将每个算法封装起来,并使它们可以互换。本文通过一个真实的生产案例,展示如何在 Spring Boot 项目中应用策略模式,实现多数据源的灵活架构。
问题背景
业务场景
在某个业务功能中,需要从多个不同的数据源获取数据:
- 实时数据源:存储实时采集数据(如温度、压力、流量等)
- 检测数据源:存储化验检测数据(如成分分析、质量指标等)
- 未来扩展:可能还有其他数据源(如 ERP 系统、MES 系统等)
传统方案的问题
如果使用传统的 if-else 方式:
java
// ❌ 不推荐:硬编码的数据源判断
public List<ReportData> fetchData(List<Parameter> parameters, LocalDate dataDate) {
List<ReportData> result = new ArrayList<>();
for (Parameter param : parameters) {
if ("realtimeDataSource".equals(param.getDataSource())) {
// 从实时数据源获取数据
result.add(fetchFromRealtimeDataSource(param, dataDate));
} else if ("labDataSource".equals(param.getDataSource())) {
// 从检测数据源获取数据
result.add(fetchFromLabDataSource(param, dataDate));
} else if ("erpDataSource".equals(param.getDataSource())) {
// 从 ERP 系统获取数据
result.add(fetchFromErpDataSource(param, dataDate));
}
// 每次新增数据源都要修改这里的代码
}
return result;
}
问题:
- ❌ 违反开闭原则(对扩展开放,对修改关闭)
- ❌ 每次新增数据源都要修改主业务逻辑
- ❌ 代码臃肿,难以维护
- ❌ 不同数据源的获取逻辑耦合在一起
策略模式设计
设计思路
核心思想:
- 定义统一的策略接口
- 每个数据源实现独立的策略类
- 通过 Spring 容器动态获取对应的策略
- 主业务逻辑不关心具体实现
架构图:
┌─────────────────────────────────────────────────────────┐
│ ReportServiceImpl │
│ (主业务逻辑) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ ApplicationContext.getBean() │ │
│ │ 根据 data_source 动态获取策略 │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ DataStrategy │
│ (策略接口) │
│ batchFetchData(parameters, date) │
└─────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────┐ ┌──────────────┐
│ RealtimeDataSource│ │ LabDataSource│ │ ErpDataSource│
│ Strategy │ │ Strategy │ │ Strategy │
│ (实时数据源) │ │ (检测数据源) │ │ (ERP系统) │
└──────────────────┘ └──────────────┘ └──────────────┘
策略接口定义
java
package com.example.biz.strategy;
import com.example.biz.model.entity.ReportData;
import com.example.biz.model.entity.Parameter;
import java.time.LocalDate;
import java.util.List;
/**
* 数据获取策略接口
*/
public interface DataStrategy {
/**
* 批量获取参数配置对应的数据
*
* @param parameters 参数配置列表
* @param dataDate 数据日期
* @return 数据列表
*/
List<ReportData> batchFetchData(
List<Parameter> parameters,
LocalDate dataDate);
}
设计要点:
- ✅ 接口简洁,只有一个方法
- ✅ 参数明确:参数配置列表 + 数据日期
- ✅ 返回值清晰:统一的数据实体列表
- ✅ 批量处理:一次调用处理多个参数,提升性能
策略实现
1. 实时数据源策略
java
package com.example.biz.strategy.impl;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.springframework.stereotype.Component;
import com.example.biz.model.entity.ReportData;
import com.example.biz.model.entity.Parameter;
import com.example.biz.strategy.DataStrategy;
import com.example.biz.support.dto.RealtimeValueDto;
import com.example.biz.support.service.IRealtimeDataService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 实时数据源策略
* 从实时数据源获取采集数据
*/
@Slf4j
@Component("realtimeDataSource")
@RequiredArgsConstructor
public class RealtimeDataSourceStrategy implements DataStrategy {
private final IRealtimeDataService realtimeDataService;
@Override
public List<ReportData> batchFetchData(
List<Parameter> parameters,
LocalDate dataDate) {
// 转换日期范围
Date startTime = Date.from(dataDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
Date endTime = Date.from(dataDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
Date date = Date.from(dataDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
// 提取所有 dataKey
List<String> dataKeys = parameters.stream()
.map(Parameter::getDataKey)
.collect(Collectors.toList());
// 批量查询数据
Map<String, BigDecimal> dataMap = realtimeDataService.batchListByKey(
dataKeys,
startTime,
endTime,
RealtimeValueDto::toNameMaps
).orElse(Collections.emptyMap());
// 构造返回数据列表
List<ReportData> result = parameters.stream()
.map(parameter -> {
BigDecimal value = dataMap.get(parameter.getDataKey());
if (value != null) {
ReportData data = new ReportData();
data.setParameterId(parameter.getId());
data.setDataDate(date);
data.setDataValue(value.doubleValue());
return data;
} else {
log.warn("未查询到数据: dataKey={}, date={}",
parameter.getDataKey(), dataDate);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
log.debug("批量查询数据完成: date={}, total={}, success={}",
dataDate, parameters.size(), result.size());
return result;
}
}
关键点:
@Component("realtimeDataSource"):指定 Bean 名称,与数据库中的data_source字段对应- 批量查询:一次性查询所有数据,提升性能
- Stream API:使用 map 和 filter 进行数据转换
- 容错处理:对查询不到的数据记录日志并过滤
2. 检测数据源策略
java
package com.example.biz.strategy.impl;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import org.springframework.stereotype.Component;
import com.example.biz.model.entity.ReportData;
import com.example.biz.model.entity.Parameter;
import com.example.biz.strategy.DataStrategy;
import lombok.extern.slf4j.Slf4j;
/**
* 检测数据源策略
* 从检测系统获取化验数据
*/
@Slf4j
@Component("labDataSource")
public class LabDataSourceStrategy implements DataStrategy {
@Override
public List<ReportData> batchFetchData(
List<Parameter> parameters,
LocalDate dataDate) {
// TODO: 实现检测数据批量查询逻辑
log.warn("检测数据源策略暂未实现: date={}, count={}",
dataDate, parameters.size());
return Collections.emptyList();
}
}
关键点:
- 同样实现
DataStrategy接口 - 可以有自己的业务逻辑和数据源
- 返回空列表而不是 null,避免 NPE
主业务逻辑
动态获取策略
java
@Slf4j
@Service
@RequiredArgsConstructor
public class ReportServiceImpl implements ReportService {
private final ParameterMapper parameterMapper;
private final ApplicationContext applicationContext;
private final DataSaveService dataSaveService;
@Override
public void generateReportData(LocalDate localDate) {
log.info("开始生成数据, 日期: {}", localDate);
// 1. 查询所有生效的配置
LambdaQueryWrapper<Parameter> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.isNotNull(Parameter::getDataSource);
List<Parameter> parameters = parameterMapper.selectList(queryWrapper);
// 2. 根据 data_source 分组
Map<String, List<Parameter>> groupedByDataSource = parameters.stream()
.collect(Collectors.groupingBy(Parameter::getDataSource));
// 3. 批量获取所有数据源的数据
List<ReportData> allDataList = groupedByDataSource.entrySet().stream()
.flatMap(entry -> {
try {
String dataSource = entry.getKey();
List<Parameter> params = entry.getValue();
// 🔑 关键:通过 Spring 容器动态获取对应的策略 bean
DataStrategy strategy = applicationContext.getBean(
dataSource, DataStrategy.class);
// 批量调用策略获取数据
List<ReportData> dataList = strategy.batchFetchData(params, localDate);
if (dataList != null && !dataList.isEmpty()) {
log.debug("数据获取成功: dataSource={}, count={}", dataSource, dataList.size());
return dataList.stream();
} else {
log.warn("批量数据获取为空: dataSource={}", dataSource);
return Stream.empty();
}
} catch (Exception e) {
log.error("处理数据源异常: dataSource={}", entry.getKey(), e);
return Stream.empty();
}
})
.collect(Collectors.toList());
// 4. 保存数据
if (!allDataList.isEmpty()) {
dataSaveService.saveData(allDataList);
log.info("数据批量保存完成: date={}, total={}", localDate, allDataList.size());
}
}
}
核心代码解析:
java
// 🔑 核心:通过 Spring 容器动态获取策略 Bean
DataStrategy strategy = applicationContext.getBean(
dataSource, // 数据源名称,如 "realtimeDataSource"
DataStrategy.class); // 策略接口类型
工作流程:
- 查询配置表,获取所有参数配置
- 按
data_source字段分组 - 遍历每个数据源:
- 通过
applicationContext.getBean()获取对应的策略实现 - 调用策略的
batchFetchData()方法获取数据 - 使用
flatMap将多个数据源的数据合并到一个列表
- 通过
- 批量保存所有数据
代码演进过程
第一版:单个数据获取
java
// ❌ 第一版:一次获取一个参数的数据
public interface DataStrategy {
ReportData fetchData(
Parameter parameter,
LocalDate dataDate);
}
问题:
- 需要循环调用,性能差
- 每次调用都要建立数据库连接
第二版:批量获取但返回 Map
java
// ⚠️ 第二版:批量获取但返回 Map
public interface DataStrategy {
Map<Long, BigDecimal> batchFetchData(
List<Parameter> parameters,
LocalDate dataDate);
ReportData fetchData(
Parameter parameter,
LocalDate dataDate);
}
问题:
- 返回 Map,不够灵活
- 有两个方法,接口复杂
- 策略实现需要构造完整的对象
第三版:简化的批量接口(最终方案)
java
// ✅ 第三版:简化的批量接口
public interface DataStrategy {
List<ReportData> batchFetchData(
List<Parameter> parameters,
LocalDate dataDate);
}
优势:
- ✅ 接口简洁,只有一个方法
- ✅ 策略实现直接构造完整对象
- ✅ 返回类型明确
- ✅ 支持批量处理
扩展性展示
如何添加新的数据源?
假设需要从 ERP 系统获取数据,只需三步:
步骤1:创建新的策略实现
java
package com.example.biz.strategy.impl;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Component;
import com.example.biz.model.entity.ReportData;
import com.example.biz.model.entity.Parameter;
import com.example.biz.strategy.DataStrategy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* ERP 数据源策略
* 从 ERP 系统获取数据
*/
@Slf4j
@Component("erpDataSource")
@RequiredArgsConstructor
public class ErpDataSourceStrategy implements DataStrategy {
private final ErpService erpService;
@Override
public List<ReportData> batchFetchData(
List<Parameter> parameters,
LocalDate dataDate) {
// 实现从 ERP 获取数据的逻辑
return parameters.stream()
.map(param -> {
// 调用 ERP 服务
BigDecimal value = erpService.getValue(
param.getErpField(), dataDate);
// 构造返回对象
ReportData data = new ReportData();
data.setParameterId(param.getId());
data.setDataDate(Date.from(dataDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
data.setDataValue(value.doubleValue());
return data;
})
.collect(Collectors.toList());
}
}
步骤2:在数据库中添加配置
sql
INSERT INTO report_parameter (
data_source,
tag_name,
parameter_no,
-- 其他字段
) VALUES (
'erpDataSource', -- 对应策略的 Bean 名称
'ERP_FIELD_NAME',
10,
-- 其他字段
);
步骤3:测试
bash
curl "http://localhost:8080/report/generate?dataDate=2026-01-08"
完成! 不需要修改任何主业务逻辑代码。
核心技术点
1. Spring 容器动态获取 Bean
java
// 通过 Bean 名称 + 类型获取
DataStrategy strategy = applicationContext.getBean(
"realtimeDataSource",
DataStrategy.class);
原理:
- Spring 容器在启动时会扫描所有
@Component注解的类 - 将类名首字母小写作为默认 Bean 名称
- 也可以通过
@Component("customName")指定 Bean 名称 applicationContext.getBean()可以通过名称和类型获取 Bean
2. 分组与扁平化处理
java
// 按 data_source 分组
Map<String, List<Parameter>> grouped = parameters.stream()
.collect(Collectors.groupingBy(Parameter::getDataSource));
// 扁平化合并多个数据源的数据
List<ReportData> allData = grouped.entrySet().stream()
.flatMap(entry -> {
List<ReportData> dataList = strategy.batchFetchData(...);
return dataList.stream(); // 将 List 转换为 Stream
})
.collect(Collectors.toList()); // 收集到一个 List
flatMap 的作用:
- 将
Stream<List<T>>转换为Stream<T> - 将多个列表合并成一个列表
- 是 map + flatten 的组合操作
3. 策略模式的 Spring 实现
传统策略模式:
- 需要手动创建策略对象
- 需要维护策略映射关系
- 策略切换需要修改代码
Spring 策略模式:
- Spring 自动管理策略 Bean
- 通过 Bean 名称动态获取策略
- 数据库配置控制策略选择
- 完全解耦,符合开闭原则
最佳实践总结
1. 策略接口设计
✅ 推荐做法:
- 接口简洁,方法数量少(1-2个)
- 方法参数明确
- 返回类型统一
- 支持批量处理
❌ 避免做法:
- 接口过于复杂
- 方法职责不清晰
- 返回类型不一致
2. 策略实现命名
命名规范:
- Bean 名称:
xxxDataSource(与数据库字段对应) - 类名:
XxxDataSourceStrategy(便于理解) - 包路径:
strategy.impl(统一管理)
java
@Component("realtimeDataSource") // Bean 名称
public class RealtimeDataSourceStrategy // 类名
implements DataStrategy {
}
3. 异常处理
推荐做法:
- 在主业务逻辑中捕获异常
- 避免某个数据源失败影响整体流程
- 记录详细的错误日志
java
.flatMap(entry -> {
try {
// 调用策略
return strategy.batchFetchData(...).stream();
} catch (Exception e) {
log.error("处理数据源异常: dataSource={}", entry.getKey(), e);
return Stream.empty(); // 返回空流,不影响其他数据源
}
})
4. 性能优化
批量处理优于单个处理:
- ✅ 批量查询数据库
- ✅ 批量调用外部服务
- ✅ 批量构造对象
对比:
java
// ❌ 单个处理:循环 100 次
for (Parameter param : parameters) {
Data data = strategy.fetchData(param); // 100 次调用
}
// ✅ 批量处理:1 次调用
List<Data> dataList = strategy.batchFetchData(parameters); // 1 次调用
策略模式 vs 其他方案
方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| if-else | 简单直接 | 违反开闭原则、代码臃肿 | 数据源固定且很少 |
| 策略模式 | 符合开闭原则、扩展性好 | 类数量增多 | 数据源多样、经常扩展 |
| 工厂模式 | 集中管理对象创建 | 需要维护工厂类 | 对象创建复杂 |
| Spring策略 | 自动管理、完全解耦 | 依赖 Spring 容器 | Spring 项目 |
选择建议
- 小型项目:if-else 足够
- 中型项目:传统策略模式
- Spring 项目:Spring 策略模式(推荐)
总结
核心要点
- 策略接口设计:简洁、明确、支持批量
- Spring 动态获取:通过 Bean 名称动态获取策略
- 完全解耦:主业务逻辑不依赖具体策略实现
- 开闭原则:新增数据源无需修改主业务逻辑
- 性能优化:批量处理优于单个处理
实战价值
通过策略模式,我们实现了:
- ✅ 多数据源的统一管理
- ✅ 新数据源的快速接入
- ✅ 代码的可维护性和可扩展性
- ✅ 符合 SOLID 原则