IoC Containers and Bean Configuration with XML
In previous documents, we explored Design Principles (SOLID, IoC, DIP) and Layered Architecture (Model-Repository-Service). Now we will dive into the technical mechanism that enables this assembly: the Spring IoC Container and the declarative definition of Spring Beans via XML files.
In this guide, we will compare how Java applications worked before Spring, analyze the differences between BeanFactory and ApplicationContext, and build step-by-step a complete application connected via XML without using annotations.
1. How Projects Worked Before Spring
In traditional Java applications (Java SE or pure Servlets), the developer was responsible for manually instantiating and assembling all system layers using the new operator.
Issues with the Traditional Approach
Consider the code of a service without Spring:
package com.example.service;
import com.example.repository.EstudianteRepositoryInMemory;
public class EstudianteServiceImpl {
// Tight coupling to the concrete low-level class
private EstudianteRepositoryInMemory repositorio;
public EstudianteServiceImpl() {
// Class directly controls instantiation of its dependency
this.repositorio = new EstudianteRepositoryInMemory();
}
}
- Tight Coupling: To replace
EstudianteRepositoryInMemorywithEstudianteRepositoryDatabase, you must modifyEstudianteServiceImpland recompile the entire project. - Inability to Unit Test: You cannot pass mock objects of the repository to test business logic in isolation.
2. The Spring IoC Container
Spring solves this problem by introducing the IoC Container (Inversion of Control Container). The container is the central framework engine responsible for:
- Reading configuration metadata declared in the XML file (
applicationContext.xml). - Instantiating POJO classes as Spring Beans.
- Injecting required dependencies between them.
- Managing their lifecycle and scopes.
Types of IoC Containers in Spring
Spring provides two main interfaces representing the IoC container:
| IoC Container | Java Interface | Key Features | Recommended Use |
|---|---|---|---|
| Bean Factory | org.springframework.beans.factory.BeanFactory | Basic container. Provides core DI support and lazy loading (creates beans only when requested). | Reserved for devices with severely limited memory. |
| Application Context | org.springframework.context.ApplicationContext | Extends BeanFactory. Adds web integration, events, internationalization (I18N), and default eager loading. | Recommended standard choice for all enterprise applications. |
3. What is a Spring Bean?
A Spring Bean is any object whose instantiation, assembly, and lifecycle are managed entirely by the Spring IoC Container, which runs inside the Java Virtual Machine (JVM) process.
Unlike classes instantiated manually with new, Spring Beans coexist within the container as living components. The IoC container in the JVM handles connecting and injecting the Service Bean with the Repository Bean via Dependency Injection (DI).
Ways to Register a Bean in XML
In this module, bean registration is performed exclusively in the applicationContext.xml file using the <bean> tag:
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!-- Basic Bean Definition -->
<bean id="myBean" class="com.example.MyClass" />
</beans>
id: Unique identifier for the bean within the IoC container (repoBean,serviceBean,servletBean).class: Fully qualified name of the Java class to instantiate.
4. XML Dependency Injection in Layered Architecture
Taking the layered structure (Model - Repository - Service) defined in the previous document, we will see how to implement and inject dependencies via XML using two methods: Constructor Injection and Setter Injection.
A. Model Layer (Model POJO)
package com.example.model;
public class Estudiante {
private String id;
private String nombre;
private String correo;
public Estudiante() {
}
public Estudiante(String id, String nombre, String correo) {
this.id = id;
this.nombre = nombre;
this.correo = correo;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public String getCorreo() {
return correo;
}
public void setCorreo(String correo) {
this.correo = correo;
}
}
B. Repository Layer (Repository)
We define the repository interface and implementation:
package com.example.repository;
import com.example.model.Estudiante;
import java.util.List;
public interface EstudianteRepository {
List<Estudiante> obtenerTodos();
void guardar(Estudiante estudiante);
}
package com.example.repository;
import com.example.model.Estudiante;
import java.util.ArrayList;
import java.util.List;
public class EstudianteRepositoryInMemory implements EstudianteRepository {
private final List<Estudiante> estudiantes = new ArrayList<>();
public EstudianteRepositoryInMemory() {
estudiantes.add(new Estudiante("1", "Ana Gómez", "ana@icesi.edu.co"));
estudiantes.add(new Estudiante("2", "Carlos Pérez", "carlos@icesi.edu.co"));
}
@Override
public List<Estudiante> obtenerTodos() {
return new ArrayList<>(this.estudiantes);
}
@Override
public void guardar(Estudiante estudiante) {
this.estudiantes.add(estudiante);
}
}
C. Service Layer with Constructor Injection
The service class receives the repository through its constructor:
package com.example.service;
import com.example.model.Estudiante;
import com.example.repository.EstudianteRepository;
import java.util.List;
public class EstudianteServiceImpl implements EstudianteService {
private final EstudianteRepository estudianteRepository;
// Required dependency injected via constructor
public EstudianteServiceImpl(EstudianteRepository estudianteRepository) {
this.estudianteRepository = estudianteRepository;
}
@Override
public List<Estudiante> listarEstudiantes() {
return this.estudianteRepository.obtenerTodos();
}
@Override
public void registrarEstudiante(Estudiante estudiante) {
if (estudiante.getCorreo() == null || !estudiante.getCorreo().contains("@")) {
throw new IllegalArgumentException("Invalid email address");
}
this.estudianteRepository.guardar(estudiante);
}
}
Declaration in applicationContext.xml:
<!-- 1. Repository Bean -->
<bean id="estudianteRepositoryBean"
class="com.example.repository.EstudianteRepositoryInMemory" />
<!-- 2. Service Bean using Constructor Injection -->
<bean id="estudianteServiceBean"
class="com.example.service.EstudianteServiceImpl">
<constructor-arg ref="estudianteRepositoryBean" />
</bean>
D. Service Layer with Setter Injection
Alternatively, injection can be done by defining setter methods:
package com.example.service;
import com.example.model.Estudiante;
import com.example.repository.EstudianteRepository;
import java.util.List;
public class EstudianteServiceSetterImpl implements EstudianteService {
private EstudianteRepository estudianteRepository;
public EstudianteServiceSetterImpl() {
}
// Setter for dependency injection
public void setEstudianteRepository(EstudianteRepository estudianteRepository) {
this.estudianteRepository = estudianteRepository;
}
@Override
public List<Estudiante> listarEstudiantes() {
return this.estudianteRepository.obtenerTodos();
}
@Override
public void registrarEstudiante(Estudiante estudiante) {
this.estudianteRepository.guardar(estudiante);
}
}
Declaration in applicationContext.xml:
<!-- Setter Injection using property 'estudianteRepository' -->
<bean id="estudianteServiceSetterBean"
class="com.example.service.EstudianteServiceSetterImpl">
<property name="estudianteRepository" ref="estudianteRepositoryBean" />
</bean>
<constructor-arg ref="..." />: Used when the class has a constructor accepting dependencies. Guarantees immutability.<property name="..." ref="..." />: Uses the corresponding setter method (setEstudianteRepository). Requires a default no-arg constructor.