SpringBoot中策略模式与Map注入的优雅实践:多实现类动态调用
1. 从“硬编码”到“优雅切换”:一个真实项目的痛点
我记得几年前刚接手一个电商后台项目时,遇到了一个让我头疼了好几天的问题。项目里有一个“消息通知”模块,需要支持短信、邮件、App推送、微信模板消息等多种渠道。当时的代码大概是这样的:
@Service
public class NotifyService {
public void send(String type, String content, String target) {
if ("sms".equals(type)) {
// 调用短信服务商的API,一堆参数拼接和HTTP请求
System.out.println("发送短信给 " + target + ",内容:" + content);
} else if ("email".equals(type)) {
// 配置SMTP,组装邮件正文和附件
System.out.println("发送邮件给 " + target + ",内容:" + content);
} else if ("app_push".equals(type)) {
// 集成个推/极光,处理设备Token
System.out.println("发送App推送给 " + target + ",内容:" + content);
} else if ("wechat".equals(type)) {
// 调用微信API,处理access_token和模板ID
System.out.println("发送微信模板消息给 " + target + ",内容:" + content);
}
// ... 未来可能还有钉钉、飞书等等
}
}
这代码看着就让人窒息,对吧?每次加一个新渠道,我都要钻进这个庞大的 if-else 森林里,小心翼翼地添加一个新的分支。更麻烦的是,不同渠道的参数、配置、错误处理逻辑完全不同,全都糅在一个方法里,导致这个方法后来膨胀到了几百行,谁都不敢轻易动它。测试的时候更是噩梦,为了测一个邮件功能,我得把短信、推送的模拟逻辑都跑一遍。
这就是典型的“硬编码”策略选择,它的坏处太明显了:代码臃肿、难以维护、违反开闭原则(对扩展开放,对修改关闭)。每次变动都像是在已经摇摇欲坠的积木塔上再加一块,风险极高。
后来,我接触到了策略模式,感觉就像发现了一把万能钥匙。策略模式的核心思想是定义一族算法,将它们分别封装起来,并且使它们可以互相替换。它把变化的逻辑(怎么发通知)从不变的主体(要发通知)中抽离出来。但光知道模式还不够,在SpringBoot的世界里,如何优雅地管理这些策略,并在需要时能像在超市货架上取商品一样,轻松拿到我想要的那个实现,这才是关键。
直到我把策略模式和SpringBoot的 Map注入结合起来,才真正解决了这个痛点。这种组合拳,让我再也不用写那些丑陋的 if-else 或者 switch,业务逻辑变得清晰无比,扩展新渠道也变成了几分钟的快乐工作。下面,我就带你一步步拆解这个“优雅实践”到底是怎么玩的。
2. 核心武器拆解:策略模式与Spring的依赖注入
2.1 策略模式:化繁为简的设计艺术
策略模式其实一点也不玄乎。咱们还用发通知的例子来说。上面那一坨 if-else,本质上就是一堆不同的“发送策略”。策略模式告诉我们,应该把这些策略独立出来。
首先,我们定义一个共同的策略接口,规定所有通知方式都必须实现“发送”这个动作:
public interface NotifyStrategy {
/**
* 发送通知
* @param content 内容
* @param target 目标(手机号/邮箱/用户ID等)
* @return 发送是否成功
*/
boolean send(String content, String target);
/**
* 获取策略类型,用于后续的映射查找
* @return 类型标识,如"sms", "email"
*/
String getType();
}
看,这个接口就是我们的“契约”。接下来,让每种通知方式都成为履行这个契约的独立个体:
@Service
public class SmsNotifyStrategy implements NotifyStrategy {
@Override
public boolean send(String content, String target) {
// 专注于短信发送的逻辑:签名、模板、调用第三方SDK...
System.out.println("[短信策略] 正在发送短信至 " + target + " : " + content);
// 模拟发送成功
return true;
}
@Override
public String getType() {
return "sms";
}
}
@Service
public class EmailNotifyStrategy implements NotifyStrategy {
@Override
public boolean send(String content, String target) {
// 专注于邮件发送的逻辑:SMTP、HTML模板、附件...
System.out.println("[邮件策略] 正在发送邮件至 " + target + " : " + content);
return true;
}
@Override
public String getType() {
return "email";
}
}
这样一来,每个策略类都变得职责单一、内聚性高。短信的修改不会影响到邮件,反之亦然。这就是策略模式带来的最直接好处:解耦。
2.2 Spring的魔法:当@Autowired遇到Map和List
策略定义好了,怎么管理呢?传统做法可能是在工厂类里手动 new 这些对象,但那就失去了Spring依赖注入容器管理的优势(比如自动装配、AOP、生命周期管理等)。
这里就要请出Spring的一个不太起眼但极其强大的特性:对集合类型的依赖注入。很多朋友只知道 @Autowired 可以注入单个Bean,其实它还能注入 Map 和 List。
1. 注入List:
当你注入 List<NotifyStrategy> 时,Spring会把容器中所有实现了 NotifyStrategy 接口的Bean,都收集到这个List里。顺序一般是Bean定义的顺序,但不要依赖这个顺序。
2. 注入Map<String, Interface>:这才是我们今天的主角。
当你注入 Map<String, NotifyStrategy> 时,Spring会做一件更聪明的事:它会以 Bean的名字(默认是类名首字母小写)作为Key,以Bean实例作为Value,自动组装成一个Map。
@Component
public class StrategyManager {
// Spring会自动将所有的NotifyStrategy实现Bean装入这个Map
// Key: Bean名称 (如 smsNotifyStrategy, emailNotifyStrategy)
// Value: 对应的Bean实例
@Autowired
private Map<String, NotifyStrategy> strategyMap;
// 也可以注入List,方便遍历或做其他操作
@Autowired
private List<NotifyStrategy> strategyList;
public void printAllStrategies() {
System.out.println("当前容器中的策略Map:" + strategyMap.keySet());
System.out.println("当前容器中的策略List数量:" + strategyList.size());
}
}
你可以写个测试方法调用 printAllStrategies(),会发现控制台打印出了 {smsNotifyStrategy=..., emailNotifyStrategy=...}。这个Map就是一个完整的策略仓库,我们随时可以根据Key来取用具体的策略。
注意:这里Map的Key默认是Spring Bean的名称。如果你的策略Bean使用了
@Service(“mySms”)自定义了名字,那么Map的Key就会变成“mySms”。这一点对于后续的查找至关重要。
3. 构建动态调用的核心:策略工厂与上下文
有了策略仓库(Map),我们还需要一个“导购员”或者“调度中心”,来根据客户(业务逻辑)的需求,从仓库里找到正确的策略。这就是策略工厂或策略上下文的角色。
3.1 基础版工厂:根据Bean名称直接获取
这是最直接的方式,假设我们的策略Bean名称(或自定义名称)就能直接作为业务中的类型标识。
@Component
public class NotifyStrategyFactory {
@Autowired
private Map<String, NotifyStrategy> strategyMap;
/**
* 根据策略类型获取具体的策略实现
* @param strategyBeanName Spring容器中的Bean名称,如 "smsNotifyStrategy"
* @return 策略实现
* @throws RuntimeException 当找不到对应策略时
*/
public NotifyStrategy getStrategy(String strategyBeanName) {
NotifyStrategy strategy = strategyMap.get(strategyBeanName);
if (strategy == null) {
throw new RuntimeException("未找到对应的通知策略: " + strategyBeanName);
}
return strategy;
}
}
在业务层这样使用:
@Service
public class OrderService {
@Autowired
private NotifyStrategyFactory notifyStrategyFactory;
public void paySuccess(String orderId, String userId, String notifyType) {
// ... 处理订单支付成功逻辑
// 动态选择通知策略
NotifyStrategy strategy = notifyStrategyFactory.getStrategy(notifyType + "NotifyStrategy");
strategy.send("您的订单" + orderId + "已支付成功", userId);
}
}
这种方式简单,但有个缺点:业务代码需要关心Spring Bean的命名规则(notifyType + “NotifyStrategy”),耦合了技术细节,不太优雅。
3.2 进阶版工厂:引入枚举进行优雅映射
更常见的做法是,定义一个业务枚举,将业务代码中的类型标识(如sms, email)与具体的策略Bean关联起来。这样业务层只需要使用枚举,无需知道背后的Bean叫什么。
第一步:定义业务枚举
public enum NotifyType {
SMS("sms", "短信通知"),
EMAIL("email", "邮件通知"),
APP_PUSH("app_push", "应用推送"),
WECHAT("wechat", "微信模板消息");
private final String code; // 业务代码,用于前端传递或数据库存储
private final String description;
// 这里可以加一个字段关联Bean名称,但更推荐在工厂里维护映射关系
// private final String beanName;
NotifyType(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
// 根据code获取枚举的静态方法(略)
}
第二步:升级策略工厂,内部维护映射
@Component
public class NotifyStrategyFactory {
@Autowired
private Map<String, NotifyStrategy> strategyMap;
// 内部映射:业务类型 -> Spring Bean名称
private static final Map<String, String> TYPE_TO_BEAN_NAME = new HashMap<>();
static {
TYPE_TO_BEAN_NAME.put(NotifyType.SMS.getCode(), "smsNotifyStrategy");
TYPE_TO_BEAN_NAME.put(NotifyType.EMAIL.getCode(), "emailNotifyStrategy");
TYPE_TO_BEAN_NAME.put(NotifyType.APP_PUSH.getCode(), "appPushNotifyStrategy");
TYPE_TO_BEAN_NAME.put(NotifyType.WECHAT.getCode(), "wechatNotifyStrategy");
}
/**
* 根据业务类型码获取策略
* @param typeCode 业务类型码,如 "sms"
* @return 策略实现
*/
public NotifyStrategy getStrategyByType(String typeCode) {
String beanName = TYPE_TO_BEAN_NAME.get(typeCode);
if (beanName == null) {
throw new IllegalArgumentException("不支持的策略类型: " + typeCode);
}
NotifyStrategy strategy = strategyMap.get(beanName);
if (strategy == null) {
throw new RuntimeException("策略Bean未找到: " + beanName);
}
return strategy;
}
/**
* 更优雅的方式:直接传入枚举
* @param notifyType 通知类型枚举
* @return 策略实现
*/
public NotifyStrategy getStrategy(NotifyType notifyType) {
return getStrategyByType(notifyType.getCode());
}
}
第三步:在策略接口实现中关联类型 为了让映射更自动化,我们可以让每个策略实现类自己上报它的业务类型。修改策略接口和实现:
public interface NotifyStrategy {
boolean send(String content, String target);
// 策略类自己声明其负责的业务类型
String getTypeCode();
}
@Service
public class SmsNotifyStrategy implements NotifyStrategy {
@Override
public boolean send(String content, String target) { ... }
@Override
public String getTypeCode() {
return NotifyType.SMS.getCode(); // 返回 "sms"
}
}
然后工厂可以优化为自动扫描建立映射,避免硬编码:
@Component
public class NotifyStrategyFactory {
private final Map<String, NotifyStrategy> strategyMap = new HashMap<>();
// 使用构造器注入,在Bean创建时立即初始化映射
@Autowired
public NotifyStrategyFactory(List<NotifyStrategy> strategies) {
for (NotifyStrategy strategy : strategies) {
strategyMap.put(strategy.getTypeCode(), strategy);
}
System.out.println("策略工厂初始化完成,可用策略: " + strategyMap.keySet());
}
public NotifyStrategy getStrategy(String typeCode) {
NotifyStrategy strategy = strategyMap.get(typeCode);
if (strategy == null) {
throw new IllegalArgumentException("未找到类型为 [" + typeCode + "] 的通知策略");
}
return strategy;
}
}
这种方式是最优雅的!工厂在启动时,通过构造器接收到所有策略Bean的List,然后遍历每个策略,调用其 getTypeCode() 方法,用业务类型码作为Key,策略实例本身作为Value,构建一个内部的 Map<String, NotifyStrategy>。业务层使用起来就非常清爽了:
@Service
public class UserService {
@Autowired
private NotifyStrategyFactory notifyStrategyFactory;
public void sendWelcomeMessage(String userId, String notifyTypeCode) {
String message = "欢迎注册我们的服务!";
NotifyStrategy strategy = notifyStrategyFactory.getStrategy(notifyTypeCode);
boolean success = strategy.send(message, userId);
// 处理发送结果...
}
}
4. 实战深化:处理复杂场景与高级技巧
掌握了基本玩法后,我们来看看在实际项目中可能遇到的更复杂的场景以及如何应对。
4.1 场景一:策略需要动态参数或配置
有些策略可能需要特定的配置才能初始化。比如短信策略需要 appKey 和 appSecret,邮件策略需要 host 和 port。我们可以利用Spring的 @ConfigurationProperties 或 @Value 为每个策略单独配置。
为策略类添加配置:
@Service
@ConfigurationProperties(prefix = "notify.sms") // 从application.yml读取notify.sms下的配置
public class SmsNotifyStrategy implements NotifyStrategy {
private String appKey;
private String appSecret;
private String signName;
// setter 方法必须提供,Spring会通过它们注入配置值
public void setAppKey(String appKey) { this.appKey = appKey; }
public void setAppSecret(String appSecret) { this.appSecret = appSecret; }
public void setSignName(String signName) { this.signName = signName; }
@Override
public boolean send(String content, String target) {
System.out.println("使用appKey[" + appKey + "]发送短信,签名[" + signName + "]");
// 实际调用短信API
return true;
}
// ... getTypeCode()
}
在 application.yml 中配置:
notify:
sms:
app-key: "your_sms_key"
app-secret: "your_sms_secret"
sign-name: "我的公司"
email:
host: "smtp.example.com"
port: 587
username: "noreply@example.com"
这样,每个策略所需的配置都被清晰地隔离和管理,工厂在注入时,Spring已经帮我们完成了配置的绑定和Bean的初始化。
4.2 场景二:策略的懒加载与条件化装配
不是所有策略在应用启动时都需要。也许有些策略依赖某些特定的开关,或者只在特定环境下才启用。这时可以用Spring的 @ConditionalOnProperty 或 @ConditionalOnClass 注解。
@Service
@ConditionalOnProperty(name = "notify.wechat.enabled", havingValue = "true")
public class WechatNotifyStrategy implements NotifyStrategy {
// 只有当配置文件中 notify.wechat.enabled=true 时,这个Bean才会被创建和注入到Map中
@Override
public boolean send(String content, String target) { ... }
@Override
public String getTypeCode() { return "wechat"; }
}
@Service
@ConditionalOnClass(name = "com.thirdparty.push.PushClient") // 只有当类路径存在这个客户端类时才生效
public class AppPushNotifyStrategy implements NotifyStrategy {
// 依赖了特定的第三方推送SDK
@Override
public boolean send(String content, String target) { ... }
@Override
public String getTypeCode() { return "app_push"; }
}
使用条件化装配后,你的策略工厂和Map注入机制完全不用改。如果某个策略的装配条件不满足,它根本不会出现在Spring容器里,自然也不会被注入到Map中。getStrategy 方法在调用时如果发现对应的 typeCode 不存在,就会抛出我们预先定义好的异常,比如“当前环境未启用微信通知功能”。这实现了策略的动态可插拔,非常灵活。
4.3 场景三:策略执行前后的统一处理(AOP)
我们可能需要在所有策略执行前后做一些统一的事情,比如记录日志、监控执行时间、进行权限校验等。这时,面向切面编程(AOP)就派上用场了。
@Aspect
@Component
@Slf4j
public class NotifyStrategyAspect {
// 拦截所有NotifyStrategy接口的send方法
@Around("execution(* com.yourpackage.strategy.NotifyStrategy.send(..))")
public Object aroundSend(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
String target = (String) args[1];
String strategyName = joinPoint.getTarget().getClass().getSimpleName();
log.info("开始执行策略 [{}] 的 [{}] 方法,目标: {}", strategyName, methodName, target);
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed(); // 执行实际的send方法
long costTime = System.currentTimeMillis() - startTime;
log.info("策略 [{}] 执行成功,耗时: {}ms", strategyName, costTime);
return result;
} catch (Exception e) {
log.error("策略 [{}] 执行失败,目标: {}", strategyName, target, e);
throw e; // 可以选择重试、降级或直接抛出
}
}
}
通过AOP,我们将横切关注点(日志、监控)从业务策略中彻底剥离。每个策略类只需要关心自己最核心的发送逻辑,代码变得更加纯粹和易于测试。
5. 避坑指南与最佳实践
踩过不少坑之后,我总结了一些关键点,能帮你更快更稳地上手这种模式。
1. 确保Map的Key唯一且明确
这是最容易出问题的地方。Spring默认用Bean名称作为Map的Key。如果你没有指定,它就是类名首字母小写(如 smsNotifyStrategy)。一定要确保你的查找逻辑(无论是直接传Bean名,还是通过枚举映射)能精确匹配到这个Key。我建议在策略接口中强制要求实现 getTypeCode() 方法,并在工厂初始化时使用这个code作为Key,这样最可控。
2. 处理“找不到策略”的异常
业务代码调用 factory.getStrategy(type) 时,如果传入的 type 不对应任何已注册的策略,一定要有友好的异常处理。不要直接返回 null,这会导致后续的NPE。像我们上面那样抛出明确的 IllegalArgumentException 或自定义的业务异常,并在全局异常处理器中捕获,返回给前端清晰的错误信息。
3. 策略的无状态与线程安全
理想情况下,你的策略实现应该是无状态的(Stateless)。即它们不包含会随着调用而改变的成员变量。所有的配置都应该在初始化时注入(通过 @Value 或 @ConfigurationProperties),业务数据通过方法参数传递。这样能保证策略Bean是单例且线程安全的。如果某个策略必须有状态,请仔细考虑其作用域(@Scope)和并发访问问题。
4. 单元测试变得极其简单 由于策略模式的高内聚和低耦合,单元测试写起来非常愉快。你可以单独测试每一个策略类,Mock掉它的外部依赖(如HTTP客户端)。对于使用策略的Service类,你可以Mock策略工厂,让它返回一个Mock的策略对象,从而专注于测试Service本身的业务逻辑。
@SpringBootTest
class SmsNotifyStrategyTest {
@Autowired
private SmsNotifyStrategy smsStrategy;
@Test
void testSendSms() {
// 可以轻松配置测试用的属性,或者Mock内部的HTTP客户端
boolean result = smsStrategy.send("测试内容", "13800138000");
assertTrue(result);
}
}
5. 与Spring Cloud整合的思考 在微服务架构下,这种模式依然适用,而且可以玩出更多花样。例如,你可以将策略的“类型”与配置中心(如Nacos、Apollo)的动态配置绑定,实现不重启服务就动态切换或降级某个通知渠道。或者,将策略工厂本身设计成一个轻量的“规则引擎”,根据从配置中心读取的规则来决定最终使用哪个策略。
从我自己的经验来看,将策略模式与SpringBoot的Map注入结合,绝不仅仅是为了消灭 if-else。它更是一种思维模式的转变,让你从“过程式”的编码,转向“声明式”的组装。当你习惯这种模式后,你会发现系统中很多类似的“分支选择”逻辑都可以被这样优雅地重构,代码的可读性、可维护性和可扩展性都会得到质的提升。下次当你再看到一堆 if-else 或 switch 时,不妨先停下来想想:这里是不是藏着一个等待被抽象的策略家族?
更多推荐

所有评论(0)