How does Spring Boot build @ConditionalOnClass and @ConditionalOnMissingBean on top of the @Conditional mechanism, and what ordering rules make auto-configuration back-off deterministic?
answer
- @ConditionalOnX = meta-annotation over @Conditional(OnXCondition.class)
- OnClassCondition: ASM/ClassUtils.isPresent, no NoClassDefFoundError
- OnBeanCondition: ConfigurationCondition + REGISTER_BEAN
- User config parsed before auto-config -> user wins
- @AutoConfigureBefore/After/Order + AutoConfiguration.imports; metadata index filter
basics
~20 sBoot's conditions are meta-annotations: each is annotated with @Conditional pointing to a Spring Condition implementation (e.g. OnClassCondition, OnBeanCondition). They read attributes via AnnotatedTypeMetadata. OnBeanCondition runs in the REGISTER_BEAN phase, and Boot parses user config before auto-config so user beans win.
solid answer
~40 sEvery @ConditionalOnX is just @Conditional composed as a meta-annotation. @ConditionalOnClass is @Conditional(OnClassCondition.class); @ConditionalOnMissingBean/@ConditionalOnBean are @Conditional(OnBeanCondition.class); @ConditionalOnProperty → OnPropertyCondition, etc. The condition reads its configured attributes (class names, bean types, property keys) from AnnotatedTypeMetadata, and inspects the container via ConditionContext. OnClassCondition uses ASM/ClassUtils to test classpath presence without loading classes eagerly, which is why an absent optional dependency doesn't cause NoClassDefFoundError. OnBeanCondition implements ConfigurationCondition with REGISTER_BEAN so it sees a populated registry. Determinism of back-off comes from Boot's ordering: user configuration is parsed before auto-configurations, and auto-configurations are ordered among themselves via @AutoConfigureBefore/After/Order (and the AutoConfiguration.imports file). Boot also short-circuits: filtering @ConditionalOnClass early (via the auto-configuration metadata index) skips whole classes cheaply.
code
java · 13 lines// A user-style auto-configuration that backs off if the user defines their own bean.
@AutoConfiguration
public class CacheAutoConfiguration {
@Bean
@ConditionalOnClass(name = "com.github.benmanes.caffeine.cache.Caffeine") // safe: never loads the type
@ConditionalOnMissingBean(CacheManager.class) // OnBeanCondition, REGISTER_BEAN phase
CacheManager caffeineCacheManager() {
return new CaffeineCacheManager();
}
}
// Equivalent core form of the class check:
// @Conditional(OnClassCondition.class) <-- what @ConditionalOnClass expands togo deeper
Know Boot's @ConditionalOnX are shortcuts built on @Conditional; details not expected.
Name the backing conditions (OnClassCondition, OnBeanCondition) and that user beans generally override auto-config defaults.
Explain safe classpath probing, REGISTER_BEAN phase for OnBeanCondition, and user-before-auto-config parse order.
Add the metadata-index fast filtering, @AutoConfigureBefore/After ordering, return-type match pitfalls, and the no-forward-reference limitation; frame trade-offs of the back-off design.
**The composition trick.** Spring core provides exactly one gating primitive: `@Conditional(Class<? extends Condition>...)`. Spring Boot adds no new core mechanism — it defines *meta-annotations*. For example (conceptually): ```java @Conditional(OnClassCondition.class) public @interface ConditionalOnClass { Class<?>[] value(); String[] name() default {}; } @Conditional(OnBeanCondition.class) public @interface ConditionalOnMissingBean { ... } ``` Because `@Conditional` is `@Inherited`-style meta-annotation-aware, placing `@ConditionalOnClass` on a class is equivalent to placing `@Conditional(OnClassCondition.class)`. The condition implementation reads *how it was configured* through the second `matches` argument, `AnnotatedTypeMetadata`, e.g. `metadata.getAnnotationAttributes(ConditionalOnClass.class.getName())` to obtain the class names to check. **OnClassCondition and safe classpath probing.** The whole point of `@ConditionalOnClass` is to reference a type that might not be present without blowing up. Boot achieves this by never *loading* the class through normal reflection at match time — it checks presence via `ClassUtils.isPresent`/ASM-based inspection so a missing type (or a missing transitive dependency) simply yields 'not present' rather than `ClassNotFoundException`/`NoClassDefFoundError`. Boot further speeds this up with a build-time `META-INF/spring-autoconfigure-metadata.properties` index: the `AutoConfigurationImportFilter` (`OnClassCondition` implements it) filters out auto-config classes whose required classes are absent *before* they're even parsed, so most non-applicable auto-configs are eliminated cheaply. **OnBeanCondition and phase.** `@ConditionalOnBean`/`@ConditionalOnMissingBean` are backed by `OnBeanCondition`, which implements `ConfigurationCondition` and returns `ConfigurationPhase.REGISTER_BEAN`. This defers evaluation until after all configuration classes are parsed, so the registry reflects user-declared and earlier auto-config beans. Type matching uses the `@Bean` method's declared return type (and generic parameters), so an overly generic return type weakens matching — a documented pitfall. **Why back-off is deterministic.** Two ordering guarantees combine: 1. **User config before auto-config.** Boot registers/parses the user's `@Configuration` (from `@SpringBootApplication` component scanning and `@Import`) before it processes auto-configurations. So a user-defined `DataSource` is already in the registry when the auto-config's `@ConditionalOnMissingBean DataSource` runs → the auto-config backs off. This is the essence of 'user wins'. 2. **Auto-config internal ordering.** Auto-configurations are listed in `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` and ordered via `@AutoConfigureOrder`, `@AutoConfigureBefore`, `@AutoConfigureAfter`. Since `@ConditionalOnMissingBean` only sees beans registered so far, if auto-config B's default should back off when auto-config A supplies the bean, B must be ordered *after* A. **Key gotchas / senior insights.** - `@ConditionalOnMissingBean` belongs on *auto-config* beans, not user beans; the ordering guarantee only makes 'user wins' work in that direction. - Conditions cannot see beans from an auto-config that runs later — no forward references. Ordering annotations are the fix, not the condition itself. - Because Boot resolves class presence without initialization, you can safely name classes from optional dependencies in `@ConditionalOnClass(value=...)`; using `name="..."` (String) avoids even referencing the type in your own bytecode. - All of this is still 'just' `@Conditional`: you could write the same primitives yourself; Boot's value is the curated conditions plus the ordering/indexing machinery. **When it matters in practice.** Understanding this explains why 'my custom bean isn't overriding the auto-configured one' (wrong type match or `@ConditionalOnMissingBean` looking at the wrong type), why an auto-config 'doesn't kick in' (a required `@ConditionalOnClass` type absent), and why ordering annotations exist.
- How does @ConditionalOnClass avoid NoClassDefFoundError when the referenced class is absent?OnClassCondition tests presence via ClassUtils.isPresent/ASM inspection instead of loading/initializing the class, and using the String name attribute avoids referencing the type in your own bytecode. Absence yields a non-match rather than an error.
- Why does @ConditionalOnMissingBean give correct 'user wins' back-off but only in one direction?Boot parses user configuration before auto-configuration and OnBeanCondition runs in REGISTER_BEAN, so user beans are already registered when the auto-config condition evaluates. It only sees beans registered so far, so it can't back off for beans defined later.
- Two auto-configs both offer a default of type X; how do you make one defer to the other?Order them with @AutoConfigureBefore/@AutoConfigureAfter/@AutoConfigureOrder so the deferring one runs later, since @ConditionalOnMissingBean only sees beans registered before it.
saying these in an interview costs you the question
- Saying @ConditionalOnClass loads the class via Class.forName (it deliberately does not, to avoid errors)
- Claiming Boot invented a new mechanism separate from @Conditional
- Thinking @ConditionalOnMissingBean can back off for beans defined later without ordering annotations
- Ignoring that type matching relies on the @Bean method's declared return type