Walk through how a custom @EnableXxx annotation works end-to-end using @Import, and the constraints selectors/registrars operate under.
answer
- @EnableXxx = meta-annotation over @Import
- ConfigurationClassPostProcessor / ConfigurationClassParser drives it
- attributes flow via AnnotationMetadata.getAnnotationAttributes
- parse-time: no bean injection, but Aware works
- AdviceModeImportSelector picks config by mode attribute
basics
~20 sAn @EnableXxx annotation is a custom annotation meta-annotated with @Import pointing at a @Configuration class, an ImportSelector, or an ImportBeanDefinitionRegistrar. When you put @EnableXxx on your config, ConfigurationClassPostProcessor follows the @Import and applies the referenced configuration, passing along the annotation's attributes.
solid answer
~40 sThe @EnableXxx pattern is just meta-annotation over @Import. You define @interface EnableFoo meta-annotated with @Import(FooConfig.class | FooSelector.class | FooRegistrar.class). At startup, ConfigurationClassPostProcessor parses configuration classes, sees @EnableFoo (and thus the @Import), and applies the target. If it's a @Configuration, its @Bean methods register; if an ImportSelector, selectImports() returns config classes to add (deferred variant runs after user config); if an ImportBeanDefinitionRegistrar, it registers BeanDefinitions directly. Selectors/registrars receive AnnotationMetadata so they can read @EnableFoo's attributes and branch. All three run during parsing, before beans exist, so they can't autowire beans but can implement EnvironmentAware/BeanFactoryAware/ResourceLoaderAware/BeanClassLoaderAware, which Spring injects beforehand. This is exactly how @EnableTransactionManagement, @EnableCaching, @EnableAsync, @EnableWebMvc, and @MapperScan are built.
code
java · 24 lines// 1) The public entry-point annotation
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AuditConfigSelector.class) // meta-annotated @Import
public @interface EnableAuditing {
AuditMode mode() default AuditMode.SYNC;
}
// 2) A selector reads the attribute and picks the right config
public class AuditConfigSelector implements ImportSelector {
@Override public String[] selectImports(AnnotationMetadata metadata) {
var attrs = metadata.getAnnotationAttributes(EnableAuditing.class.getName());
AuditMode mode = (AuditMode) attrs.get("mode");
return switch (mode) {
case SYNC -> new String[]{ SyncAuditConfig.class.getName() };
case ASYNC -> new String[]{ AsyncAuditConfig.class.getName() };
};
}
}
// 3) Usage — the user just adds one annotation
@Configuration
@EnableAuditing(mode = AuditMode.ASYNC)
public class AppConfig { }go deeper
Know @EnableXxx turns on a feature; internals likely beyond scope.
State that @EnableXxx is @Import under the hood and passes attributes to the imported config.
Trace parsing by ConfigurationClassPostProcessor, attribute propagation via AnnotationMetadata, and the parse-time no-injection/Aware constraints.
Discuss target-type selection trade-offs, deferred vs regular ordering, AdviceModeImportSelector, startup-critical-path cost, and composition with conditions/profiles.
The `@EnableXxx` family (`@EnableTransactionManagement`, `@EnableCaching`, `@EnableAsync`, `@EnableScheduling`, `@EnableWebMvc`, `@EnableJpaRepositories`, `@MapperScan`, `@EnableFeignClients`, …) looks like magic but is a thin, uniform pattern layered on `@Import`. Understanding it lets you both read framework internals and author your own feature-toggle annotations. **1. Definition — a meta-annotated @Import.** An `@EnableXxx` is a custom annotation whose declaration carries `@Import(...)`: ```java @Target(TYPE) @Retention(RUNTIME) @Documented @Import(TransactionManagementConfigurationSelector.class) public @interface EnableTransactionManagement { boolean proxyTargetClass() default false; AdviceMode mode() default AdviceMode.PROXY; int order() default Ordered.LOWEST_PRECEDENCE; } ``` Because Spring resolves *meta-annotations*, putting `@EnableTransactionManagement` on your `@Configuration` is equivalent to putting the underlying `@Import` there. **2. The processor that drives it.** `ConfigurationClassPostProcessor` is a `BeanDefinitionRegistryPostProcessor` registered early. It parses each configuration class with `ConfigurationClassParser`, which discovers `@Import` (directly or via meta-annotation) and dispatches by target type: - **`@Configuration` / component class** → parsed as an imported configuration class; its `@Bean` methods become bean definitions. - **`ImportSelector`** → instantiated, `selectImports(metadata)` called, returned class names fed back into parsing. If it's a **`DeferredImportSelector`**, it's queued and processed only after all regular config classes are parsed (so user config wins — the basis of Boot auto-config precedence). - **`ImportBeanDefinitionRegistrar`** → instantiated, `registerBeanDefinitions(metadata, registry[, nameGen])` called to add definitions directly. **3. Attribute propagation via `AnnotationMetadata`.** The selector/registrar receives the `AnnotationMetadata` of the *annotated user class*. Calling `getAnnotationAttributes(EnableTransactionManagement.class.getName())` yields the `mode`, `proxyTargetClass`, `order` the user set. Spring's `AdviceModeImportSelector` base class reads `mode` and returns `ProxyTransactionManagementConfiguration` vs `AspectJ...Configuration`. This is how one annotation attribute rewires the whole imported configuration. **4. Lifecycle constraints (the important 'gotchas').** All of `@Import` processing happens during **configuration-class parsing**, which is *before* the `BeanFactory` finishes and *before* any singleton bean is instantiated. Therefore, inside an `ImportSelector` or `ImportBeanDefinitionRegistrar`: - **No bean injection** — you cannot `@Autowired` a bean; there are none yet. The selector/registrar class is itself *not* registered as a bean either (Spring news it up directly). - **Aware callbacks are honored** — implement `EnvironmentAware`, `BeanFactoryAware` (the `ConfigurableListableBeanFactory`), `ResourceLoaderAware`, `BeanClassLoaderAware`, and Spring injects them *before* invoking `selectImports`/`registerBeanDefinitions`. Use these for property reads, profile checks, classpath scanning, and name generation. - **@Conditional interplay** — imported config classes and their `@Bean`s can carry `@Conditional`; conditions are evaluated as those classes are processed. Deferred selectors interact with conditions like `@ConditionalOnMissingBean` correctly *because* they run last (that's a sibling-leaf topic, but the timing is what makes it coherent). - **Ordering** — deferred selectors honor `@Order`/`Ordered` and `getImportGroup()`; plain imports process in declaration order during parse. **5. Choosing the target type when you author @EnableXxx:** - Static, fixed configuration → `@Import(FooConfig.class)`. - Configuration that varies by attribute/environment/classpath → `ImportSelector` (or `DeferredImportSelector` if it must yield to user config). - A dynamic/variable set of beans or beans needing custom `BeanDefinition` shaping → `ImportBeanDefinitionRegistrar`. **6. Why this design.** It gives libraries a *single, discoverable annotation* as their public entry point while keeping the wiring logic encapsulated and testable, and it composes cleanly with `@ComponentScan`, `@Conditional`, and profiles. The cost: all this runs on the startup critical path and before the bean graph exists, so the code must be side-effect-light and injection-free. **Out of scope for this leaf:** the actual condition-evaluation semantics (`@Conditional`) and application-side functional registration (`GenericApplicationContext.registerBean`, Kotlin bean DSL) are separate mechanisms — here we only trace the `@Import`-driven path.
- Which infrastructure component actually reads @Import (and meta-annotated @EnableXxx) and applies it?ConfigurationClassPostProcessor, a BeanDefinitionRegistryPostProcessor that runs early. It uses ConfigurationClassParser to parse @Configuration classes, resolve meta-annotations, discover @Import, and dispatch to config classes, ImportSelectors (deferred ones queued to the end), and ImportBeanDefinitionRegistrars — all before singletons are instantiated.
- If you wanted your @EnableXxx to yield to a bean the user already defined, which import type and why?A DeferredImportSelector. It's processed after all user configuration is parsed, so conditions like @ConditionalOnMissingBean on your imported config evaluate against the complete user bean set and back off correctly, letting the user's bean win.
saying these in an interview costs you the question
- Believing @EnableXxx is special compiler/framework magic rather than meta-annotated @Import
- Saying selectors/registrars can autowire beans to make decisions
- Not knowing ConfigurationClassPostProcessor drives @Import processing
- Thinking annotation attributes reach the selector by any means other than AnnotationMetadata