Conditional Wiring in Full: Ten Questions About @Conditional
In plain words: conditional wiring means "let the container decide, at startup, whether this object should be created at all." You do not scatter if (production) { ... } branches through your code — you stick one annotation on a configuration class or a @Bean method, and the container asks a few questions before building it: Is the class on the classpath? Does that bean exist? Is the switch property on? Did the user already configure one? All four must pass for the bean to get registered; fail any one and it simply does not exist. The hard part is this: when a condition fails there is no error at all — the object just quietly disappears. That makes the real skill of this chapter not syntax but "how do I find out why it never took effect".
think of a fully-furnished apartment. Every appliance and piece of furniture comes pre-installed — until you place your own sofa in the living room, at which point the fit-out company will not put theirs there. That is exactly @ConditionalOnMissingBean: "the guest did not bring an umbrella, so the shop lends one; if you brought your own, the staff member puts the umbrella rack back". Conversely, @ConditionalOnClass is a restaurant saying "this dish is only served if its ingredient arrived today", and @ConditionalOnProperty is "this lamp only lights up if the wall switch is on". Hold these four sentences and every annotation below is just a variation on them.

That figure gives the panorama, but "order" is not something you absorb by looking — boxes ③ and ④ look so alike that most people remember them backwards. Here is the same path as boxes you can click through one at a time:
After this article you should be able to answer:
- In what order does condition evaluation ask its four questions, and which condition class answers each one?
- Why does
@ConditionalOnBeansilently work in an auto-configuration class but break on your own@Configuration? - When a bean vanishes with no exception, what is the very first command you run and the keyword you search for?
Before syntax, let's answer a more fundamental question: why not just hard-code the configuration instead of inventing a whole "condition" mechanism? Because the same code often has to behave differently across environments and dependency combinations. Three scenarios nearly every backend engineer has met:
- Scenario one: H2 in development, MySQL in production. The team wants in-memory H2 for local tests and MySQL when deployed. A hard-coded datasource means maintaining two code paths; the ideal is "one codebase that uses H2 when it detects H2 on the classpath"
- Scenario two: optional dependencies must degrade gracefully. A module uses Redis for caching, but some edge services deploy without Redis. It should "use Redis if present, fall back to a local
ConcurrentHashMapif not", rather than failing startup - Scenario three: a starter provides a default while letting users replace it. Your starter should supply a default
HttpClient, but if the consumer defines one of the same type, theirs must win
All three share a trait: whether to wire something depends on the runtime environment and dependencies, not on a constant in the code. Conditional wiring is Spring's standard answer — it moves the "if this, then that" decision from if statements into annotations, letting the container decide at startup.
And the first place "runtime dependencies" are decided is not an annotation at all, it is the pom. Tick it yourself and the mechanism becomes obvious: untick H2 and every bean that DataSourceAutoConfiguration guards with @ConditionalOnClass disappears in a batch, while the startup log does not change by a single character.
<?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>Every condition annotation ultimately reduces to one meta-annotation: @Conditional. It carries no logic itself; it only binds a condition class:
@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Conditional { Class<? extends Condition>[] value();}The real decision is made by the Condition interface — note it has a single method returning a boolean:
@FunctionalInterfacepublic interface Condition { boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);}context: aConditionContext, the "whole world" a condition can reach at runtime. It exposes theBeanFactory(to look up existing beans), theEnvironment(to read properties and profiles), aResourceLoader(to look up resources), theClassLoader(to test class presence), and theBeanDefinitionRegistry(to inspect bean definitions)metadata: anAnnotatedTypeMetadatathat lets you read the attributes of the annotation on the annotated class or method. For example, thenameandhavingValueof@ConditionalOnProperty(name = "x", havingValue = "y")are read from here
A plain Condition only answers "true or false", but Spring adds a finer design inside — ConfigurationCondition, which splits evaluation into two phases:
public interface ConfigurationCondition extends Condition { ConfigurationPhase getConfigurationPhase(); enum ConfigurationPhase { PARSE_CONFIGURATION, // evaluated while parsing: should this class be parsed at all REGISTER_BEAN // evaluated while registering: should this @Bean be registered }}| Phase | When it evaluates | What it decides | Typical annotation |
|---|---|---|---|
PARSE_CONFIGURATION | while parsing the configuration class | whether the whole @Configuration class is parsed | @ConditionalOnClass |
REGISTER_BEAN | while registering beans | whether an individual @Bean method is registered | @ConditionalOnMissingBean |
why two phases? Because the two kinds of decisions depend on different information. @ConditionalOnClass only inspects the classpath, so it can decide before parsing and, placed in the PARSE phase, saves the entire parsing cost of the class. @ConditionalOnMissingBean needs to know "did anyone register?", which is only knowable at the bean-registration stage once other definitions have arrived. Different phases exist because the information they need appears at different times.
That paragraph answered "when may a condition speak". What about "what can it ask about"? ConditionContext exposes exactly five things, and each condition class is tied to one of them. This is not memorable from a list, so play a round: pick a condition class on the left, then the one thing it can query inside matches().
On top of @Conditional, Spring Boot wraps a whole set of ready-made conditions. They fall into two families by what they look at:
| Annotation | What it checks | Typical use |
|---|---|---|
@ConditionalOnClass | the class is on the classpath | wire only when the dependency is present (HikariCP, Redis) |
@ConditionalOnMissingClass | the class is absent | rule out an implementation |
@ConditionalOnBean | a bean of the type already exists | wire only when another bean exists |
@ConditionalOnMissingBean | no bean of the type exists | the default fallback when the user configured nothing |
@ConditionalOnSingleCandidate | exactly one candidate bean | a single DataSource or transaction manager |
@ConditionalOnProperty | a property equals / differs from a value | feature toggles |
@ConditionalOnResource | the resource exists | wire only when classpath:xxx.yml exists |
@ConditionalOnWebApplication | the app is a web application | web-only auto-configuration |
@ConditionalOnNotWebApplication | the app is not a web application | non-web auto-configuration |
@ConditionalOnExpression | a SpEL expression is true | complex combined conditions |
@ConditionalOnJava | the JDK version is in a range | version-dependent compatibility |

@ConditionalOnProperty has a high-frequency pitfall — by default it requires the property to exist and not equal false. So with @ConditionalOnProperty("my.feature") (name only), my.feature=false makes the condition fail, but leaving the property out entirely also fails. To express "enabled by default when absent", use matchIfMissing = true.
Conditions are not independent — they have temporal dependencies. The classic case is @ConditionalOnBean: for it to answer "is a bean of this type present?", that bean's BeanDefinition must already be registered. Get the order wrong and it sees "not yet", making an incorrect decision.
This yields a counter-intuitive conclusion: the same condition annotation, evaluated at a different position and time, can produce a different result. Spring Boot uses three mechanisms to keep ordering controllable:
// 1. User first: AutoConfigurationImportSelector is a DeferredImportSelector,// so auto-configuration always runs after the user's own @Configuration// → OnMissingBean can always see the beans you defined// 2. Between auto-configurations: order dependencies with before / after@AutoConfiguration(after = DataSourceAutoConfiguration.class)@ConditionalOnBean(DataSource.class)public class JpaAutoConfiguration { }// 3. Absolute-order fallback: @AutoConfigureOrder (lower number runs first)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 100)public class MyFirstAutoConfiguration { }
- User first is the foundation of the whole design: because the
DeferredImportSelectorpostpones auto-configuration until after user configuration,@ConditionalOnMissingBeangets to "step aside" - before / after handles ordering among auto-configurations, declared in pairs and forbidden from forming cycles
- @AutoConfigureOrder is the absolute-number fallback, and the official guidance is to prefer the first two
"User first" turns into a literal six-step hand-off the moment @ConditionalOnMissingBean is involved. Watch frame ⑤: the default withdraws, and nothing at all gets printed.

inside a single @Configuration class, do not rely on the declaration order of two @Bean methods to have one @Bean carry a @ConditionalOnBean checking the other. The processing order of @Bean methods within one class is not guaranteed (it is affected by signatures, proxies and more), making such code very fragile. To express a dependency, split the beans into different configuration classes and order them explicitly with before / after.
Turn that trap into one side-by-side picture and the only thing a beginner has to carry away is "which door does this annotation belong behind":

the registration desk only answers once the roster is up. @ConditionalOnBean is a hospital desk asking "is the doctor you want on shift today?" — that question only has a reliable answer after the roster has been posted. Auto-configuration is that roster: the container registers your own departments first (user first), then lines the auto-configured ones up with before / after, and only then does "is doctor X here" have a definite answer. Put the same annotation on a business configuration class and you are asking it at six in the morning, before any roster exists: somebody says "yes" if a colleague happened to arrive early and "no" if nobody did, so the answer tracks who got there first rather than the fact itself.
One-line test: write @ConditionalOnBean / @ConditionalOnMissingBean only in auto-configuration classes and starters; when your own business configuration needs "wire it conditionally", use @Profile or @ConditionalOnProperty.
The criterion is easy to state; "in which beat does a bean actually vanish" is not. Spread it out as a single-step run: six lines on the left, and on the right the variables and the call stack as they change. Press step repeatedly and watch beat ⑤ — the one where beanDefinitionCount drops by one and the screen shows no exception whatsoever:
@SpringBootApplication // its @EnableAutoConfiguration registers a DeferredImportSelectorSpringApplication.run(...) // the user's own @Configuration classes parse first// only after every user class is parsed do auto-configurations arrive (that is the 'deferred' part)@ConditionalOnBean(DataSource.class) // the question I wrote on my own configuration classmatches() -> registry.containsBeanDefinition("dataSource") // asked once, right now// condition is false: no registration, no error, one Did not match line in the report| just read | @EnableAutoConfiguration |
| import selector registered | DeferredImportSelector (deferred) |
| definitions registered | 0 |
SpringApplication.run@Import(AutoConfigurationImportSelector)Once you understand the Condition interface, you can build your own condition annotation. The "only on a given weekday" example below is playful, but every detail it uses is production-grade:
@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Conditional(OnDayOfWeekCondition.class) // binds the condition classpublic @interface ConditionalOnDayOfWeek { DayOfWeek[] value();}public class OnDayOfWeekCondition implements ConfigurationCondition { // put it in the REGISTER_BEAN phase, consistent with bean conditions @Override public ConfigurationPhase getConfigurationPhase() { return ConfigurationPhase.REGISTER_BEAN; } @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { // 1. read attribute values from the annotation Map<String, Object> attrs = metadata .getAnnotationAttributes(ConditionalOnDayOfWeek.class.getName()); DayOfWeek[] days = (DayOfWeek[]) attrs.get("value"); // 2. read "today" from the Environment so tests can override it Environment env = context.getEnvironment(); String override = env.getProperty("demo.day-of-week"); DayOfWeek today = override != null ? DayOfWeek.valueOf(override.toUpperCase()) : LocalDate.now().getDayOfWeek(); // 3. also peek at existing beans (shows what ConditionContext can do) boolean hasReportService = context.getBeanFactory() != null && context.getBeanFactory().getBeanNamesForType(ReportService.class).length > 0; return Arrays.asList(days).contains(today) && !hasReportService; }}@Configuration@ConditionalOnDayOfWeek(DayOfWeek.MONDAY) // wire only on Mondayspublic class MondayReportConfig { @Bean public ReportJob reportJob() { return new ReportJob(); }}metadata.getAnnotationAttributes(...): reads the annotation attributes;value()is whatever sits in@ConditionalOnDayOfWeek(DayOfWeek.MONDAY)context.getEnvironment(): reaches the configuration environment; readingdemo.day-of-weeklets a test force "today" to any day, making the condition testablecontext.getBeanFactory(): inspects existing beans — exactly what@ConditionalOnBeandoes internally
Tip: a ConditionContext hands you five things — BeanFactory, Environment, ResourceLoader, ClassLoader, and BeanDefinitionRegistry. When writing a custom condition, these five cover essentially everything you need for a decision. Asked "how do you write a custom condition annotation", answer "implement Condition, override matches, and bind with @Conditional"; adding the two-phase ConfigurationCondition earns extra credit.
Many people think @Profile is a separate mechanism, but it is just an official preset of conditional wiring. Its definition makes this obvious:
@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Conditional(ProfileCondition.class) // still essentially @Conditionalpublic @interface Profile { String[] value();}ProfileCondition does something plain: it reads spring.profiles.active / spring.profiles.default and checks whether the active profiles fall within the list declared on the annotation. So "switch beans by profile" and "switch beans by condition" are the same mechanism — @Profile is a special case of @Conditional. Understand that and you can hold both in one mental model instead of memorizing two unrelated rule sets.
The hardest part of conditional wiring is its silence — when a condition fails, the bean vanishes with no error. So debugging skill matters more than syntax. The first tool is still the condition evaluation report:
java -jar app.jar --debugEach line tells you which condition class (the OnClassCondition etc. in parentheses) made the call:
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)- Check
Positive matchesto confirm "did what I expected actually take effect" - Read the
Did not match:list underNegative matchesand compare each item to find which condition stopped you — is the class missing from the classpath (dependency not added), or is a property not matching (config typo)? It is instantly visible - You can even grab this report object in code:
ConditionEvaluationReport.get(beanFactory), to publish wiring results to your own ops dashboard
That loop — read the report, find the one line, fix the cause — deserves its own walkthrough, because the real blocker for a beginner is not writing conditions but not knowing where to look once a bean disappears:

A route is not enough, though — you have to practise it on a real stack. This is the most common shape in production: the code did not change, only the machine did. Do not read the analysis; click the frame you believe is guilty:
The report job runs fine on your laptop. On a colleague's machine startup fails. You are certain not one line changed — his pom is simply missing one dependency.
This annotation is the easiest to misuse, and two pitfalls must be known up front:
- Pitfall one: it only sees beans that are already registered.
@ConditionalOnBeaninspects already registered BeanDefinitions, not beans that will appear later. So it is tightly coupled to "who registers first" — which is why Section 4 keeps stressing order. When using it between auto-configuration classes, always pair it withbefore/after - Pitfall two: never do heavy work in a condition.
matchesis evaluated repeatedly (the same condition may be called many times in one startup) and it runs synchronously on the startup critical path. Querying a database, making a network call, or parsing a large file inside it will slow down or even hang startup. Conditions should be fast, pure, and side-effect free
do not write logic like "read a remote config center to decide the condition". Condition evaluation happens very early, when much of the infrastructure (connection pools, registry clients) is itself still being wired up. You are likely to get a component that is not yet initialized, sending startup into an indeterminate state that is agonizing to debug.
The demo below turns condition evaluation into switchable knobs. Toggle "user-defined DataSource" and "spring-jdbc on the classpath" and watch the wiring result change — exactly how @ConditionalOnMissingBean and @ConditionalOnClass cooperate in real work:
Everything above has been prose. Run these five kernel labs in order, about 30 seconds each; they map onto the five threads of this chapter: first how a single condition decides, then the whole wiring chain, then what a condition actually inspects, then applying the rules back to your own starter, and finally the more basic question of how an object gets into the container at all.
The first lab interrogates four conditions separately. Click onclass first (is the dependency there), then onprop (is the switch on), then compare onbean against missing side by side — the former asks "does it exist", the latter "did you configure one yourself". This pair is the easiest to remember backwards:
The second lab puts those conditions back on the auto-configuration pipeline. Click through imports → filter → sort → apply → report and you will see at which station the candidate count drops hardest, and why the same annotation answers differently at different stages:
The third lab answers a more fundamental question: what does a condition actually judge? The answer is the BeanDefinition — the "recipe" the container writes down before ever building the object (a metadata object describing how this bean should be created, not the bean itself). Once you see that layer, you understand that conditional wiring intercepts "should we write this recipe", not "should we call the constructor":
The fourth lab applies the whole rule set to your own starter: register the list → bind properties → create the bean → switch it off. After the first three stations, be sure to click off and compare the two ways of turning it off — exclude versus a switch property:
The fifth lab asks the question that comes even earlier: conditions decide "may we write this recipe", but how many sources are there for a recipe in the first place? Click scan, bean, import, auto in turn and each one shows which phase writes the BeanDefinition into the registry. Then click miss — the "package was never scanned" scene, whose error wording is almost identical to "blocked by a condition", which is precisely the distinction the stack in Section 7 turns on:
the shared conclusion of all five labs is that condition evaluation happens before any object is built. That is why it is fast and cheap — and precisely because it happens so early, many things you rely on (connection pools, remote config) do not exist yet at that moment. That is the origin of the warning in Section 8.
Buttons are fine, but eventually you type the commands yourself. This console talks to the same in-browser kernel and every reply is computed there — boot builds the container and prints the wiring log, then flip the switches one by one:
after cond jdbcOnClasspath false you still have to type beans to see the damage — cond rebuilds the container but only echoes the new switch values, it does not list them for you. That sequence is the manual version of "change a condition, read the report" from Section 7.
Now it is your turn to act as the container. Flip the three switches on the left; the right shows the startup result and the exact verdict line from the --debug report. Read the defaults across all three cells first, then change one cell at a time — the fastest way to internalize conditional wiring:
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)The container holds 1 CacheService: the auto-configured one
the last cell is the combination people overlook most. What @ConditionalOnMissingBean guarantees is only that "the first bean the user registered beats the default"; it does not clean up duplicate definitions for you. When two beans of the same type really exist, it is the injection site that errors, not the container's wiring phase — memorize required a single bean, but 2 were found, that is the target of this section.
A warm-up question, testing exactly the default-value trap flagged in Section 3:
Then a comprehensive question that ties Sections 4 and 8 together with the vs figure:
Every "error text" row below can be copied verbatim into a search engine — do not paraphrase, do not abbreviate. Conditional-wiring failures share one trait: most never appear as a red exception; they appear as "something is simply missing".
| Error text (fragment) | Real cause | 30-second fix | Deep dive |
|---|---|---|---|
@ConditionalOnBean is clearly written, yet the bean was never created (and no error either) | Wrong evaluation timing: the bean definition being tested had not been registered yet, so the condition saw "none". Especially easy to hit in business configuration classes | Move it into an auto-configuration class with before / after; or replace it with an explicit @ConditionalOnProperty switch | Sections 4 and 8 |
NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.ReportJob' available | The annotated class was never registered at all — its condition failed and it vanished silently | Restart with --debug, find this class under Negative matches, read the line after Did not match: which names the blocking condition | Sections 7 and 12 |
java.lang.NoClassDefFoundError: org/springframework/data/redis/core/RedisOperations | The class existed at compile time (provided scope or visible in the IDE) but its jar is not on the runtime classpath | Check whether <optional> / <scope>provided</scope> excluded it; verify with mvn dependency:tree; this is precisely the accident @ConditionalOnClass prevents | #2 Maven dependencies · #18 auto-configuration |
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'noticeService': Unsatisfied dependency expressed through constructor parameter 0 | The bean you inject never got wired — usually an upstream condition (property switch, missing class) blocked it | Take the type name from the error and search the --debug report for which auto-configuration owns it; confirm the switch property and the dependency jar before blaming your code | Section 12 sandbox · #6 IoC and DI |
Parameter 0 of constructor in xxx required a single bean, but 2 were found | Multiple candidates of the same type: @ConditionalOnMissingBean failed to yield, or the user and the auto-configuration each registered one | Add @Qualifier("name") at the injection point or mark the preferred one @Primary; the root fix is writing the default's condition precisely | Section 12 · #7 BeanDefinition |
@ConditionalOnProperty (my.feature) did not match (OnPropertyCondition) | Only the name was given, no matchIfMissing = true, and the property does not exist at all — absence also counts as no match | Either add my.feature: true to the yml, or add matchIfMissing = true to the annotation | Tip bar in Section 3 |
My auto-configuration class appears nowhere in CONDITIONS EVALUATION REPORT (not even under Negative matches) | It never entered the candidate list: wrong imports file path, or it is an ordinary configuration class picked up by @ComponentScan | Verify META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports; confirm it lists fully-qualified class names | #20 Custom starter |
when searching an error, search only the first fragment after the colon (for example required a single bean, but 2 were found) — far higher hit rate than the full sentence, since Spring versions append extra wording to the second half.
Goal: build a tiny app where a bean exists only when a switch is on, and confirm both paths with your own eyes in the --debug report. Four files total.
Step one, the only dependency needed is Boot itself (no database, no Redis):
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency></dependencies>Step two, the main class DemoApplication.java (at 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("is featureRunner in the container -> " + ctx.containsBean("featureRunner")); System.out.println("beanDefinitionCount = " + ctx.getBeanDefinitionCount()); }}Step three, the condition-gated configuration class FeatureConfig.java (same package as the main class):
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] I only exist when the switch is on"); }}Step four, the config file src/main/resources/application.yml:
feature: enabled: false # ← run once with false, then flip to true and run againlogging: level: root: infoExpected output of the two runs (compare against these key lines):
# run 1: feature.enabled=falseis featureRunner in the container -> falsebeanDefinitionCount = 78# run 2: feature.enabled=trueis featureRunner in the container -> truebeanDefinitionCount = 79Step five, add --debug and run once more, then scroll the report at the top of the console to your own entry:
java -jar target/demo-0.0.1-SNAPSHOT.jar --debugReport fragment (on run 1 — your class is a scanned ordinary configuration class, so it appears neither in the auto-configuration positive nor negative list; search by the property name directly instead):
============================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)Acceptance checklist: ① is featureRunner in the container prints false / true for the two values; ② you can explain why beanDefinitionCount differs by exactly 1; ③ you can locate the Negative matches: block in the --debug output and explain why any one line was rejected.
Goal: change one thing at a time and watch how condition behaviour shifts. Do each once and write the two log lines into your notes.
- Change the annotation to
@ConditionalOnProperty(prefix = "feature", name = "enabled", havingValue = "true", matchIfMissing = true), then delete the wholeenabledline from the yml. You will observe: the bean still registers (containsBeanreturnstrue) — that is "absent counts as on". Compare with deleting the same line withoutmatchIfMissingin Level 1 (false) and the default value is burned into memory. - Replace
@ConditionalOnPropertywith@ConditionalOnBean(String.class)and leave the yml alone. You will observe: the result now drifts with "is there a String bean in the container at this moment", and may even differ between your machine and a colleague's because of loading order — exactly what Section 4 and thevsfigure describe. - Keep the Level 1 code but add a second
@Configurationclass next toDemoApplicationthat defines its own@Bean Runnable featureRunner(). You will observe: because order within one class is not guaranteed, you may getBeanDefinitionOverrideException(disallowed by default since Boot 2.1) or both beans may exist. The fix is moving the default into an auto-configuration class using@ConditionalOnMissingBean.
Hint: after item 3, re-run the missing argument of the Section 11 cond lab — the abstract rule lands immediately.
Build yourself a "feature toggle kit" that uses every mechanism in this chapter.
- A custom condition annotation
@ConditionalOnDayOfWeek(see Section 5) supportingDayOfWeek[] value() - Two configuration classes: wire
WorkdayReporterMonday to Friday,WeekendReporteron the weekend, both implementing one interfaceReporter - One master switch: when
report.enabled=falseneither is wired (@ConditionalOnPropertywithmatchIfMissing = true) - One test hook: let the property
report.day-of-weekfake "what day today is", so unit tests need not wait until Monday - A
mainthat prints which Reporter is active, plus the corresponding verdict line from the--debugreport
Acceptance checklist: ① with one and the same jar, produce three outcomes via command-line arguments (workday / weekend / all off) without repackaging; ② report.day-of-week=MONDAY triggers the workday branch on any actual day; ③ you can state in one sentence "why the two Reporters never appear together" and point at the evidence line in the --debug report; ④ someone picking this up for the first time can operate the toggles from the application.yml comments alone, without reading code.
from memory, give the order of the four questions of condition evaluation and which condition class answers each (OnClassCondition / OnBeanCondition / OnPropertyCondition).
what does @ConditionalOnBean ask, and what does @ConditionalOnMissingBean ask? Why are they two sides of the same coin?
why may @ConditionalOnClass sit in the PARSE_CONFIGURATION phase while @ConditionalOnMissingBean must wait for REGISTER_BEAN? Answer with the phrase "when does the information become available".
with @ConditionalOnProperty("my.feature") given by name only, do "property absent" and "property equals false" produce the same result? What do you add to get "on unless stated otherwise"?
a bean carrying a condition annotation has disappeared. What is your first move, and which keyword do you search for in the report?
class present? bean present? switch on? user configured? — all four must pass to register, fail one and it vanishes silently; to learn why, run --debug and read Negative matches.
the entire secret of conditional wiring is "moving if into annotations". Every condition reduces to @Conditional(Condition.class): a matches method returning a boolean plus a ConditionContext exposing five capabilities such as BeanFactory, Environment, and ClassLoader. Conditions evaluate in two phases, PARSE_CONFIGURATION and REGISTER_BEAN, because the information they need appears at different times. Memorize the family in two groups: existence checks (OnClass / OnBean / OnMissingBean / OnResource) and environment/config checks (OnProperty / OnWebApplication / OnExpression). In practice, three rules deserve muscle memory: evaluation order decides success or failure (user first + before/after + @AutoConfigureOrder), @Profile is conditional wiring too, and conditions must be fast and side-effect free — and the first debugging tool is always the --debug condition report.