Conditional Wiring in Full: Ten Questions About @Conditional

bee2026-10-0844 min read0 views
The real brain behind auto-configuration is conditional wiring. From the @Conditional meta-annotation to the whole OnClass/OnMissingBean/OnProperty family, with evaluation order and debugging.
1 / 136
Section
0. The 30-second version
2 / 136

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".

3 / 136
类比|Analogy

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.

4 / 136
Diagram
Figure · The four questions a condition asks: map of this chapter
Figure · The four questions a condition asks: map of this chapter
5 / 136

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:

6 / 136
Diagram
FlowThe four questions of conditional wiring (click through them)1 / 5
Go from ① to ⑤. Boxes ③ and ④ are the pair people invert: one asks 'is the switch on', the other 'did the user configure one'
→
→
→
→
① Is the class there
`OnClassCondition` asks only the ClassLoader: is this type on the classpath? It runs before the configuration class is parsed (the PARSE phase), and a 'no' skips parsing entirely — the cheapest question, therefore the earliest one.
All clearThe order cannot be shuffled: the earlier the check, the cheaper it is, and the less it depends on anybody else's progress.
7 / 136

After this article you should be able to answer:

8 / 136
  • In what order does condition evaluation ask its four questions, and which condition class answers each one?
  • Why does @ConditionalOnBean silently 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?
9 / 136
Section
1. Why conditional wiring exists: three real scenarios
10 / 136

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:

11 / 136
  • 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 ConcurrentHashMap if 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
12 / 136

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.

13 / 136

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.

14 / 136
Generator
GeneratorWhether something gets wired is already decided in the pompom.xml3 / 7
Generate once with the defaults, then untick H2 or Redis and generate again: one fewer line in the pom, one fewer class on the classpath, one fewer 'yes' for @ConditionalOnClass — and Section 7's --debug report grows a whole block of Negative matches. The cond lab in Section 11 plays the same scene back
Output
<?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>
Why each choice matters
parentInheriting 3.3.4 starter-parent means no spring-boot-starter-* needs a version; the moment someone adds an explicit version to one starter, that one wins — the most common source of dependency drift.
WebAnything that serves HTTP needs it: DispatcherServlet, embedded Tomcat and JSON mapping come inside this starter.
JDBCJust the template class and HikariCP — the minimum when you refuse an ORM.
H2 内存库Runtime scope so local runs and tests need no real server; exclude it from the prod profile.
15 / 136
Section
2. The @Conditional meta-annotation: the root of every condition
16 / 136

Every condition annotation ultimately reduces to one meta-annotation: @Conditional. It carries no logic itself; it only binds a condition class:

17 / 136
java
@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Conditional {    Class<? extends Condition>[] value();}
18 / 136

The real decision is made by the Condition interface — note it has a single method returning a boolean:

19 / 136
Code
Codejava
@FunctionalInterfacepublic interface Condition {    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);}
Notes
  • context: a ConditionContext, the "whole world" a condition can reach at runtime. It exposes the BeanFactory (to look up existing beans), the Environment (to read properties and profiles), a ResourceLoader (to look up resources), the ClassLoader (to test class presence), and the BeanDefinitionRegistry (to inspect bean definitions)
  • metadata: an AnnotatedTypeMetadata that lets you read the attributes of the annotation on the annotated class or method. For example, the name and havingValue of @ConditionalOnProperty(name = "x", havingValue = "y") are read from here
20 / 136

A plain Condition only answers "true or false", but Spring adds a finer design inside — ConfigurationCondition, which splits evaluation into two phases:

21 / 136
java
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    }}
22 / 136
Table
PhaseWhen it evaluatesWhat it decidesTypical annotation
PARSE_CONFIGURATIONwhile parsing the configuration classwhether the whole @Configuration class is parsed@ConditionalOnClass
REGISTER_BEANwhile registering beanswhether an individual @Bean method is registered@ConditionalOnMissingBean
23 / 136
Key point

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.

24 / 136

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().

25 / 136
Match
MatchWho makes the call, and what it can reachMatched 0/6 · Missed 0
Left column: condition classes. Right column: the single thing each one queries inside matches(). Positions are shuffled on both sides, so you cannot pass this by guessing
Pick a card on the left first
26 / 136
Section
3. The full family cheat sheet
27 / 136

On top of @Conditional, Spring Boot wraps a whole set of ready-made conditions. They fall into two families by what they look at:

28 / 136
Table
AnnotationWhat it checksTypical use
@ConditionalOnClassthe class is on the classpathwire only when the dependency is present (HikariCP, Redis)
@ConditionalOnMissingClassthe class is absentrule out an implementation
@ConditionalOnBeana bean of the type already existswire only when another bean exists
@ConditionalOnMissingBeanno bean of the type existsthe default fallback when the user configured nothing
@ConditionalOnSingleCandidateexactly one candidate beana single DataSource or transaction manager
@ConditionalOnPropertya property equals / differs from a valuefeature toggles
@ConditionalOnResourcethe resource existswire only when classpath:xxx.yml exists
@ConditionalOnWebApplicationthe app is a web applicationweb-only auto-configuration
@ConditionalOnNotWebApplicationthe app is not a web applicationnon-web auto-configuration
@ConditionalOnExpressiona SpEL expression is truecomplex combined conditions
@ConditionalOnJavathe JDK version is in a rangeversion-dependent compatibility
29 / 136
Diagram
Figure 1 · The @Conditional family
Figure 1 · The @Conditional family
30 / 136
Tip

@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.

31 / 136
Section
4. Why evaluation order matters
32 / 136

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.

33 / 136

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:

34 / 136
java
// 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 { }
35 / 136
Animation
Animation · Evaluation order matters
Animation · Evaluation order matters
36 / 136
  • User first is the foundation of the whole design: because the DeferredImportSelector postpones auto-configuration until after user configuration, @ConditionalOnMissingBean gets 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
37 / 136

"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.

38 / 136
Animation
Animation · The whole hand-off: how the default withdraws
Animation · The whole hand-off: how the default withdraws
39 / 136
Trap

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.

40 / 136

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":

41 / 136
Diagram
Figure · @ConditionalOnBean placement: one annotation, two outcomes
Figure · @ConditionalOnBean placement: one annotation, two outcomes
42 / 136
类比|Analogy

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.

43 / 136

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.

44 / 136

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:

45 / 136
Stepper
StepperStep by step: how one @Bean vanishes in silence1 / 6
Six beats. Keep your eye on beanDefinitionCount and on 'exceptions thrown' in beat 5 — that is the whole lesson of Section 8
Code under debug
1@SpringBootApplication // its @EnableAutoConfiguration registers a DeferredImportSelector
2SpringApplication.run(...) // the user's own @Configuration classes parse first
3// only after every user class is parsed do auto-configurations arrive (that is the 'deferred' part)
4@ConditionalOnBean(DataSource.class) // the question I wrote on my own configuration class
5matches() -> registry.containsBeanDefinition("dataSource") // asked once, right now
6// condition is false: no registration, no error, one Did not match line in the report
Variables now
just read@EnableAutoConfiguration
import selector registeredDeferredImportSelector (deferred)
definitions registered0
Call stack
1SpringApplication.run
2@Import(AutoConfigurationImportSelector)
1Deferred import is the load-bearing wall of the whole ordering story. It pushes auto-configuration behind every user class, so 'user first' is not politeness — it is a guarantee made by the timeline.
46 / 136
Section
5. Writing a custom condition annotation
47 / 136

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:

48 / 136
java
@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Conditional(OnDayOfWeekCondition.class)   // binds the condition classpublic @interface ConditionalOnDayOfWeek {    DayOfWeek[] value();}
49 / 136
java
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;    }}
50 / 136
Code
Codejava
@Configuration@ConditionalOnDayOfWeek(DayOfWeek.MONDAY)   // wire only on Mondayspublic class MondayReportConfig {    @Bean    public ReportJob reportJob() {        return new ReportJob();    }}
Notes
  • metadata.getAnnotationAttributes(...): reads the annotation attributes; value() is whatever sits in @ConditionalOnDayOfWeek(DayOfWeek.MONDAY)
  • context.getEnvironment(): reaches the configuration environment; reading demo.day-of-week lets a test force "today" to any day, making the condition testable
  • context.getBeanFactory(): inspects existing beans — exactly what @ConditionalOnBean does 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.

51 / 136
Section
6. @Profile is conditional wiring too
52 / 136

Many people think @Profile is a separate mechanism, but it is just an official preset of conditional wiring. Its definition makes this obvious:

53 / 136
java
@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Conditional(ProfileCondition.class)   // still essentially @Conditionalpublic @interface Profile {    String[] value();}
54 / 136

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.

55 / 136
Section
7. Debugging: see exactly why it did not take effect
56 / 136

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:

57 / 136
bash
java -jar app.jar --debug
58 / 136

Each line tells you which condition class (the OnClassCondition etc. in parentheses) made the call:

59 / 136
Code
Codetext
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)
Notes
  • Check Positive matches to confirm "did what I expected actually take effect"
  • Read the Did not match: list under Negative matches and 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
60 / 136

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:

61 / 136
Animation
Animation · Chasing a bean that vanished
Animation · Chasing a bean that vanished
62 / 136

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:

63 / 136
Triage
Error triageNoSuchBeanDefinitionException: No qualifying bean of type 'com.example.report.ReportJob' available
A bean carrying a condition annotation has vanished into thin air

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.

APPLICATION FAILED TO START
Parameter 0 of constructor in com.example.report.ReportService required a bean of type 'com.example.report.ReportJob' that could not be found:
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1801)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBean(DefaultListableBeanFactory.java:1357)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:1309)
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:542)
at com.example.report.ReportService.<init>(ReportService.java:22)
The following candidate was skipped by a condition:
- ReportJobConfig#reportJob: @ConditionalOnClass did not find required class 'com.opencsv.CSVWriter' (OnClassCondition)
Click the frame you blame — guessing is allowed
No pressure: guess the exception first, then which line actually made the call.
64 / 136
Section
8. Trap: two pitfalls of @ConditionalOnBean
65 / 136

This annotation is the easiest to misuse, and two pitfalls must be known up front:

66 / 136
  • Pitfall one: it only sees beans that are already registered. @ConditionalOnBean inspects 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 with before / after
  • Pitfall two: never do heavy work in a condition. matches is 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
67 / 136
Warning

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.

68 / 136
Section
9. Hands-on: trigger condition evaluation yourself
69 / 136

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:

70 / 136
Kernel lab
71 / 136
Section
10. Decision: when writing a starter, OnMissingBean or OnProperty
72 / 136
Decision
Decisionyou are writing an internal starter that should provide a default `HttpClient` to consumers while letting them replace it with their own. Which condition should you use?
73 / 136
Section
11. Labs: run the four questions one by one
74 / 136

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.

75 / 136

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:

76 / 136
Kernel lab
TeaVMWhat each of the four conditions asks: onclass / onbean / onprop / missingidle
Focus on the contrast between onbean and missing: one means 'install only if it exists', the other 'fall back only if none exists'
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
77 / 136

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:

78 / 136
Kernel lab
TeaVMThe auto-configuration pipeline: at which station does filtering happenidle
Watch the candidate count at the filter station, then look at how report groups positive and negative matches
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
79 / 136

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":

80 / 136
Kernel lab
TeaVMFrom @Bean to BeanDefinition: which step does the condition interceptidle
Look at the scan and registry stations first, then use attrs to see how scope / lazy change the creation timing
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
81 / 136

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:

82 / 136
Kernel lab
TeaVMWire an auto-configuration by hand: imports -> properties -> bean -> turn it offidle
When clicking off, contrast spring.autoconfigure.exclude with sms.enabled=false as two different shutdown paths
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
83 / 136

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:

84 / 136
Kernel lab
TeaVMThe four doors an object uses to enter the container: scan / bean / import / autoidle
Run all four buttons; watch when each door writes its BeanDefinition, and how that lines up against the phase each condition is allowed to speak in
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
85 / 136
Kernel lab
TeaVMThe not-scanned scene: why it looks exactly like 'blocked by a condition'idle
Compare with the stack in Section 7: both end in 'the container has none', but this one was never found, the other was found and rejected — the difference lives in the phrase skipped by a condition
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
86 / 136
Key point

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.

87 / 136

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:

88 / 136
Console
89 / 136
Note

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.

90 / 136
Section
12. Sandbox: three switches decide whether the bean enters the container
91 / 136

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:

92 / 136
Sandbox
SandboxWill this bean actually get registered
Result
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
Effect of matchIfMissing = true: an absent property still counts as on. This cell is the all-green default.
93 / 136
Explanation

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.

94 / 136
Section
13. Quick quizzes
95 / 136

A warm-up question, testing exactly the default-value trap flagged in Section 3:

96 / 136
Quiz
Check yourselfA configuration class carries `@ConditionalOnProperty("my.feature")` (name only, no havingValue, no matchIfMissing). The yml has no `my.feature` line at all. Does this bean get registered?
Pick one — you get feedback right away
97 / 136

Then a comprehensive question that ties Sections 4 and 8 together with the vs figure:

98 / 136
Quiz
Check yourselfYou put `@ConditionalOnBean(DataSource.class)` on a `@Bean` method inside your own business `@Configuration` class. It starts fine locally, but on another machine that bean is reported missing. What is the most reasonable explanation and fix?
Pick one — you get feedback right away
99 / 136
Section
14. Common errors cheat sheet
100 / 136

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".

101 / 136
Table
Error text (fragment)Real cause30-second fixDeep 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 classesMove it into an auto-configuration class with before / after; or replace it with an explicit @ConditionalOnProperty switchSections 4 and 8
NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.ReportJob' availableThe annotated class was never registered at all — its condition failed and it vanished silentlyRestart with --debug, find this class under Negative matches, read the line after Did not match: which names the blocking conditionSections 7 and 12
java.lang.NoClassDefFoundError: org/springframework/data/redis/core/RedisOperationsThe class existed at compile time (provided scope or visible in the IDE) but its jar is not on the runtime classpathCheck 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 0The bean you inject never got wired — usually an upstream condition (property switch, missing class) blocked itTake 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 codeSection 12 sandbox · #6 IoC and DI
Parameter 0 of constructor in xxx required a single bean, but 2 were foundMultiple candidates of the same type: @ConditionalOnMissingBean failed to yield, or the user and the auto-configuration each registered oneAdd @Qualifier("name") at the injection point or mark the preferred one @Primary; the root fix is writing the default's condition preciselySection 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 matchEither add my.feature: true to the yml, or add matchIfMissing = true to the annotationTip 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 @ComponentScanVerify META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports; confirm it lists fully-qualified class names#20 Custom starter
102 / 136
Tip

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.

103 / 136
Section
15. Hands-on exercises
104 / 136
Section
Level 1 · Follow along
105 / 136

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.

106 / 136

Step one, the only dependency needed is Boot itself (no database, no Redis):

107 / 136
xml
<dependencies>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter</artifactId>    </dependency></dependencies>
108 / 136

Step two, the main class DemoApplication.java (at src/main/java/com/example/cond/DemoApplication.java):

109 / 136
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());    }}
110 / 136

Step three, the condition-gated configuration class FeatureConfig.java (same package as the main class):

111 / 136
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] I only exist when the switch is on");    }}
112 / 136

Step four, the config file src/main/resources/application.yml:

113 / 136
yaml
feature:  enabled: false      # ← run once with false, then flip to true and run againlogging:  level:    root: info
114 / 136

Expected output of the two runs (compare against these key lines):

115 / 136
text
# run 1: feature.enabled=falseis featureRunner in the container -> falsebeanDefinitionCount = 78# run 2: feature.enabled=trueis featureRunner in the container -> truebeanDefinitionCount = 79
116 / 136

Step five, add --debug and run once more, then scroll the report at the top of the console to your own entry:

117 / 136
bash
java -jar target/demo-0.0.1-SNAPSHOT.jar --debug
118 / 136

Report 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):

119 / 136
text
============================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)
120 / 136

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.

121 / 136
Section
Level 2 · Variants
122 / 136

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.

123 / 136
  1. Change the annotation to @ConditionalOnProperty(prefix = "feature", name = "enabled", havingValue = "true", matchIfMissing = true), then delete the whole enabled line from the yml. You will observe: the bean still registers (containsBean returns true) — that is "absent counts as on". Compare with deleting the same line without matchIfMissing in Level 1 (false) and the default value is burned into memory.
  2. Replace @ConditionalOnProperty with @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 the vs figure describe.
  3. Keep the Level 1 code but add a second @Configuration class next to DemoApplication that defines its own @Bean Runnable featureRunner(). You will observe: because order within one class is not guaranteed, you may get BeanDefinitionOverrideException (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.
124 / 136

Hint: after item 3, re-run the missing argument of the Section 11 cond lab — the abstract rule lands immediately.

125 / 136
Section
Level 3 · Build something
126 / 136

Build yourself a "feature toggle kit" that uses every mechanism in this chapter.

127 / 136
  • A custom condition annotation @ConditionalOnDayOfWeek (see Section 5) supporting DayOfWeek[] value()
  • Two configuration classes: wire WorkdayReporter Monday to Friday, WeekendReporter on the weekend, both implementing one interface Reporter
  • One master switch: when report.enabled=false neither is wired (@ConditionalOnProperty with matchIfMissing = true)
  • One test hook: let the property report.day-of-week fake "what day today is", so unit tests need not wait until Monday
  • A main that prints which Reporter is active, plus the corresponding verdict line from the --debug report
128 / 136

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.

129 / 136
Section
16. Self-check
130 / 136
Self-check

from memory, give the order of the four questions of condition evaluation and which condition class answers each (OnClassCondition / OnBeanCondition / OnPropertyCondition).

131 / 136
Self-check

what does @ConditionalOnBean ask, and what does @ConditionalOnMissingBean ask? Why are they two sides of the same coin?

132 / 136
Self-check

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".

133 / 136
Self-check

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"?

134 / 136
Self-check

a bean carrying a condition annotation has disappeared. What is your first move, and which keyword do you search for in the report?

135 / 136
Mnemonic

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.

136 / 136
Summary

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.