Este artículo detalla una implementación básica del Inversion of Control (IoC) utilizando Java, simulando algunas de las funcionalidades clave de un framework como Spring.
Definición de Beans (beans.properties)
Utilizamos un archivo de propiedades simple para definir nuestros beans. Cada línea representa una definición de bean con el formato nombreBean:claseCompleta.
userDao:com.example.beans.UserDao
Clase de Definición de Bean (BeanDefinition.java)
Esta clase actúa como un modelo de datos para almacenar la información de cada bean definida en el archivo de configuraicón.
public class BeanDefinition {
private String nombreBean;
private Class<?> claseBean;
// Getters y Setters omitidos para brevedad
public String getNombreBean() { return nombreBean; }
public void setNombreBean(String nombreBean) { this.nombreBean = nombreBean; }
public Class<?> getClaseBean() { return claseBean; }
public void setClaseBean(Class<?> claseBean) { this.claseBean = claseBean; }
}
Cargador de Recursos (ResourceLoader.java)
Este componente es responsable de leer el archivo de configuración (beans.properties) y parsear las definiciones de beans, creando instancias de BeanDefinition.
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
public class ResourceLoader {
public static Map<String, BeanDefinition> cargarConfiguracion() {
Map<String, BeanDefinition> definicionesBeans = new HashMap<>(16);
Properties propiedades = new Properties();
try (InputStream inputStream = ResourceLoader.class.getResourceAsStream("/beans.properties")) {
if (inputStream == null) {
throw new IOException("Archivo de configuración no encontrado: /beans.properties");
}
propiedades.load(inputStream);
Iterator<String> nombresBeans = propiedades.stringPropertyNames().iterator();
while (nombresBeans.hasNext()) {
String nombreBean = nombresBeans.next();
String nombreClase = propiedades.getProperty(nombreBean);
BeanDefinition definicion = new BeanDefinition();
definicion.setNombreBean(nombreBean);
definicion.setClaseBean(Class.forName(nombreClase));
definicionesBeans.put(nombreBean, definicion);
}
} catch (IOException | ClassNotFoundException e) {
System.err.println("Error al cargar la configuración: " + e.getMessage());
e.printStackTrace();
}
return definicionesBeans;
}
}
Registro de Beans (BeanRegistry.java)
Gestiona el almacenamiento y recuperación de beans singleton. En esta impleemntación simplificada, todos los beans son tratados como singletons y se guardan en un HashMap.
import java.util.HashMap;
import java.util.Map;
public class BeanRegistry {
private final Map<String, Object> beansSingleton = new HashMap<>(32);
public Object obtenerSingleton(String nombreBean) {
return beansSingleton.get(nombreBean);
}
public void registrarSingleton(String nombreBean, Object bean) {
if (!beansSingleton.containsKey(nombreBean)) {
beansSingleton.put(nombreBean, bean);
}
}
}
Factoría de Beans (BeanFactory.java)
El núcleo de nuestro contenedor IoC. Al instanciarse, carga las definiciones de beans y crea un registro. Al solicitar un bean, primero intenta recuperarlo del registro de singletons; si no existe, lo crea, lo registra y luego lo devuelve.
import java.util.Map;
public class BeanFactory {
private Map<String, BeanDefinition> definicionesBeans;
private BeanRegistry registroBeans;
public BeanFactory() {
this.registroBeans = new BeanRegistry();
this.definicionesBeans = ResourceLoader.cargarConfiguracion();
}
public Object obtenerBean(String nombreBean) {
Object bean = registroBeans.obtenerSingleton(nombreBean);
if (bean != null) {
return bean;
}
BeanDefinition definicion = definicionesBeans.get(nombreBean);
if (definicion == null) {
throw new IllegalArgumentException("No se encontró la definición para el bean: " + nombreBean);
}
return crearYRegistrarBean(definicion);
}
private Object crearYRegistrarBean(BeanDefinition definicion) {
try {
Object bean = definicion.getClaseBean().getDeclaredConstructor().newInstance();
registroBeans.registrarSingleton(definicion.getNombreBean(), bean);
return bean;
} catch (InstantiationException | IllegalAccessException | java.lang.reflect.InvocationTargetException | NoSuchMethodException e) {
System.err.println("Error al crear el bean '" + definicion.getNombreBean() + "': " + e.getMessage());
e.printStackTrace();
return null;
}
}
}
Bean de Ejemplo (UserDao.java)
Una clase simple que representa un bean gestionado por la factoría.
package com.example.beans;
public class UserDao {
public void consultarInformacionUsuario() {
System.out.println("Un buen tipo.");
}
}
Prueba de Unidad
Demuestra cómo interactuar con la BeanFactory para obtener e invocar métodos en beans.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
// Asegúrate de tener el archivo beans.properties en src/main/resources
// y las clases BeanDefinition, BeanRegistry, BeanFactory, UserDao en el classpath.
public class ApiTest {
@Test
void test_BeanFactory_SimulacionIoC() {
// 1. Inicializar la factoría (carga de configuración y registro)
BeanFactory factoria = new BeanFactory();
// 2. Primera obtención del bean 'userDao' (se crea y se cachea)
UserDao daoUsuario1 = (UserDao) factoria.obtenerBean("userDao");
assertNotNull(daoUsuario1, "El primer bean no debería ser nulo.");
daoUsuario1.consultarInformacionUsuario();
// 3. Segunda obtención del bean 'userDao' (se recupera de la caché)
UserDao daoUsuario2 = (UserDao) factoria.obtenerBean("userDao");
assertNotNull(daoUsuario2, "El segundo bean no debería ser nulo.");
// Verificamos que ambos objetos son la misma instancia (singleton)
assertSame(daoUsuario1, daoUsuario2, "Ambas instancias deben ser la misma (singleton).");
daoUsuario2.consultarInformacionUsuario();
}
}
Resultado Esperado
La ejecución de la prueba de unidad producirá la siguiente salida en la consola:
Un buen tipo.
Un buen tipo.