条件装配全解:@Conditional 十连问
先说人话。条件装配就是「这个对象到底要不要造出来,让容器在启动时替你判断」。你不用写 if (环境是生产) { ... } 这种散在各处的分支,只要往配置类或 @Bean 方法上贴一个注解,容器就会在造它之前先问几句:这个类在不在依赖里?那个 Bean 有没有?开关属性开了没?用户自己配过没?四问全过,才把它注册进容器;任何一问不过,就当它不存在。关键在于:条件不满足时不会有任何报错——对象就是安静地没了。这也是本篇最难的地方:不是语法,而是「怎么知道它为什么没生效」。
要用这套机制,你必须先分清三个词。条件注解(@ConditionalOnXxx)是贴在类或方法上的那张「准入贴纸」;条件类(实现 Condition 接口的那个类)是真正做判断的人,它只有一个方法 matches,返回 true 就放行;求值阶段是这个判断发生在哪一刻——解析配置类时,还是注册某个 Bean 时。三者关系一句话:贴纸负责声明「用哪条规矩」,条件类负责执行这条规矩,阶段决定它什么时候有资格开口。
精装房的水电家具都是提前配好的,但你一旦把自己的沙发摆进客厅,装修方就不会再往那儿放它的沙发。条件装配就是这套「先看你自己有没有安排,没有我才补」的规矩:@ConditionalOnMissingBean——客人没带伞,店里才借一把;你自己带了伞,店员看一眼就把伞架收回去。反过来,@ConditionalOnClass 像餐厅的「这道菜要用的食材今天到货了才上」,@ConditionalOnProperty 像「这盏灯要靠墙那个开关打开才亮」。记住这四句话,下面所有注解都只是它们的变体。

上面这张图给的是全景,但「顺序」这件事光看不够——四问里第 ③ 和第 ④ 长得很像,记反的人一大半。把它摊成能点的路径,一格一格走:
学完这一篇,你要能回答三个问题:
- 同样是「看容器里有没有某个 Bean」,为什么写在自动配置类上稳如泰山、写在你自己的
@Configuration上就可能失效? @ConditionalOnProperty("my.feature")只写名字不写值时,属性「不写」和写false,结果一样吗?- 一个贴着条件注解的 Bean 莫名其妙消失了,我该敲哪个参数让 Boot 亲口告诉我原因?
在讲语法之前,先回答一个更根本的问题:为什么不能直接写死配置,非要多出一套「条件」机制? 因为同一份代码,往往要在不同的环境、不同的依赖组合下表现出不同的行为。下面三个场景,几乎每个后端都遇到过:
- 场景一:开发用 H2,生产用 MySQL。 团队想在本地跑测试时用内存数据库 H2,部署到生产时换成 MySQL。如果配置文件写死一个数据源,就要维护两套代码;理想状态是「同一份代码,检测到 classpath 里有 H2 就用 H2」
- 场景二:可选依赖要能优雅降级。 某个模块依赖 Redis 做缓存,但有些边缘服务不部署 Redis。这时应该「有 Redis 就用 Redis,没有就退回本地
ConcurrentHashMap」,而不是启动直接报错 - 场景三:写 starter 时给默认实现,同时允许用户替换。 你的 starter 要提供一个默认的
HttpClient,但如果使用方自己定义了同类型的 Bean,就应该让用户的实现生效
这三个场景的共同点是:「要不要装配,取决于运行时的环境与依赖,而不是写在代码里的常量」。条件装配就是 Spring 给出的标准答案——它把「如果……就……」这套判断,从 if 语句搬到了注解上,让容器在启动时替你做决策。
而「运行时的依赖」这件事,第一处真相不在注解里,在 pom 里。自己勾一遍就明白了:把 H2 那一行取消勾选,DataSourceAutoConfiguration 里靠 @ConditionalOnClass 守着的那批 Bean 会整批消失,而启动日志一个字都不会变。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version> <!-- 版本由 BOM 统管,子依赖不写 version -->
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>所有条件注解,最后都归结到一个元注解:@Conditional。它本身不带任何判断逻辑,只负责绑定一个条件类:
@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Conditional { Class<? extends Condition>[] value();}真正做判断的是 Condition 接口——注意它只有一个方法,返回一个布尔值:
@FunctionalInterfacepublic interface Condition { boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);}context:ConditionContext,是条件类在运行时能摸到的「整个世界」。它能拿到BeanFactory(查已有 Bean)、Environment(查配置项和 profile)、ResourceLoader(查资源)、ClassLoader(查类是否存在),以及BeanDefinitionRegistry(查 BeanDefinition)metadata:AnnotatedTypeMetadata,让你读到「被标注的类/方法上那个注解的属性值」。比如@ConditionalOnProperty(name = "x", havingValue = "y")里的name和havingValue就是从这里取的
普通的 Condition 只管「返回 true 还是 false」,但 Spring 内部还有一层更精细的设计——ConfigurationCondition,它把求值拆成了两个阶段:
public interface ConfigurationCondition extends Condition { ConfigurationPhase getConfigurationPhase(); enum ConfigurationPhase { PARSE_CONFIGURATION, // 解析配置类时求值:决定这个类要不要被解析 REGISTER_BEAN // 注册 Bean 时求值:决定这个 @Bean 要不要注册 }}| 阶段 | 求值时机 | 决定什么 | 典型注解 |
|---|---|---|---|
PARSE_CONFIGURATION | 解析配置类时 | 整个 @Configuration 类要不要被解析 | @ConditionalOnClass |
REGISTER_BEAN | 注册 Bean 时 | 单个 @Bean 方法要不要注册 | @ConditionalOnMissingBean |
为什么条件要分两个阶段?因为这两类判断依赖的信息不同。@ConditionalOnClass 只看 classpath,解析类之前就能判定,放在 PARSE 阶段可以省掉整个类的解析开销;而 @ConditionalOnMissingBean 需要知道「别人注册了没有」,必须等到 Bean 注册阶段、别的定义都已经进场,它才有资格开口。阶段不同,是因为它们能看到的「信息」出现的时机不同。
上一条讲的是「什么时候能问」,那「能问到什么」呢?ConditionContext 一共给了五样东西,哪一种条件能用哪一样,是有硬对应的。背不住,来玩一局:先点左边的条件类,再点它唯一能问到的东西。
Spring Boot 在 @Conditional 之上封装了一整套开箱即用的条件注解。它们按「看什么」分成两大家族:
| 注解 | 判断依据 | 典型用途 |
|---|---|---|
@ConditionalOnClass | classpath 存在指定类 | 有依赖才装配(HikariCP、Redis) |
@ConditionalOnMissingClass | classpath 不存在指定类 | 排除某个实现 |
@ConditionalOnBean | 容器内已有指定类型 Bean | 依赖另一个 Bean 才能装配 |
@ConditionalOnMissingBean | 容器内没有指定类型 Bean | 用户没配时的默认兜底 |
@ConditionalOnSingleCandidate | 该类型只有一个候选 Bean | 唯一数据源、唯一事务管理器 |
@ConditionalOnProperty | 配置项等于 / 不等于某值 | 功能开关 |
@ConditionalOnResource | 指定资源存在 | 有 classpath:xxx.yml 才装配 |
@ConditionalOnWebApplication | 当前是 Web 应用 | Web 专属自动配置 |
@ConditionalOnNotWebApplication | 当前不是 Web 应用 | 非 Web 专属自动配置 |
@ConditionalOnExpression | SpEL 表达式为真 | 需要组合的复杂条件 |
@ConditionalOnJava | JDK 版本在某范围内 | 版本相关的兼容逻辑 |

@ConditionalOnProperty 有个高频易错点——默认情况下它要求属性存在且不等于 false。也就是说 @ConditionalOnProperty("my.feature") 只写了名字时,my.feature=false 会让条件不成立,但属性完全不写时也不成立。想表达「不写就默认开启」,要写 matchIfMissing = true。
条件注解不是各判各的、互不相干。它们之间有时序依赖,最典型的就是 @ConditionalOnBean:它之所以能判断「容器里有没有某个 Bean」,前提是那个 Bean 的 BeanDefinition 已经被注册。顺序错了,它就会看到「还没有」,从而做出错误判断。
这带来一个反直觉的结论:同一个条件注解,放在不同的位置、求值的时机不同,结果可能不同。 Spring Boot 用三套机制来保证顺序可控:
// 1. 用户优先:AutoConfigurationImportSelector 是 DeferredImportSelector,// 自动配置类永远排在用户自己的 @Configuration 之后处理// → 所以 OnMissingBean 总能看见你定义的 Bean// 2. 自动配置之间:用 before / after 排依赖@AutoConfiguration(after = DataSourceAutoConfiguration.class)@ConditionalOnBean(DataSource.class)public class JpaAutoConfiguration { }// 3. 绝对顺序兜底:@AutoConfigureOrder(数字越小越靠前)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 100)public class MyFirstAutoConfiguration { }
- 用户优先是整套设计的地基:正因为
DeferredImportSelector把自动配置推迟到用户配置之后,@ConditionalOnMissingBean才有资格「让路」 - before / after 解决的是自动配置之间的顺序,成对声明、不允许成环
- @AutoConfigureOrder 用绝对数字兜底,官方建议优先用相对的前两个
「用户优先」这四个字落到 @ConditionalOnMissingBean 上,就是一次有先后顺序的让路。这段动画把它拆成六步,注意第 ⑤ 帧——默认值被收回去的时候,全程没有任何报错:

在同一个 @Configuration 类里,不要依赖两个 @Bean 方法的声明顺序去让其中一个 @Bean 带上 @ConditionalOnBean 判断另一个。同类内 @Bean 方法之间的处理顺序并不保证(受方法签名、代理等影响),这种写法非常脆弱。要表达依赖,请把这些 Bean 拆到不同的配置类,并用 before / after 明确排序。
把这条坑画成一张对照图,小白只要记住「这个注解该贴在哪扇门后」:

挂号窗口只在大厅开放后才有效。@ConditionalOnBean 像医院挂号台问你「你要看的这位医生今天到岗了吗」——这个问题只有在排班表贴出来之后才答得准。自动配置就是那张排班表:容器先把你自己的科室登记完(用户优先),再按 before / after 把自动配置的科室逐个排好,此时问「某位医生在不在」才有确定答案。而把它贴在业务配置类上,等于在早上六点、排班表还没贴时问同样的话:有人已经来上班了就说「在」,没人来就说「不在」,答案取决于谁先到,而不是事实本身。
一句话判据:@ConditionalOnBean / @ConditionalOnMissingBean 只往自动配置类和 starter 里写;你自己的业务配置想「看情况装配」,用 @Profile 或 @ConditionalOnProperty。
判据好说,「一个 Bean 到底是在哪一拍消失的」就没有那么好记了。把它摊成一次单步执行:左边六行是要走的代码,右边同步刷新此刻的变量和调用栈。连点「下一步」,重点看第 ⑤ 拍——beanDefinitionCount 少 1 的那一拍,屏幕上什么异常都没有:
@SpringBootApplication // 其中的 @EnableAutoConfiguration 登记一个 DeferredImportSelectorSpringApplication.run(...) // 先解析用户自己的 @Configuration// 用户配置全部解析完,自动配置类才进场(这就是「延迟导入」)@ConditionalOnBean(DataSource.class) // 我写在业务配置类上的这一问matches() -> registry.containsBeanDefinition("dataSource") // 此刻查一次// 条件为 false:不注册、不报错,只在报告里留一行 Did not match| 刚读到 | @EnableAutoConfiguration |
| 登记的导入器 | DeferredImportSelector(延迟) |
| 已注册定义数 | 0 |
SpringApplication.run@Import(AutoConfigurationImportSelector)理解了 Condition 接口,就可以自己造一个条件注解。下面这个「只在星期几生效」的例子虽然趣味,但用到的每个细节都是生产级的:
@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Conditional(OnDayOfWeekCondition.class) // 绑定条件类public @interface ConditionalOnDayOfWeek { DayOfWeek[] value();}public class OnDayOfWeekCondition implements ConfigurationCondition { // 放在 REGISTER_BEAN 阶段,和其他 Bean 条件保持一致 @Override public ConfigurationPhase getConfigurationPhase() { return ConfigurationPhase.REGISTER_BEAN; } @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { // 1. 从注解上取属性值 Map<String, Object> attrs = metadata .getAnnotationAttributes(ConditionalOnDayOfWeek.class.getName()); DayOfWeek[] days = (DayOfWeek[]) attrs.get("value"); // 2. 从 Environment 取「今天」,方便测试时覆盖 Environment env = context.getEnvironment(); String override = env.getProperty("demo.day-of-week"); DayOfWeek today = override != null ? DayOfWeek.valueOf(override.toUpperCase()) : LocalDate.now().getDayOfWeek(); // 3. 也可以顺带看一眼已有 Bean(演示 ConditionContext 的能力) boolean hasReportService = context.getBeanFactory() != null && context.getBeanFactory().getBeanNamesForType(ReportService.class).length > 0; return Arrays.asList(days).contains(today) && !hasReportService; }}@Configuration@ConditionalOnDayOfWeek(DayOfWeek.MONDAY) // 只在周一装配public class MondayReportConfig { @Bean public ReportJob reportJob() { return new ReportJob(); }}metadata.getAnnotationAttributes(...):读注解属性,value()就是@ConditionalOnDayOfWeek(DayOfWeek.MONDAY)里的内容context.getEnvironment():拿到配置环境,读取demo.day-of-week可以在测试里把「今天」捏成任意一天,让条件变得可测context.getBeanFactory():查容器里已有的 Bean,这正是@ConditionalOnBean内部做的事
提示:ConditionContext 一共能拿到五样东西——BeanFactory、Environment、ResourceLoader、ClassLoader、BeanDefinitionRegistry。写自定义条件时,这五样基本覆盖了「判断所需的一切信息」。面试问「自定义条件注解怎么写」,答出「实现 Condition、覆写 matches、用 @Conditional 绑定」即可,再补充 ConfigurationCondition 两阶段就是加分项。
很多人以为 @Profile 是另一套独立机制,其实它只是条件装配的一个「官方预设」。看它的定义就一目了然:
@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Conditional(ProfileCondition.class) // 本质还是 @Conditionalpublic @interface Profile { String[] value();}ProfileCondition 做的事很朴素:读取 spring.profiles.active / spring.profiles.default,看当前激活的 profile 是不是落在注解声明的列表里。所以「按 profile 切换 Bean」和「按条件切换 Bean」在机制上是同一件事——@Profile 是 @Conditional 的一个特例。理解了这一点,你就能把它们放在同一套心智模型里,而不是记两套互不相干的规则。
条件装配最大的难点是「静默」——条件不满足时,Bean 悄无声息地消失,没有任何报错。所以调试能力比语法本身更重要。第一手段还是开启条件评估报告:
java -jar app.jar --debug报告里每一行都告诉你是哪个条件类(括号里的 OnClassCondition 等)做出的判断:
Positive matches:----------------- RedisAutoConfiguration#redisTemplate matched: - @ConditionalOnClass found required class 'org.springframework.data.redis.core.RedisOperations' (OnClassCondition) - @ConditionalOnMissingBean (names: redisTemplate) did not find any beans (OnBeanCondition) - @ConditionalOnProperty (spring.data.redis.host) matched (OnPropertyCondition)Negative matches:----------------- MongoAutoConfiguration: Did not match: - @ConditionalOnClass did not find required class 'com.mongodb.client.MongoClient' (OnClassCondition)- 看
Positive matches确认「我以为会生效的,到底生效了没有」 - 看
Negative matches里的Did not match:列表,逐条比对是哪个条件把你拦下了——是类没在 classpath(依赖没加全),还是属性没配对(配置写错),一眼便知 - 甚至可以在代码里直接拿到这份报告对象:
ConditionEvaluationReport.get(beanFactory),用于把装配结果输出到自己的运维面板
这套「读报告 → 找那一行 → 对症下药」的动作值得单独走一遍,因为新手最常见的卡点不是不会写条件,而是看到 Bean 消失之后不知道从哪查起:

光有路线还不够,得在真堆栈上练一次。下面这段就是线上最常出现的那种「代码一行没改、换台机器就炸」,先别看解析——点出你认为的凶手行:
本地跑得好好的报表任务,交到同事机器上启动就失败。你确认代码一行没动,只是他的 pom 里少了一个依赖。
这个注解最容易用错,有两个坑必须提前知道:
- 坑一:它只能看见「已注册」的 Bean。
@ConditionalOnBean判断的是容器里已经注册的 BeanDefinition,而不是「将来会出现」的 Bean。所以它和「谁先注册」强绑定——这也是第四节反复强调顺序的原因。在自动配置类之间用它,务必配合before/after - 坑二:条件里不要做耗时操作。
matches会被反复求值(同一个条件在一次启动里可能被调用很多次),而且它是在启动的关键路径上同步执行的。在里面查数据库、发起网络请求、解析大文件,会直接拖慢甚至卡死启动。条件应该是快、纯、无副作用的
不要写「读远程配置中心来判断条件」这类逻辑。条件求值的时机非常早,此时很多基础设施(连接池、注册中心客户端)本身还在装配中,你很可能拿到一个还没初始化好的组件,导致启动进入不确定状态,排查起来极其痛苦。
下面这个演示把条件求值做成了可切换的开关。试着切换「用户自定义 DataSource」与「classpath 有 spring-jdbc」这两个条件,观察装配结果如何随之改变——这正是 @ConditionalOnMissingBean 与 @ConditionalOnClass 在真实工作中的配合方式:
前面写的都是文字结论。下面五个内核实验请按顺序跑完,每个大约 30 秒,它们分别对应本篇的五条主线:先看单个条件怎么判,再看整条装配链,再往下看「条件到底在判什么东西」,然后把这套规矩用回 starter,最后回头确认「一个对象究竟是怎么进容器的」。
第一个实验单独盘问四种条件。先点 onclass(依赖在不在),再点 onprop(开关开没开),然后点 onbean 与 missing 对照着看——前者问「有没有」,后者问「你有没有自己配」,这一对最容易记反:
第二个实验把这些条件放回自动配置的流水线上。按 imports → filter → sort → apply → report 点一遍,你会看到候选数量在哪一步掉得最狠,以及为什么同一个注解在不同阶段答案不同:
第三个实验回答一个更底层的问题:条件判的到底是什么?答案是 BeanDefinition——容器在真正造对象之前先写好的一份「配方」(描述这个 Bean 该怎么造的元数据对象,不是 Bean 本身)。看清这一层,你就明白条件装配拦的是「要不要写这份配方」,而不是「要不要调构造器」:
第四个实验把整套规矩用回你自己的 starter:清单登记 → 属性绑定 → 生成 Bean → 被你关掉。走完前三站一定要点 off,看清楚 exclude 和开关属性这两条「关掉它」的路有什么区别:
第五个实验回答一个更前置的问题:条件判的是「要不要写这份配方」,那配方到底有几条来源?依次点 scan、bean、import、auto,四条路(组件扫描、@Bean 方法、@Import 直送、自动配置登记)会各自打出它在哪个阶段把 BeanDefinition 塞进注册表;最后点 miss——包没被扫到的现场,报错措辞和被条件拦下几乎一模一样,这正是第七节那段堆栈里最需要分辨的一件事:
五个实验的共同结论是——条件求值发生在「还没造对象」的时候。所以它快、便宜,也正因为它发生得这么早,很多你熟悉的东西(连接池、远程配置)在此刻根本还不存在,这就是第八节那条警告的由来。
实验按完了,换成命令行自己敲。下面这台控制台连着浏览器里的同一个内核,回显全部由内核算出来——先 boot 建容器,再逐条翻开关:
cond jdbcOnClasspath false 之后必须再敲一次 beans 才看得出差别——cond 会顺手重建容器,但它只回显新开关值,不替你列清单。这一串动作就是第七节「改条件 → 看报告」的手工版。
现在轮到你自己当一回容器。左边拨三个开关,右边立刻给出启动结果和 --debug 报告里的那一行判决。建议先把三格都调到默认值读一遍,再每次只改一格——这是理解条件装配最快的路径:
Positive matches: CacheAutoConfiguration#cacheService matched- @ConditionalOnClass found required class 'redis.clients.jedis.Jedis' (OnClassCondition)- @ConditionalOnProperty (cache.redis.enabled) matched (OnPropertyCondition)- @ConditionalOnMissingBean did not find any beans (OnBeanCondition)容器里有 1 个 CacheService:自动配置的那个
最后一格是最容易被忽略的组合。@ConditionalOnMissingBean 保证的是「用户先注册的那一个能顶掉默认值」,它不负责帮你清理重复定义。真的出现两个同类型 Bean,报错的是注入方而不是容器装配阶段——记住那句 required a single bean, but 2 were found,它就是本节要找的目标。
先来一道热身题,考的就是第三节那条提示里的默认值陷阱:
再来一道综合题,把第四、八节和那张 vs 图串起来:
下面每一行的「报错原文」都可以整段复制去搜索,别意译、别缩写。条件装配的坑有个共同特征:大多不以红字异常出现,而是「东西凭空少了」。
| 报错原文(片段) | 真实原因 | 30 秒自救 | 深挖看第几篇 |
|---|---|---|---|
@ConditionalOnBean 明明写了,Bean 却根本没生成(也没有任何报错) | 求值时机不对:被判断的那个 Bean 的定义还没注册进来,条件就看到「没有」。写在业务配置类里尤其容易踩 | 把它挪进自动配置类并配 before / after;或者干脆换成 @ConditionalOnProperty 做显式开关 | 本篇第四、八节 |
NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.ReportJob' available | 贴着条件注解的类压根没被注册——条件没过,静默消失 | 加 --debug 重启,在 Negative matches 里找这个类的 Did not match: 后面那行,它会写明是哪个条件拦的 | 本篇第七节 · 第十二节沙盘 |
java.lang.NoClassDefFoundError: org/springframework/data/redis/core/RedisOperations | 编译期这个类在(provided 或 IDE 里能看到),运行期对应的 jar 不在 classpath 上 | 确认依赖是否 <optional> / <scope>provided</scope> 被排掉了;mvn dependency:tree 核对;这正是 @ConditionalOnClass 要防的事故 | #2 Maven 依赖 · #18 自动配置 |
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'noticeService': Unsatisfied dependency expressed through constructor parameter 0 | 你要注入的那个 Bean 没被装配出来——通常是某个上游条件(属性开关、类缺失)把它拦掉了 | 顺着报错里的类型名去 --debug 报告搜它属于哪个自动配置;先确认开关属性和依赖 jar,再怀疑代码 | 本篇第十二节沙盘 · #6 IoC 与 DI |
Parameter 0 of constructor in xxx required a single bean, but 2 were found | 同类型出现多个候选:@ConditionalOnMissingBean 让位失败、或用户与自动配置各注册了一个 | 注入点加 @Qualifier("名字"),或给首选者标 @Primary;根治是把默认实现的条件写准 | 本篇第十二节 · #7 BeanDefinition |
@ConditionalOnProperty (my.feature) did not match (OnPropertyCondition) | 只写了名字没写 matchIfMissing = true,而属性完全不存在——不存在同样算不匹配 | 要么在 yml 里补上 my.feature: true,要么在注解上加 matchIfMissing = true | 本篇第三节提示条 |
CONDITIONS EVALUATION REPORT 里找不到我的自动配置类(连 Negative matches 都没有) | 该类没被登记进候选清单:imports 文件路径写错,或它是被 @ComponentScan 扫进来的普通配置类 | 核对 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports;确认里面写的是全限定类名 | #20 自定义 Starter |
搜报错时只搜冒号后第一段原文(例如 required a single bean, but 2 were found),命中率远高于搜整句——不同 Spring 版本会在后半句加话。
目标:造一个「只有开关打开才装配」的小应用,并用 --debug 报告亲眼确认两条路径的差异。全程只需要四个文件。
第一步,依赖只要 Boot 本体(不需要数据库、不需要 Redis):
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency></dependencies>第二步,主类 DemoApplication.java(放在 src/main/java/com/example/cond/DemoApplication.java):
package com.example.cond;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ConfigurableApplicationContext;@SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { ConfigurableApplicationContext ctx = SpringApplication.run(DemoApplication.class, args); System.out.println("featureRunner 在容器里吗 -> " + ctx.containsBean("featureRunner")); System.out.println("beanDefinitionCount = " + ctx.getBeanDefinitionCount()); }}第三步,被条件管束的配置类 FeatureConfig.java(与主类同包):
package com.example.cond;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration(proxyBeanMethods = false)public class FeatureConfig { @Bean @ConditionalOnProperty(prefix = "feature", name = "enabled", havingValue = "true") public Runnable featureRunner() { return () -> System.out.println("[feature] 我只在开关打开时存在"); }}第四步,配置文件 src/main/resources/application.yml:
feature: enabled: false # ← 先写 false,跑一次;再改成 true 跑第二次logging: level: root: info两次运行的预期输出(关键行照抄即可对照):
# 第一次:feature.enabled=falsefeatureRunner 在容器里吗 -> falsebeanDefinitionCount = 78# 第二次:feature.enabled=truefeatureRunner 在容器里吗 -> truebeanDefinitionCount = 79第五步,加上 --debug 再跑一次,把控制台开头那段条件报告翻到自己这一项:
java -jar target/demo-0.0.1-SNAPSHOT.jar --debug--debug 报告片段(第一次运行时,你的类是被扫描进来的普通配置类,所以它不出现在自动配置的正选/落选里,但你可以直接搜属性名):
============================CONDITIONS EVALUATION REPORT============================Positive matches:----------------- PropertyPlaceholderAutoConfiguration matched: - @ConditionalOnMissingBean (types: org.springframework.context.support.PropertySourcesPlaceholderConfigurer) did not find any beans (OnBeanCondition)Negative matches:----------------- RedisAutoConfiguration: Did not match: - @ConditionalOnClass did not find required classes 'org.springframework.data.redis.core.RedisOperations', ... (OnClassCondition)验收清单:① 两种取值下 featureRunner 在容器里吗 分别是 false / true;② 你能说出 beanDefinitionCount 为什么只差 1;③ 你能在 --debug 输出里找到 Negative matches: 这一段,并解释任意一行为什么落选。
目标:只用一个改动,观察条件行为的变化。每条各做一次,把两行日志记进笔记。
- 把注解改成
@ConditionalOnProperty(prefix = "feature", name = "enabled", havingValue = "true", matchIfMissing = true),然后把 yml 里那行enabled整行删掉。你会观察到:Bean 照样被注册(containsBean返回true)——这就是「不写也算开」。再对比第一档里不带matchIfMissing时删掉同一行的结果(false),你就彻底记住了这个默认值。 - 把
@ConditionalOnProperty换成@ConditionalOnBean(String.class),yml 保持原样。你会观察到:结果开始随「容器里此刻有没有一个 String Bean」漂移,甚至可能因加载顺序在你机器上和同事机器上不一致——这正是第四节和那张vs图讲的现象。 - 保留第一档的写法,但在
DemoApplication旁边再加一个@Configuration类,里面用@Bean自己定义一个Runnable featureRunner()。你会观察到:由于同类内@Bean顺序不保证,你可能看到BeanDefinitionOverrideException(Boot 2.1+ 默认禁止覆盖),也可能两个都在。修法是把默认值搬进自动配置类并使用@ConditionalOnMissingBean。
提示:做完第 3 条,回头重跑第十一节的 cond 实验的 missing 参数,抽象规则会立刻落地。
给自己做一个「功能开关套件」,要求把本篇所有机制都用上一遍。
- 一个自定义条件注解
@ConditionalOnDayOfWeek(参考第五节),支持DayOfWeek[] value() - 两个配置类:周一到周五装配
WorkdayReporter,周末装配WeekendReporter,两者实现同一个接口Reporter - 一个总开关:
report.enabled=false时两者都不装配(@ConditionalOnProperty,且带matchIfMissing = true) - 一个测试钩子:允许用配置项
report.day-of-week捏造「今天是星期几」,让单元测试不用等到周一才能跑 - 一段
main:打印当前激活的是哪个 Reporter,以及--debug报告里对应的那一行判决
验收清单:① 用同一个 jar 通过命令行参数切换出三种结果(工作日 / 周末 / 全关),不需要重新打包;② report.day-of-week=MONDAY 能在任意一天触发工作日分支;③ 你能写出「为什么两个 Reporter 不会同时出现」的一句话解释,并在 --debug 报告里指出证据行;④ 有人第一次接手时,不看代码只看 application.yml 的注释就能知道怎么开关。
不看上文,说出条件求值的「四问」顺序,以及每一问由哪个条件类负责(OnClassCondition / OnBeanCondition / OnPropertyCondition)。
@ConditionalOnBean 和 @ConditionalOnMissingBean 各自问的是什么?为什么说它们是同一枚硬币的两面?
为什么 @ConditionalOnClass 可以放在 PARSE_CONFIGURATION 阶段,而 @ConditionalOnMissingBean 必须等到 REGISTER_BEAN?用「信息什么时候出现」这句话回答。
@ConditionalOnProperty("my.feature") 只写名字时,「属性不存在」和「属性等于 false」结果一样吗?想让「不写就默认开启」要加什么?
一个贴着条件注解的 Bean 消失了,你的第一步动作是什么?在报告里该搜哪个关键词?
类在不在、Bean 有没有、开关开没开、用户配没配——四问全过才注册,不过就静默消失;想知道为什么,--debug 看 Negative matches。
条件装配的全部秘密,是「把 if 搬进注解」。所有条件最终都落到 @Conditional(Condition.class):一个返回 boolean 的 matches 方法,配上一个能拿到 BeanFactory / Environment / ClassLoader 等五大能力的 ConditionContext。条件分 PARSE_CONFIGURATION 与 REGISTER_BEAN 两个阶段求值,是因为它们需要的信息出现时机不同。全家族按两大家族记忆:存在性判断(OnClass / OnBean / OnMissingBean / OnResource)与环境配置判断(OnProperty / OnWebApplication / OnExpression)。实战里最该刻进肌肉记忆的是三条:求值顺序决定成败(用户优先 + before/after + @AutoConfigureOrder)、@Profile 也是条件装配、条件必须快且无副作用——调试的第一手段永远是 --debug 条件报告。