Spring Boot 自动配置的3种设计模式,比 @Autowired 更值得搞懂

4 阅读1分钟

Spring Boot 最核心的能力是什么?大多数人会说是"约定优于配置"。但如果你翻过自动配置的源码,会发现这背后的设计远不止"约定"两个字那么简单。

自动配置本质上是在解决一个问题:在不确定运行环境的情况下,安全地创建合适的 Bean。这个问题涉及三个子问题——"要不要创建"、"创建什么"、"创建后通知谁",分别对应三种设计模式。

条件模式:Spring Boot 的"要不要创建"决策

Spring Boot 没有一个叫"条件模式"的 GoF 模式,但 @Conditional 系列注解就是条件模式的标准实现。它的本质是:把创建决策从代码逻辑变成声明式规则

// 自动配置类的典型写法
@AutoConfiguration
@ConditionalOnClass(DataSource.class)           // classpath 上有 DataSource 才生效
@ConditionalOnMissingBean(DataSource.class)      // 容器里没有 DataSource 才创建
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource(DataSourceProperties properties) {
        return DataSourceBuilder.create().build();
    }
}

这种写法跟你在业务代码里写的 if (xxx != null) 看似一样,但有一个根本区别:条件的评估时机不同

if-else 在运行时评估,每次调用都要判断。@Conditional 在容器启动时评估一次,评估结果决定了 Bean 是否注册,之后不再判断。这意味着条件模式把"分支决策"从方法级提升到了容器级。

我在一个项目里看到有人这么写:

@Service
public class PaymentService {
    @Autowired
    private AlipayClient alipayClient;     // 可能不存在
    @Autowired
    private WechatPayClient wechatPayClient; // 可能不存在

    public void pay(String channel, Order order) {
        if ("alipay".equals(channel)) {
            alipayClient.pay(order);  // NPE 风险
        } else if ("wechat".equals(channel)) {
            wechatPayClient.pay(order);  // NPE 风险
        }
    }
}

问题在于:用运行时 if-else 来做 Bean 存在性判断,跟自动配置的思路完全相反。正确的做法是用 @ConditionalOnProperty@ConditionalOnBean,让不存在的支付渠道在启动时就排除。

但这里有个陷阱:@ConditionalOnMissingBean 的评估顺序跟 @Configuration 类的加载顺序有关。如果你在自定义 Configuration 里手动声明了一个 DataSource,但加载顺序在自动配置之后,那自动配置可能已经先创建了默认的 DataSource。这是很多人遇到的"自动配置不生效"的根源。

工厂模式:SpringFactoriesLoader → AutoConfiguration

Spring Boot 2.x 用 SpringFactoriesLoader,3.x 用 ImportSelect + META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,但本质都是同一个模式:配置驱动的工厂

// SpringFactoriesLoader 的核心逻辑
public final class SpringFactoriesLoader {
    public static <T> List<T> loadFactories(Class<T> factoryType, ClassLoader classLoader) {
        // 1. 读 META-INF/spring.factories
        // 2. 找 factoryType 对应的实现类名
        // 3. 反射创建实例
        List<String> factoryNames = loadFactoryNames(factoryType, classLoader);
        List<T> result = new ArrayList<>(factoryNames.size());
        for (String factoryName : factoryNames) {
            result.add(instantiateFactory(factoryName, factoryType, classLoader));
        }
        return result;
    }
}

这个设计的精妙之处不在工厂本身,而在配置文件作为产品目录。传统的工厂模式是代码里硬编码产品列表,Spring Boot 把产品列表放到了 spring.factories 文件里。这意味着:

  • 加一个自动配置类,不需要改任何已有代码,只需要在 spring.factories 里加一行
  • 去掉一个自动配置类,在 application.ymlspring.autoconfigure.exclude 就行
  • starter 包的"引入即生效",本质上就是往 spring.factories 里注册了新的工厂产品

我在一个项目里看到有人自定义了一个 starter,但忘了在 spring.factories 里注册,结果自动配置根本不生效。debug 了半天才发现不是条件不满足,而是工厂压根不知道这个配置类的存在。

这种 bug 的特点:不报错,只是静默不生效。比报错更难排查。

观察者模式:自动配置后的事件通知

自动配置完成后,很多场景需要做初始化动作——建表、灌数据、检查连接。Spring Boot 用 ApplicationEvent 机制来做这件事,这就是观察者模式。

// 监听容器启动完成事件
@Component
public class DataInitializer implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 所有 Bean 都创建完了,可以安全地做初始化
        initDatabase();
    }
}

// 或者用 @EventListener(更现代的写法)
@Component
public class CacheWarmer {
    @EventListener(ApplicationReadyEvent.class)
    public void onReady() {
        // 应用完全就绪,开始预热缓存
        cacheManager.preload();
    }
}

关键区别:ContextRefreshedEvent 在 Bean 创建完后触发,ApplicationReadyEvent 在所有初始化(包括自动配置的后处理)完成后触发。如果你在 ContextRefreshedEvent 里访问自动配置创建的 Bean,可能还没完全初始化。

踩过一个坑:在 @PostConstruct 里访问自动配置的 RedisTemplate,结果拿到 null。原因是 @PostConstruct 在当前 Bean 初始化时触发,但自动配置的 Bean 可能还没创建。改用 @EventListener(ApplicationReadyEvent.class) 后问题解决。

这个问题的本质:观察者模式的时序很关键。同样的模式,触发时机差一步就是 null 和正常对象的区别。

三种模式的协作关系

把这三个模式放在一起看,Spring Boot 自动配置的架构就很清晰了:

  1. 工厂模式负责"从哪找配置类"——通过 spring.factories 或 imports 文件发现
  2. 条件模式负责"要不要创建"——通过 @Conditional 系列注解过滤
  3. 观察者模式负责"创建后做什么"——通过 ApplicationEvent 通知

三个模式各管一个阶段,互不干扰,但协作完成了一个完整的"发现→过滤→生效→通知"流程。这种分层设计比在一个类里 if-else 搞定所有逻辑要灵活得多。

但灵活是有代价的。自动配置最让人头疼的就是排查困难——一个 Bean 为什么没创建?是条件不满足?是工厂没发现?还是加载顺序问题?Spring Boot 提供了 --debug/actuator/conditions 端点来帮助排查,但说实话,第一次遇到的时候你大概率还是得翻源码。

一些实践建议

  1. 自定义自动配置时,条件越具体越好@ConditionalOnClass + @ConditionalOnMissingBean + @ConditionalOnProperty 三件套一起用,避免"不该创建的 Bean 被创建了"。

  2. @ConditionalOnMissingBean 的参数要精确。不要写 @ConditionalOnMissingBean 不带参数,这会导致任何同类型的 Bean 都阻止自动配置。用 valuename 精确指定。

  3. 自动配置类不要做重逻辑。自动配置的职责是"创建和装配",不是"执行业务"。重逻辑放到 @EventListener@PostConstruct 里。

  4. 排查自动配置问题的第一步永远是 /actuator/conditions。别上来就翻源码,先看条件评估报告。


我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个设计模式用漫画 + 答题的方式讲,感兴趣可以搜一下看看。