Skip to main content

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:

java
public class UserService {
private UserRepository userRepository;

public UserService() {
// Service creates its own dependency
this.userRepository = new UserRepository();
}

// Methods that use userRepository
}

With IoC:

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

  1. Creating both the UserService and UserRepository objects
  2. Recognizing that UserService needs a UserRepository
  3. 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:

  1. BeanFactory - The basic container that provides fundamental IoC functionality
  2. 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:

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

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

java
@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:

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

java
@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:

java
@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:

  1. Instantiation: Spring creates an instance of the bean
  2. Populating properties: Dependencies are injected
  3. BeanNameAware/BeanFactoryAware: Interface callbacks (if implemented)
  4. Pre-initialization: @PostConstruct methods or InitializingBean interface
  5. Initialization: Custom init-method
  6. Post-initialization: PostProcessor callbacks
  7. Bean is ready for use
  8. Destruction: When container is closed, @PreDestroy methods or DisposableBean interface

You can hook into this lifecycle with annotations:

java
@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:

  1. singleton (default): One instance per container
  2. prototype: New instance each time the bean is requested
  3. request: One instance per HTTP request (web-aware contexts)
  4. session: One instance per HTTP session (web-aware contexts)
  5. application: One instance per ServletContext (web-aware contexts)
  6. websocket: One instance per WebSocket (web-aware contexts)

Example of specifying a scope:

java
@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:

java
// 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:

  1. The Spring container creates instances of BlogRepository and BlogService
  2. It automatically injects the repository into the service
  3. Our main method simply retrieves the service from the container and uses it
  4. 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:

java
@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:

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

java
@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:

java
@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:

  1. Use setter injection instead of constructor injection
  2. Redesign to remove the circular dependency
  3. Use @Lazy annotation
java
@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:

java
@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:

  1. Takes control of object creation - You define what you need, Spring instantiates and manages objects
  2. Handles dependency injection - Spring automatically wires your components together
  3. Manages bean lifecycle - From creation through destruction
  4. Supports multiple configuration approaches - XML, Java, and annotation-based
  5. 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:

  1. Official Spring Documentation
  2. Spring in Action by Craig Walls
  3. Spring Guides - Hands-on tutorials for various Spring topics

Practice Exercises

  1. Create a simple Spring application with a service that depends on two repositories. Configure it using:

    • XML configuration
    • Java-based configuration
    • Annotation-based configuration
  2. Implement a bean with custom initialization and destruction methods, and demonstrate its lifecycle.

  3. Create a prototype-scoped bean and a singleton-scoped bean, and observe how the container manages them.

  4. Build a small application with at least three layers (controller, service, repository) and use dependency injection to wire them together.

  5. 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! :)