策略模式(Strategy Pattern)是后端开发中消灭满屏 if-else 的终极武器。在 Spring Boot 项目中,结合 Spring 的 IoC(控制反转)机制,策略模式的实现会变得极其优雅。
一、 业务场景说明
在 HR 系统中,计算工资的逻辑极其复杂。
-
如果是正式员工 (Regular),需要计算基本工资 + 绩效 + 扣除五险一金。
-
如果是实习生 (Intern),按日薪计算,且不需要扣除五险一金,但要扣除超额的劳务报税。
-
如果是外包人员 (Contractor),按项目结算。
如果不走策略模式,你的代码里就会写满
if (type == "Regular") { ... } else if (type == "Intern") { ... },一旦新增员工类型,这段代码就会极度膨胀且难以维护。
二、 Spring Boot 中的标准代码实现
1. 定义策略接口(Strategy)
首先,我们定义一个统一的薪资计算接口。
import java.math.BigDecimal;
public interface SalaryStrategy {
/**
* 执行具体的薪资计算
*/
BigDecimal calculate(Long employeeId);
}
2. 编写具体的策略实现类(Concrete Strategies)
为每种类型的员工写一个实现类,并一定要加上 @Service 或 @Component 注解 ,将其交给 Spring 容器管理。为了方便后续查找,我们可以通过注解的 value 属性给 Bean 起一个特定的名字。
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
// 正式员工策略,Bean 名称命名为 "regularSalaryStrategy"
@Service("regularSalaryStrategy")
public class RegularSalaryStrategy implements SalaryStrategy {
@Override
public BigDecimal calculate(Long employeeId) {
System.out.println("查询绩效、扣除五险一金...");
return new BigDecimal("15000.00");
}
}
// 实习生策略,Bean 名称命名为 "internSalaryStrategy"
@Service("internSalaryStrategy")
public class InternSalaryStrategy implements SalaryStrategy {
@Override
public BigDecimal calculate(Long employeeId) {
System.out.println("按出勤天数结算,扣除劳务税...");
return new BigDecimal("4000.00");
}
}
3. 构建策略工厂(Context / Factory)✨ 核心部分
在这里,我们利用 Spring 强大的集合注入能力,把所有的策略统一管理起来。Java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class SalaryStrategyFactory {
/**
* 重点来了:Spring 会自动查找到所有实现了 SalaryStrategy 接口的 Bean。
* 并把它们注入到这个 Map 中。
* Map 的 Key 就是 Bean 的名字(如 "regularSalaryStrategy"),Value 就是 Bean 的实例对象。
*/
@Autowired
private Map<String, SalaryStrategy> strategyMap;
/**
* 外部服务调用此方法获取对应的策略
*/
public SalaryStrategy getStrategy(String employeeType) {
// 拼接出约定的 Bean 名称
String beanName = employeeType + "SalaryStrategy";
SalaryStrategy strategy = strategyMap.get(beanName);
if (strategy == null) {
throw new IllegalArgumentException("未找到对应的薪资策略: " + employeeType);
}
return strategy;
}
}
4. 在业务服务中使用
现在,主业务流的代码变得极其清爽,完全看不到任何的 if-else。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class PayrollService {
@Autowired
private SalaryStrategyFactory strategyFactory;
// 前端传入员工ID和员工类型(如 "regular" 或 "intern")
public void paySalary(Long employeeId, String employeeType) {
// 1. 通过工厂拿到对应的策略
SalaryStrategy strategy = strategyFactory.getStrategy(employeeType);
// 2. 执行计算
BigDecimal finalSalary = strategy.calculate(employeeId);
System.out.println("员工 " + employeeId + " 最终发放薪资: " + finalSalary);
}
}
三、 Bean 到底是在什么时候注入的?
这又回到了我们之前聊过的 Spring Boot 启动流程 和 Bean 生命周期。
Bean 的注入发生在 Spring Boot 启动核心阶段的 refreshContext()(刷新上下文) 这一步。具体到细节层面,顺序是这样的:
-
扫描阶段 (BeanFactoryPostProcessor): Spring 容器启动时,会扫描你代码里所有的
@Service和@Component注解。它发现了RegularSalaryStrategy、InternSalaryStrategy和SalaryStrategyFactory。此时,它只是把它们的"图纸(BeanDefinition)"记录下来了,还没有真正new出对象。 -
实例化具体的策略类: Spring 开始根据图纸挨个
new对象。它发现RegularSalaryStrategy和InternSalaryStrategy没有依赖别人,于是非常痛快地把它们实例化完毕,并放进了一级缓存(单例池 singletonObjects)里。- 此时单例池里有了:
regularSalaryStrategy -> 实例对象A,internSalaryStrategy -> 实例对象B。
- 此时单例池里有了:
-
处理策略工厂(依赖注入发生在这里): 接着,Spring 准备实例化
SalaryStrategyFactory。它new出了工厂对象后,进入了生命周期的第二大步------属性赋值(Populate Properties)。-
Spring 看到工厂类里面写了
@Autowired private Map<String, SalaryStrategy> strategyMap;。 -
Spring 的反射机制开始工作(没错,这里用到了反射)。它去单例池里大喊一声:"谁实现了
SalaryStrategy接口?" -
刚才准备好的
实例对象A和实例对象B齐刷刷地站了出来。 -
Spring 就会自动构建一个 Map,把它们按
BeanName:实例的键值对存进去,然后利用反射(类似于Field.set())塞进工厂的strategyMap属性中。
-
-
启动完成,准备接客: 当 SpringBoot 完全启动并监听 8080 端口时,上述的 Map 早就已经在内存里组装好并严阵以待了。等到用户发起 HTTP 请求调接口时,程序只是在这个组装好的 Map 里执行了一次极其轻量的
get(key)操作而已。
@Autowired
private Map<String, SalaryStrategy> strategyMap;
写了这个就可以把策略实现类就可以自动注入了
Spring 容器在底层做了以下几件事:
1. 触发特殊解析机制
普通的 @Autowired 是去找名字 或者类型 匹配的单个 Bean。但当 Spring 的依赖注入器(AutowiredAnnotationBeanPostProcessor)发现你要注入的类型是一个 Map,并且 Key 的类型是 String 时,它会立刻触发"集合注入模式"。
2. 全局搜索目标接口
Spring 不会去容器里找一个叫 strategyMap 的 Map 对象。相反,它会去看Map 的 Value 泛型类型 。 它发现你要的是 SalaryStrategy,于是 Spring 会去单例池(Singleton Pool)里把所有实现了 SalaryStrategy 接口的 Bean 全部搜刮出来。
3. 自动组装键值对 (Key-Value)
Spring 搜集到这些实现类(比如 RegularSalaryStrategy 和 InternSalaryStrategy)之后,会自动把它们塞进你声明的这个 Map 里:
-
Key (键): 自动使用该 Bean 在 Spring 容器中的名字(比如 "regularSalaryStrategy")。
-
Value (值): 自动使用该 Bean 的实例化对象。
⚠️ 成功触发的两个硬性前提条件
虽然写起来爽,但必须满足两个条件,否则 Map 注入进来就是空的,或者直接报错:
-
实现类必须交给了 Spring 管理: 你的那些策略实现类上面,必须 加了
@Service或@Component注解。如果只是自己new出来的普通类,Spring 是感知不到的。 -
Map 的 Key 必须是 String 类型: 如果你写成
Map<Integer, SalaryStrategy>,Spring 就傻眼了,因为它不知道该用什么 Integer 来代表你的 Bean,自动注入就会失败。
既然 Map 可以这么玩,那 List 行不行? 当然行,而且在某些业务场景下比 Map 更好用!
如果你写成这样:
@Autowired
private List<SalaryStrategy> strategyList;
Spring 会把所有实现了该接口的 Bean 全部塞进这个 List 里。
这有什么用呢?------ 责任链模式 / 批量校验器! 假设你要做一个员工入职校验(校验身份证、校验学历、校验黑名单)。你可以写 3 个校验类实现同一个 Validator 接口。然后注入一个 List<Validator>。 在业务代码里,只需要一个 for 循环遍历这个 List,就能把所有校验规则挨个执行一遍。未来如果新增规则,只需要新建一个类并加上 @Component,原有代码一行都不用改!这种将"策略模式/责任链模式与 Spring 集合注入完美结合"的编码思维,正是高级后端工程师和普通 CRUD 工程师的分水岭。