策略模式怎么实现,bean是在什么时候注入的

策略模式(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()(刷新上下文) 这一步。具体到细节层面,顺序是这样的:

  1. 扫描阶段 (BeanFactoryPostProcessor): Spring 容器启动时,会扫描你代码里所有的 @Service@Component 注解。它发现了 RegularSalaryStrategyInternSalaryStrategySalaryStrategyFactory。此时,它只是把它们的"图纸(BeanDefinition)"记录下来了,还没有真正 new 出对象。

  2. 实例化具体的策略类: Spring 开始根据图纸挨个 new 对象。它发现 RegularSalaryStrategyInternSalaryStrategy 没有依赖别人,于是非常痛快地把它们实例化完毕,并放进了一级缓存(单例池 singletonObjects)里。

    • 此时单例池里有了:regularSalaryStrategy -> 实例对象A, internSalaryStrategy -> 实例对象B
  3. 处理策略工厂(依赖注入发生在这里): 接着,Spring 准备实例化 SalaryStrategyFactory。它 new 出了工厂对象后,进入了生命周期的第二大步------属性赋值(Populate Properties)

    • Spring 看到工厂类里面写了 @Autowired private Map<String, SalaryStrategy> strategyMap;

    • Spring 的反射机制开始工作(没错,这里用到了反射)。它去单例池里大喊一声:"谁实现了 SalaryStrategy 接口?"

    • 刚才准备好的 实例对象A实例对象B 齐刷刷地站了出来。

    • Spring 就会自动构建一个 Map,把它们按 BeanName:实例 的键值对存进去,然后利用反射(类似于 Field.set())塞进工厂的 strategyMap 属性中。

  4. 启动完成,准备接客: 当 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 搜集到这些实现类(比如 RegularSalaryStrategyInternSalaryStrategy)之后,会自动把它们塞进你声明的这个 Map 里:

  • Key (键): 自动使用该 Bean 在 Spring 容器中的名字(比如 "regularSalaryStrategy")。

  • Value (值): 自动使用该 Bean 的实例化对象

⚠️ 成功触发的两个硬性前提条件

虽然写起来爽,但必须满足两个条件,否则 Map 注入进来就是空的,或者直接报错:

  1. 实现类必须交给了 Spring 管理: 你的那些策略实现类上面,必须 加了 @Service@Component 注解。如果只是自己 new 出来的普通类,Spring 是感知不到的。

  2. 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 工程师的分水岭。