新同事用策略模式重构了满是if-else的优惠券系统,第二天却遇到了麻烦。为了处理10种优惠类型,他定义了10个策略类、1个工厂类,并且都加上了@Component注解。Spring启动时,几十个策略Bean瞬间加载,导致测试环境内存告警。这绝非个例——许多开发者在用策略模式‘优化’代码时,无意中制造了Spring容器管理的灾难。本文将彻底解决这个问题,让你既能享受策略模式的优雅,又能保持Spring容器的清爽。
一、问题的根源:当“优雅”遇上“框架”
用策略模式替换多重if-else时,代码整洁性和扩展性确实提升了。但这种美好可能止步于Spring容器启动日志的刷屏:
Creating shared instance of singleton bean 'vipDiscountStrategy'
Creating shared instance of singleton bean 'newUserDiscountStrategy'
...
// 还有十多个类似的日志
在Spring的IoC容器中,每个被@Component、@Service标记的类都会在应用启动时被实例化为单例Bean。当策略数量激增时,问题随之而来:
- 启动时间变长:每个Bean的创建都涉及反射、代理等过程。
- 内存占用增加:即使某些策略很少使用,它们仍占用堆内存。
- Bean定义污染:Spring配置变得臃肿,可读性下降。
- 测试复杂度上升:单元测试需要加载整个应用上下文。
二、三个典型的“灾难”现场
场景一:电商优惠券系统
@Service
public class CouponService {
@Autowired
private Map<String, CouponStrategy> strategyMap; // 自动注入所有策略Bean
public BigDecimal applyCoupon(String couponType, BigDecimal amount) {
CouponStrategy strategy = strategyMap.get(couponType);
if (strategy == null) {
throw new IllegalArgumentException("不支持的优惠券类型");
}
return strategy.calculate(amount);
}
}
// 每个优惠类型对应一个Spring Bean
@Component("vipCoupon")
public class VipCouponStrategy implements CouponStrategy { /* 实现 */ }
@Component("newUserCoupon")
public class NewUserCouponStrategy implements CouponStrategy { /* 实现 */ }
// ... 至少10个以上的策略类
问题诊断:20种优惠类型即对应20个Spring Bean,若策略类还依赖其他Service,会形成复杂的Bean依赖网。
场景二:支付渠道路由
@Component
public class PaymentRouter {
@Autowired
private List<PaymentProcessor> processors; // 注入所有支付处理器Bean
public PaymentResult process(PaymentRequest request) {
for (PaymentProcessor processor : processors) {
if (processor.supports(request.getChannel())) {
return processor.process(request);
}
}
throw new UnsupportedPaymentChannelException();
}
}
// 每个支付渠道一个Bean
@Component
public class AlipayProcessor implements PaymentProcessor { /* 实现 */ }
@Component
public class WechatPayProcessor implements PaymentProcessor { /* 实现 */ }
// ... 国内外至少15个支付渠道
问题诊断:策略模式与责任链模式混用,但每个处理器都是Spring Bean,增加新渠道需确保其被正确注入List。
场景三:风控规则引擎
@Service
public class RiskControlEngine {
@Autowired
private ApplicationContext context; // 直接持有容器上下文
public RiskResult evaluate(User user, Order order) {
List<RiskRule> rules = loadRulesByScene(order.getScene());
return rules.stream()
.map(rule -> {
// 运行时从容器动态查找Bean
RiskRuleHandler handler = context.getBean(rule.getHandlerName());
return handler.evaluate(user, order, rule);
})
.filter(RiskResult::isBlocked)
.findFirst()
.orElse(RiskResult.pass());
}
}
// 每个风控规则一个处理器Bean
@Component("blacklistHandler")
public class BlacklistRuleHandler implements RiskRuleHandler { /* 实现 */ }
// 一个成熟的风控系统,规则处理器轻松超过30个
问题诊断:不仅每个策略是Bean,还用ApplicationContext.getBean()动态查找,丧失了依赖注入的编译时检查优势,引入了运行时依赖。
三、解决方案一:策略工厂 + 手动实例化
核心思想:让策略类回归POJO本质,不交给Spring管理。
// 策略接口
public interface DiscountStrategy {
BigDecimal calculate(BigDecimal amount);
}
// 策略实现:移除了 @Component 注解!
public class VipDiscountStrategy implements DiscountStrategy {
@Override
public BigDecimal calculate(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.8"));
}
}
// 策略工厂,负责创建和管理策略实例
@Component
public class DiscountStrategyFactory {
private final Map<String, DiscountStrategy> strategyCache = new ConcurrentHashMap<>();
private final UserService userService; // 工厂可以依赖Spring Bean
public DiscountStrategyFactory(UserService userService) {
this.userService = userService;
}
public DiscountStrategy getStrategy(String type) {
return strategyCache.computeIfAbsent(type, this::createStrategy);
}
private DiscountStrategy createStrategy(String type) {
switch (type) {
case "VIP":
// 创建时传入所需依赖
return new VipDiscountStrategy(userService);
case "NEW_USER":
return new NewUserDiscountStrategy();
// ... 其他策略
default:
throw new IllegalArgumentException("未知的策略类型: " + type);
}
}
}
核心要点:策略类完全脱离Spring容器管理,由工厂控制实例化时机和生命周期。
优点:
- Spring容器中仅工厂一个Bean,极大减轻负担。
- 可享受对象池、缓存等优化。
- 完全控制策略的创建过程。
缺点:
- 工厂中的
switch-case像是if-else的变体。
- 新增策略需修改工厂类,违反开闭原则。
- 策略类无法直接使用Spring AOP、事务等特性。
适用场景:策略数量有限(<10),逻辑简单,不需要复杂Spring特性的场景。
四、解决方案二:注解扫描 + 自动注册
想要Spring的便利又避免Bean膨胀?此方案可能是首选。
// 1. 自定义策略注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Strategy {
String value();
}
// 2. 策略实现:使用自定义注解
@Strategy("alipay")
public class AlipayHandler implements PaymentHandler {
private final RiskService riskService; // 通过构造器注入依赖
public AlipayHandler(RiskService riskService) {
this.riskService = riskService;
}
// ... 实现方法
}
// 3. 策略注册中心
@Component
public class StrategyRegistry implements ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
private final Map<String, PaymentHandler> handlerMap = new ConcurrentHashMap<>();
@Override
public void afterPropertiesSet() {
// 扫描所有带@Strategy注解的类并注册
Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(Strategy.class);
for (Object bean : beansWithAnnotation.values()) {
if (bean instanceof PaymentHandler) {
Strategy annotation = bean.getClass().getAnnotation(Strategy.class);
handlerMap.put(annotation.value(), (PaymentHandler) bean);
}
}
}
// ... 获取方法
}
// 4. 配置类:显式声明策略Bean(集中管理)
@Configuration
public class StrategyConfiguration {
@Bean
@Strategy("alipay")
public PaymentHandler alipayHandler(RiskService riskService) {
return new AlipayHandler(riskService); // 享受依赖注入
}
// ... 声明其他策略Bean
}
核心要点:通过自定义注解和注册中心,既利用Spring的依赖注入,又避免策略类自动成为单例Bean。
优点:
- 策略类可享受Spring依赖注入。
- 配置集中,所有策略清晰可见。
- 符合开闭原则,新增策略只需添加新
@Bean定义。
缺点:
- 仍需为每个策略声明
@Bean方法。
- 配置类可能变得庞大。
适用场景:策略数量中等(10-30),需要Spring依赖注入,且希望保持配置显式声明的场景,常见于支付路由等系统。
五、解决方案三:动态注册 + 条件化Bean(高级方案)
适用于策略数量多、变化频繁的大型系统。
// 1. 策略类:纯粹的POJO,无任何Spring注解
public class AgeRuleEngine extends AbstractRuleEngine {
public AgeRuleEngine(RuleConfig ruleConfig, MetricsCollector metricsCollector) {
super(ruleConfig, metricsCollector);
}
@Override
public String getRuleType() {
return "AGE_VALIDATION";
}
// ... 具体业务逻辑
}
// 2. 核心:动态Bean注册器
@Component
public class DynamicStrategyRegistrar implements BeanDefinitionRegistryPostProcessor {
@Value("${rule.strategies.packages:com.example.rules}")
private String[] scanPackages;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AssignableTypeFilter(RuleEngine.class));
for (String basePackage : scanPackages) {
Set<BeanDefinition> candidates = scanner.findCandidateComponents(basePackage);
for (BeanDefinition beanDefinition : candidates) {
// 动态注册BeanDefinition,并设置为原型(SCOPE_PROTOTYPE)作用域
GenericBeanDefinition dynamicBeanDef = new GenericBeanDefinition();
dynamicBeanDef.setBeanClass(Class.forName(beanDefinition.getBeanClassName()));
dynamicBeanDef.setScope(BeanDefinition.SCOPE_PROTOTYPE); // 关键!
registry.registerBeanDefinition(generateBeanName(clazz), dynamicBeanDef);
}
}
}
}
// 3. 策略工厂:按需获取原型Bean
@Component
public class RuleEngineFactory {
private final ConfigurableListableBeanFactory beanFactory;
private final Map<String, Class<? extends RuleEngine>> strategyTypes = new ConcurrentHashMap<>();
public RuleEngine getRuleEngine(String ruleType) {
Class<? extends RuleEngine> engineClass = strategyTypes.get(ruleType);
// 每次获取都通过beanFactory创建新实例(原型)
return beanFactory.createBean(engineClass, AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR, false);
}
}
核心要点:结合BeanDefinitionRegistryPostProcessor实现策略类的动态发现和注册,并使用原型作用域避免单例堆积。
优点:
- 策略类完全解耦,无需Spring注解。
- 支持热更新策略映射。
- 原型作用域避免内存常驻,适合规则多变场景。
缺点:
- 实现复杂,需深入理解Spring机制。
- 调试困难,存在过度设计风险。
适用场景:大型风控系统、规则引擎、插件化架构,策略数量多(>30)且需动态更新的复杂系统。
六、方案对比与选择指南
| 指标 |
方案一(工厂+POJO) |
方案二(注解+注册) |
方案三(动态注册) |
| 启动时间 (50策略) |
~1.2s |
~2.8s |
~3.5s |
| 内存占用 |
最低 |
中等 |
中等 |
| 代码复杂度 |
简单 |
中等 |
复杂 |
| 扩展性 |
较差 |
好 |
优秀 |
| 可测试性 |
优秀 |
良好 |
一般 |
| 学习成本 |
低 |
中 |
高 |
选择建议:
- 策略数量 < 10,逻辑简单:选方案一。简单直接,易于维护。
- 策略数量 10-30,需要Spring特性:选方案二。平衡便利性与性能。
- 策略数量 > 30,或需热更新:选方案三。提供最大灵活性。
- 开发框架或中间件:推荐方案三,为使用者提供高灵活性。
七、应对策略依赖的进阶思考
问题:“如果策略本身也需要依赖数据库或外部服务,如何处理?”
答:应区分“纯业务策略”和“有依赖策略”。对于后者,不应让策略类直接@Autowired依赖,而是:
- 由工厂/上下文注入:在工厂创建策略实例时,将所需的DAO或Client作为构造参数传入。
- 使用‘策略上下文’:策略接口方法中增加一个
StrategyContext参数,上下文内包含所有可能需要的工具类、数据访问对象。
这样做保持了策略类的可移植性和可测试性,它不绑定Spring环境,可在任何地方实例化和测试。
// 策略上下文持有依赖
public class StrategyContext {
private final UserDao userDao;
private final ExternalServiceClient externalClient;
// ... 构造器与getter
}
// 策略实现:依赖通过context传入
public class UserLevelStrategy implements ComplexStrategy {
@Override
public Result execute(StrategyContext context, Input input) {
User user = context.getUserDao().findById(input.getUserId());
// 使用context中的依赖
return processUserLevel(user, context);
}
}
实战总结
✅ 原则一:评估策略数量与稳定性。少量稳定直接用@Component;量多易变则考虑轻量化管理。
✅ 原则二:优先让策略类做“无状态POJO”,将生命周期管理和依赖查找交给专门的“策略注册中心”。
✅ 技巧一:善用ApplicationContextAware+InitializingBean接口,实现策略的“自注册”。
✅ 技巧二:考虑使用Map<String, Class<?>>配置式绑定,利用反射实例化,实现策略“热插拔”。
✅ 避坑点:避免在策略类中直接注入大量其他Spring Bean。必须的依赖,应通过构造器参数由工厂传入,保持策略类的纯净与可测试性。
记住:设计模式是工具,不是教条。真正的优雅不在于使用了多少模式,而在于在框架约束与业务需求之间找到最佳平衡点。当你下次重构那些if-else时,不妨先问自己:这些策略,真的都需要成为Spring Bean吗?