Understanding Spring's internal architecture begins with its foundational abstractions. This article explores the essential interfaces and classes that form the backbone of Spring’s IoC container, clarifying their responsibilities, relationships, and practical usage patterns — without relying on high-level abstractions like @SpringBootApplication.
BeanDefinition: The Blueprint for Beans
A BeanDefinition encapsulates metadata required to instantiate, configure, and manage a bean. Its not the bean itself, but rather a configuration contract containing:
beanClass: Target class type (e.g.,UserService.class)scope: Lifecycle scope (singleton,prototype, etc.)lazyInit: Whether instantiation should be deferred until first accessdependsOn: Names of beans that must be initialized before this oneinitMethodName/destroyMethodName: Lifecycle callback method names
Both declarative definitions (e.g., @Component, @Bean) and programmatic registration ultimately produce BeanDefinition instances. For example:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
AbstractBeanDefinition def = BeanDefinitionBuilder
.genericBeanDefinition(UserService.class)
.getBeanDefinition();
def.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
ctx.registerBeanDefinition("userSvc", def);
BeanDefinitionReader: Configuration Loader Abstraction
This interface standardizes how external configuration sources are parsed in to BeanDefinitions and registered in a BeanDefinitionRegistry. Though rarely used directly by application code, it underpins all configuration loading mechanisms.
XmlBeanDefinitionReader
Loads bean definitions from XML resources:
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(ctx);
int loaded = reader.loadBeanDefinitions("classpath:spring-config.xml");
AnnotatedBeanDefinitionReader
Registers classes annotated with stereotype annotations or @Bean-producing methods programmatically:
AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(ctx);
reader.register(UserService.class, OrderService.class);
It processes annotations such as @Scope, @Lazy, @Primary, and @Conditional, enabling conditional registration logic.
ClassPathBeanDefinitionScanner
Performs classpath scanning to detect and register candidate components:
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(ctx);
scanner.addIncludeFilter(new AnnotationTypeFilter(Component.class));
scanner.scan("com.example.app");
Filters support custom matching rules beyond built-in annotations like @Component, @Named, or @ManagedBean.
BeanFactory: Core Container Interface
The BeanFactory interface defines the minimal contract for a factory capable of managing named objects (beans), including:
getBean(String name)andgetBean(Class<T> type)containsBean(String name)isSingleton(String name),isPrototype(String name)getType(String name),getAliases(String name)
It serves as the foundation for all Spring-managed object resolution — no dependency injection or lifecycle management happens outside this contract.
DefaultListableBeanFactory: The Default Implementation
This concrete implementation provides full support for:
- Registration and retrieval of
BeanDefinitions via an internalConcurrentHashMap<String, BeanDefinition> - Dependency resolution using
doResolveDependency() - Autowiring candidate selection (e.g., honoring
@Primary) - Eager singleton instantiation via
preInstantiateSingletons()
It implements multiple interfaces (ListableBeanFactory, AutowireCapableBeanFactory, ConfigurableBeanFactory), making it central to most non-web Spring contexts.
AbstractBeanFactory & AbstractAutowireCapableBeanFactory
AbstractBeanFactory introduces the core doGetBean() algorithm — handling scope resolution, prototype creation, and circular reference detection. Its subclass AbstractAutowireCapableBeanFactory adds full instantiation and initialization logic:
createBeanInstance(): Instantiates via constructor inference or factory methodpopulateBean(): Injects dependencies (by name or type)initializeBean(): InvokesInitializingBean,@PostConstruct, and custom init methodsapplyBeanPostProcessorsBeforeInitialization()andAfterInitialization()
These layers separate concerns: resolution → instantiation → population → initialization → post-processing.
HierarchicalBeanFactory
This interface enables nested container hierarchies. A child BeanFactory can delegate unresolved bean lookups to its parent, allowing shared infrastructure beans (e.g., data sources) while isolating application-specific beans. Merged bean definitions rely on this delegation to resolve overridden properties.
ApplicationContext: Enhanced BeanFactory
An ApplicationContext extends BeanFactory with enterprise features:
- Event publishing (
ApplicationEventPublisher) - Internationalization (
MessageSource) - Resource loading (
ResourcePatternResolver) - Environment-aware configuration (
EnvironmentCapable)
Concrete implementations include:
AnnotationConfigApplicationContext: Bootstraps from Java config classes and performs component scanningClassPathXmlApplicationContext: Loads XML-based configurations
Extension Points: Post-Processors and Factories
BeanPostProcessor
Allows interception of bean initialization phases. Multiple processors are ordered and executed before/after initialization:
public class LoggingBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String name) {
System.out.println("Initializing: " + name);
return bean;
}
}
BeanFactoryPostProcessor
Modifies the BeanFactory itself before any beans are instantiated — commonly used by frameworks to alter bean definitions globally (e.g., PropertySourcesPlaceholderConfigurer):
public class CustomBeanFactoryPP implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {
// Modify bean definitions here
}
}
FactoryBean
Provides fine-grained control over bean creation. Unlike regular beans, FactoryBean instances are managed by Spring but produce arbitrary objects via getObject():
@Component
public class DataSourceFactoryBean implements FactoryBean<DataSource> {
@Override
public DataSource getObject() { return buildCustomDataSource(); }
@Override
public Class<?> getObjectType() { return DataSource.class; }
}
Note: Objects returned by FactoryBean.getObject() bypass standard Spring lifecycle callbacks and dependency injection unless explicitly applied.
Metadata Reading with ASM
To avoid expensive classloading during startup, Spring uses MetadataReader (backed by ASM) to read class and annotation metadata directly from bytecode:
MetadataReaderFactory factory = new SimpleMetadataReaderFactory();
MetadataReader reader = factory.getMetadataReader("com.example.service.UserService");
System.out.println(reader.getClassMetadata().getClassName());
System.out.println(reader.getAnnotationMetadata().getAnnotationTypes());
This mechanism powers annotation-based configuration without triggering full class initialization.