Spring IoC Container
Introduction
At the heart of the Spring Framework lies one of its core features: the Inversion of Control (IoC) container. If you're new to Spring, understanding this concept is fundamental to mastering the framework. But don't worry—we'll break it down step by step!
In traditional programming, your application code controls the flow and creates objects when needed. With IoC, this control is "inverted"—the Spring container takes charge of object creation, wiring dependencies, and managing their lifecycle. This approach leads to more modular, testable, and maintainable code.
In this guide, we'll explore:
- What IoC means and why it's beneficial
- How Spring implements IoC through its container
- Different container types and configurations
- Practical examples to solidify your understanding
What is Inversion of Control?
The Traditional Approach vs. IoC
Let's understand the difference with a simple example:
Traditional approach:
public class UserService {
private UserRepository userRepository;
public UserService() {
// Service creates its own dependency
this.userRepository = new UserRepository();
}
// Methods that use userRepository
}
With IoC:
public class UserService {
private UserRepository userRepository;
// Constructor injection - Spring will provide the dependency
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// Methods that use userRepository
}
In the IoC approach, the UserService
no longer creates the UserRepository
—it simply declares that it needs one. The Spring container is responsible for:
- Creating both the
UserService
andUserRepository
objects - Recognizing that
UserService
needs aUserRepository
- Providing (or "injecting") the repository into the service
This is often referred to as Dependency Injection (DI), which is a specific form of IoC.
The Spring IoC Container
Spring provides IoC container functionality through two core interfaces:
- BeanFactory - The basic container that provides fundamental IoC functionality
- ApplicationContext - An advanced container that extends BeanFactory with enterprise features
For most applications, especially as a beginner, you'll work with the ApplicationContext
.
Understanding Spring Beans
In Spring terminology, the objects managed by the IoC container are called beans. A bean is simply an object that is instantiated, assembled, and managed by the Spring IoC container.
Configuring the Spring IoC Container
There are three main ways to configure the Spring IoC container:
1. XML-based Configuration
This was the original method and is still used in many legacy applications:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userRepository" class="com.example.UserRepository" />
<bean id="userService" class="com.example.UserService">
<constructor-arg ref="userRepository" />
</bean>
</beans>
Loading this configuration:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean("userService", UserService.class);
2. Java-based Configuration
Modern Spring applications often use Java-based configuration:
@Configuration
public class AppConfig {
@Bean
public UserRepository userRepository() {
return new UserRepository();
}
@Bean
public UserService userService() {
// The container automatically wires dependencies
return new UserService(userRepository());
}
}
Loading this configuration:
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean("userService", UserService.class);
3. Annotation-based Configuration
This approach uses annotations in your component classes:
@Repository
public class UserRepository {
// Implementation
}
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// Implementation
}
To enable component scanning:
@Configuration
@ComponentScan("com.example")
public class AppConfig {
// No need to define beans manually
}
Understanding Bean Lifecycle
The Spring container manages the complete lifecycle of beans:
- Instantiation: Spring creates an instance of the bean
- Populating properties: Dependencies are injected
- BeanNameAware/BeanFactoryAware: Interface callbacks (if implemented)
- Pre-initialization:
@PostConstruct
methods or InitializingBean interface - Initialization: Custom init-method
- Post-initialization: PostProcessor callbacks
- Bean is ready for use
- Destruction: When container is closed,
@PreDestroy
methods or DisposableBean interface
You can hook into this lifecycle with annotations:
@Component
public class DatabaseService {
@PostConstruct
public void initialize() {
System.out.println("Establishing database connection...");
// Initialization code
}
@PreDestroy
public void cleanup() {
System.out.println("Closing database connection...");
// Cleanup code
}
}
Bean Scopes
Spring supports several bean scopes that determine the lifecycle and visibility of beans:
- singleton (default): One instance per container
- prototype: New instance each time the bean is requested
- request: One instance per HTTP request (web-aware contexts)
- session: One instance per HTTP session (web-aware contexts)
- application: One instance per ServletContext (web-aware contexts)
- websocket: One instance per WebSocket (web-aware contexts)
Example of specifying a scope:
@Component
@Scope("prototype")
public class ShoppingCart {
// Every user needs their own shopping cart
}
Practical Example: Building a Simple Application
Let's build a simple blog application to see how the IoC container works in practice:
// Define the model
public class BlogPost {
private Long id;
private String title;
private String content;
// Constructors, getters, setters
}
// Repository layer
@Repository
public class BlogRepository {
private Map<Long, BlogPost> posts = new HashMap<>();
private AtomicLong idSequence = new AtomicLong(1);
public BlogPost save(BlogPost post) {
if (post.getId() == null) {
post.setId(idSequence.getAndIncrement());
}
posts.put(post.getId(), post);
System.out.println("Saved post with ID: " + post.getId());
return post;
}
public BlogPost findById(Long id) {
System.out.println("Finding post with ID: " + id);
return posts.get(id);
}
public List<BlogPost> findAll() {
return new ArrayList<>(posts.values());
}
}
// Service layer
@Service
public class BlogService {
private final BlogRepository blogRepository;
@Autowired
public BlogService(BlogRepository blogRepository) {
this.blogRepository = blogRepository;
}
public BlogPost createPost(String title, String content) {
BlogPost post = new BlogPost();
post.setTitle(title);
post.setContent(content);
return blogRepository.save(post);
}
public BlogPost getPost(Long id) {
return blogRepository.findById(id);
}
public List<BlogPost> getAllPosts() {
return blogRepository.findAll();
}
}
// Configuration
@Configuration
@ComponentScan("com.example.blog")
public class BlogAppConfig {
// Component scanning will find our annotated classes
}
// Main application
public class BlogApplication {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(BlogAppConfig.class);
// Get the BlogService bean
BlogService blogService = context.getBean(BlogService.class);
// Use the service
blogService.createPost("Spring IoC Container", "Learn about Spring's IoC container");
blogService.createPost("Dependency Injection", "Understanding DI in Spring");
System.out.println("\nAll posts:");
blogService.getAllPosts().forEach(post ->
System.out.println(post.getId() + ": " + post.getTitle())
);
System.out.println("\nFetching specific post:");
BlogPost post = blogService.getPost(1L);
System.out.println("Post title: " + post.getTitle());
}
}
Output:
Saved post with ID: 1
Saved post with ID: 2
All posts:
1: Spring IoC Container
2: Dependency Injection
Fetching specific post:
Finding post with ID: 1
Post title: Spring IoC Container
In this example:
- The Spring container creates instances of
BlogRepository
andBlogService
- It automatically injects the repository into the service
- Our main method simply retrieves the service from the container and uses it
- We didn't have to manually create or wire any objects
Advanced Features
Factory Beans
Sometimes you need complex instantiation logic for beans. Spring provides FactoryBean
for this purpose:
@Component
public class DataSourceFactoryBean implements FactoryBean<DataSource> {
@Override
public DataSource getObject() throws Exception {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:mem:testdb");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
@Override
public Class<?> getObjectType() {
return DataSource.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
Bean Definition Inheritance
In XML configuration, you can create bean definitions that inherit from others:
<bean id="baseRepository" abstract="true">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userRepository" class="com.example.UserRepository" parent="baseRepository">
<!-- Inherits dataSource property from parent -->
<property name="tableName" value="users" />
</bean>
Method Injection
For rare cases where a singleton bean needs to use a prototype bean:
@Component
@Scope("singleton")
public abstract class TicketService {
// Spring will implement this method to return a new instance each time
@Lookup
public abstract TicketGenerator getTicketGenerator();
public Ticket generateTicket(String customerName) {
// Gets a fresh instance of TicketGenerator each time
TicketGenerator generator = getTicketGenerator();
return generator.createTicket(customerName);
}
}
@Component
@Scope("prototype")
public class TicketGenerator {
private final AtomicInteger counter = new AtomicInteger(1);
public Ticket createTicket(String customerName) {
return new Ticket(counter.getAndIncrement(), customerName, new Date());
}
}
Common Challenges and Solutions
Circular Dependencies
When Bean A depends on Bean B, and Bean B depends on Bean A, you have a circular dependency:
@Component
public class BeanA {
private final BeanB beanB;
@Autowired
public BeanA(BeanB beanB) { // Requires BeanB
this.beanB = beanB;
}
}
@Component
public class BeanB {
private final BeanA beanA;
@Autowired
public BeanB(BeanA beanA) { // Requires BeanA
this.beanA = beanA;
}
}
This creates a chicken-and-egg problem. Solutions include:
- Use setter injection instead of constructor injection
- Redesign to remove the circular dependency
- Use
@Lazy
annotation
@Component
public class BeanA {
private final BeanB beanB;
@Autowired
public BeanA(@Lazy BeanB beanB) { // Will use proxy initially
this.beanB = beanB;
}
}
Bean Initialization Order
Sometimes you need beans to initialize in a specific order:
@Component
@DependsOn("databaseInitializer")
public class UserService {
// This bean will only be initialized after databaseInitializer
}
Summary
The Spring IoC Container is a fundamental component of the Spring Framework that:
- Takes control of object creation - You define what you need, Spring instantiates and manages objects
- Handles dependency injection - Spring automatically wires your components together
- Manages bean lifecycle - From creation through destruction
- Supports multiple configuration approaches - XML, Java, and annotation-based
- Provides enterprise-ready features - Scoping, initialization ordering, factory patterns
By embracing the IoC principle, your code becomes:
- More modular and maintainable
- Easier to test (dependencies can be mocked)
- More loosely coupled (components don't directly create their dependencies)
- Focused on business logic rather than infrastructure concerns
Additional Resources
To further your understanding of Spring's IoC Container:
- Official Spring Documentation
- Spring in Action by Craig Walls
- Spring Guides - Hands-on tutorials for various Spring topics
Practice Exercises
-
Create a simple Spring application with a service that depends on two repositories. Configure it using:
- XML configuration
- Java-based configuration
- Annotation-based configuration
-
Implement a bean with custom initialization and destruction methods, and demonstrate its lifecycle.
-
Create a prototype-scoped bean and a singleton-scoped bean, and observe how the container manages them.
-
Build a small application with at least three layers (controller, service, repository) and use dependency injection to wire them together.
-
Experiment with different types of dependency injection (constructor, setter, field) and compare the approaches.
Happy Spring coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)