Compreender os fundamentos de um contêiner de Inversão de Controle (IoC) é crucial para desenvolvedores Java, especialmente para aqueles que trabalham com frameworks como o Spring. Este artigo demonstra a criação de um contêiner IoC simplificado, revelando os mecanismos subjacentes que permitem a injeção de dependências e o gerenciamento de componentes.
Os conceitos centrais explorados nesta implementação incluem:
- Reflexão Java: Utilizada para inspecionar e manipular classes, métodos e campos em tempo de execução, permitindo a descoberta e instanciação dinâmica de componentes.
- Proxies Dinâmicos JDK: Empregados para criar objetos proxy que podem interceptar chamadas de método para um componente, adicionando funcionalidades como logging ou transações sem modificar o código original do componente.
- Anotações Personalizadas: Usadas para marcar classes como componentes gerenciáveis e campos para injeção de dependências.
A seguir, apresentamos o código-fonte para um contêiner IoC mínimo e um exemplo de como utilizá-lo.
- Ferramentas e Anotações Essenciais
1.1. Utilitário de Scanner de Pacotes Este utilitário é responsável por escanear um pacote base e recursivamente encontrar todos os nomes de classes (.class) nele contidos.
package org.simpledi.core.util;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* Utilitário para escanear pacotes e encontrar nomes de classes.
*/
public class PackageScanner {
/**
* Encontra os nomes completos de todas as classes dentro de um pacote especificado.
*
* @param basePackageName O nome do pacote a ser escaneado (ex: "com.example.app").
* @param recursive Se deve escanear subpacotes recursivamente.
* @return Um conjunto de nomes de classes completos.
*/
public static Set<String> findClassNames(String basePackageName, boolean recursive) {
Set<String> classNames = new HashSet<>();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String packagePath = basePackageName.replace('.', '/');
URL resource = classLoader.getResource(packagePath);
if (resource == null) {
System.err.println("WARN: Recurso para o pacote não encontrado: " + basePackageName);
return Collections.emptySet();
}
try {
URI uri = resource.toURI();
File directory = new File(uri);
if (directory.exists() && directory.isDirectory()) {
scanDirectory(directory, basePackageName, recursive, classNames);
} else {
System.err.println("WARN: O caminho do pacote não é um diretório: " + directory.getAbsolutePath());
}
} catch (URISyntaxException | IOException e) {
System.err.println("ERRO: Falha ao escanear o pacote '" + basePackageName + "': " + e.getMessage());
}
return classNames;
}
/**
* Escaneia um diretório em busca de arquivos .class.
*
* @param directory O diretório a ser escaneado.
* @param currentPackage O nome do pacote atual para as classes encontradas neste diretório.
* @param recursive Se deve continuar escaneando subdiretórios.
* @param classNames O conjunto para adicionar os nomes das classes encontradas.
* @throws IOException Se ocorrer um erro de I/O.
*/
private static void scanDirectory(File directory, String currentPackage, boolean recursive, Set<String> classNames) throws IOException {
File[] files = directory.listFiles();
if (files == null) {
return;
}
for (File file : files) {
if (file.isDirectory()) {
if (recursive) {
scanDirectory(file, currentPackage + "." + file.getName(), recursive, classNames);
}
} else {
String fileName = file.getName();
if (fileName.endsWith(".class") && !fileName.contains("$")) { // Excluir classes internas/anônimas
String className = currentPackage + "." + fileName.substring(0, fileName.length() - ".class".length());
classNames.add(className);
}
}
}
}
}
1.2. Utilitário de Texto Uma classe utilitária simples para manipulação de strings.
package org.simpledi.core.util;
/**
* Utilitário para operações básicas de string.
*/
public class TextUtil {
/**
* Verifica se uma string é nula ou vazia após remoção de espaços em branco.
*
* @param text A string a ser verificada.
* @return true se a string for nula ou vazia, false caso contrário.
*/
public static boolean isEmptyOrNull(String text) {
return text == null || text.trim().isEmpty();
}
/**
* Verifica se uma string não é nula e não está vazia após remoção de espaços em branco.
*
* @param text A string a ser verificada.
* @return true se a string contiver conteúdo, false caso contrário.
*/
public static boolean isPresent(String text) {
return !isEmptyOrNull(text);
}
}
1.3. Anotação @ComponentUsada para marcar classes que devem ser gerenciadas pelo contêiner IoC.
package org.simpledi.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Anotação para marcar classes como componentes gerenciáveis pelo contêiner DI.
* Pode especificar um nome opcional para o componente.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
/**
* O nome do componente dentro do contêiner. Se vazio, o nome será inferido
* (geralmente o nome da classe em minúsculas).
* @return O nome do componente.
*/
String name() default "";
}
1.4. Anotação @InjectUsada para marcar campos onde o contêiner deve injetar uma dependência.
package org.simpledi.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Anotação para marcar campos que devem ter dependências injetadas pelo contêiner DI.
* Pode especificar o nome do bean a ser injetado.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {
/**
* O nome do bean a ser injetado. Se vazio, a injeção ocorrerá por tipo.
* @return O nome do bean.
*/
String beanName() default "";
}
- Componentes Principais do Contêiner
2.1. Definição de Componente (BeanDefinition) Representa os metadados e as instâncias de um componente gerenciado pelo contêiner.
package org.simpledi.core;
import java.util.Optional;
/**
* Define os metadados e as instâncias associadas a um componente gerenciado pelo contêiner DI.
*/
public class BeanDefinition {
private Class<?> implementationClass;
private String identifier; // Nome ou ID do bean no contêiner
private Class<?> contractType; // Tipo pelo qual o bean é conhecido (interface ou classe de implementação)
private Object rawInstance; // A instância original do objeto
private Object proxyInstance; // A instância proxy, se aplicável
public BeanDefinition(Class<?> implementationClass) {
this.implementationClass = implementationClass;
}
public Class<?> getImplementationClass() {
return implementationClass;
}
public void setImplementationClass(Class<?> implementationClass) {
this.implementationClass = implementationClass;
}
public String getIdentifier() {
return identifier;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public Class<?> getContractType() {
return contractType;
}
public void setContractType(Class<?> contractType) {
this.contractType = contractType;
}
public Object getRawInstance() {
return rawInstance;
}
public void setRawInstance(Object rawInstance) {
this.rawInstance = rawInstance;
}
public Optional<Object> getProxyInstance() {
return Optional.ofNullable(proxyInstance);
}
public void setProxyInstance(Object proxyInstance) {
this.proxyInstance = proxyInstance;
}
/**
* Retorna a instância gerenciada do componente (o proxy, se existir, caso contrário, a instância bruta).
* @return A instância do componente.
*/
public Object getManagedInstance() {
return proxyInstance != null ? proxyInstance : rawInstance;
}
}
2.2. Contexto da Aplicação (ApplicationContext) Atua como o registro central para todas as instâncias de compoenntes.
package org.simpledi.core;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* Representa o contexto da aplicação, armazenando e fornecendo acesso aos componentes gerenciados.
*/
public class ApplicationContext {
private final Map<String, Object> namedComponents = new HashMap<>();
private final Map<Class<?>, Object> typedComponents = new HashMap<>();
/**
* Obtém um componente registrado por nome e tipo.
* @param name O nome do componente.
* @param type O tipo esperado do componente.
* @param <T> O tipo do componente.
* @return Um Optional contendo a instância do componente, se encontrada.
*/
public <T> Optional<T> getComponent(String name, Class<T> type) {
Object component = namedComponents.get(name);
return Optional.ofNullable(type.cast(component));
}
/**
* Obtém um componente registrado por tipo.
* @param type O tipo do componente.
* @param <T> O tipo do componente.
* @return Um Optional contendo a instância do componente, se encontrada.
*/
public <T> Optional<T> getComponent(Class<T> type) {
Object component = typedComponents.get(type);
return Optional.ofNullable(type.cast(component));
}
/**
* Registra um componente com um nome.
* @param name O nome do componente.
* @param instance A instância do componente.
*/
public void registerComponent(String name, Object instance) {
if (name != null && !name.trim().isEmpty()) {
namedComponents.put(name, instance);
}
}
/**
* Registra um componente com seu tipo de contrato.
* @param type O tipo de contrato do componente (geralmente uma interface).
* @param instance A instância do componente.
*/
public void registerComponent(Class<?> type, Object instance) {
typedComponents.put(type, instance);
}
}
2.3. Log de Invocação de Proxy (InvocationLogger) Um InvocationHandler simples que adiciona logging antes e depois da execução de um método.
package org.simpledi.core;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* Um manipulador de invocação para proxies dinâmicos JDK que registra a execução do método.
* Demonstra um aspecto simples como logging.
*/
public class InvocationLogger implements InvocationHandler {
private final Object targetInstance;
public InvocationLogger(Object targetInstance) {
this.targetInstance = targetInstance;
}
/**
* Intercepta a chamada de método para a instância alvo, adicionando lógica de log.
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("LOG >> Antes da execução do método: " + method.getName());
long startTime = System.nanoTime();
Object result = method.invoke(targetInstance, args);
long endTime = System.nanoTime();
System.out.println("LOG << Depois da execução do método: " + method.getName() + " (" + (endTime - startTime) / 1_000_000.0 + " ms)");
return result;
}
}
2.4. Construtor do Contêiner (ContainerBuilder) A classe principal que orquestra o escaneamento, instanciação, injeção de dependências e gerenciamento de componentes.
package org.simpledi.core;
import org.simpledi.core.annotation.Component;
import org.simpledi.core.annotation.Inject;
import org.simpledi.core.util.PackageScanner;
import org.simpledi.core.util.TextUtil;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* Construtor do Contêiner DI (Inversão de Controle).
* Responsável por escanear pacotes, instanciar componentes, resolver dependências e gerenciar o ciclo de vida básico.
*/
public class ContainerBuilder {
private final String scanPackage;
private final ApplicationContext appContext;
private final List<BeanDefinition> registeredDefinitions = new ArrayList<>();
/**
* Constrói o contêiner DI, iniciando o processo de escaneamento e configuração.
* @param scanPackage O pacote base para procurar por componentes.
*/
public ContainerBuilder(String scanPackage) {
this.scanPackage = scanPackage;
this.appContext = new ApplicationContext();
initializeContainer();
}
/**
* Inicializa o contêiner, descobrindo e instanciando componentes, e depois resolvendo suas dependências.
*/
private void initializeContainer() {
System.out.println("DEBUG: Iniciando descoberta e instanciação de componentes no pacote: " + scanPackage);
discoverAndInstantiateComponents();
System.out.println("DEBUG: Iniciando resolução de dependências para componentes.");
resolveDependencies();
System.out.println("DEBUG: Contêiner totalmente inicializado.");
}
/**
* Escaneia o pacote base, identifica classes anotadas com @Component, as instancia e as registra no contexto.
*/
private void discoverAndInstantiateComponents() {
Set<String> classNames = PackageScanner.findClassNames(scanPackage, true);
for (String className : classNames) {
try {
Class<?> componentClass = Class.forName(className);
if (componentClass.isAnnotationPresent(Component.class)) {
Component componentAnnotation = componentClass.getAnnotation(Component.class);
String componentName = componentAnnotation.name();
BeanDefinition beanDef = new BeanDefinition(componentClass);
beanDef.setIdentifier(TextUtil.isPresent(componentName) ? componentName : componentClass.getSimpleName().toLowerCase());
Class<?>[] interfaces = componentClass.getInterfaces();
boolean hasInterfaces = interfaces.length > 0;
Class<?> contractType = hasInterfaces ? interfaces[0] : componentClass;
beanDef.setContractType(contractType);
Object rawInstance = componentClass.getDeclaredConstructor().newInstance();
beanDef.setRawInstance(rawInstance);
Object managedInstance = rawInstance;
if (hasInterfaces) {
// Se houver interfaces, criamos um proxy para permitir intercepção
Object proxy = createProxyForComponent(rawInstance);
beanDef.setProxyInstance(proxy);
managedInstance = proxy; // O contêiner gerenciará a instância proxy
}
// Registrar no ApplicationContext
appContext.registerComponent(beanDef.getIdentifier(), managedInstance);
appContext.registerComponent(contractType, managedInstance);
registeredDefinitions.add(beanDef);
System.out.println("DEBUG: Componente registrado: " + beanDef.getIdentifier() + " de tipo " + contractType.getName());
}
} catch (Exception e) {
System.err.println("ERRO: Falha ao registrar componente '" + className + "': " + e.getMessage());
}
}
}
/**
* Itera sobre todos os componentes registrados e injeta suas dependências.
*/
private void resolveDependencies() {
for (BeanDefinition beanDef : registeredDefinitions) {
Object instanceToInjectInto = beanDef.getRawInstance(); // Injetar na instância bruta
for (Field field : instanceToInjectInto.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class)) {
Inject injectAnnotation = field.getAnnotation(Inject.class);
String dependencyName = injectAnnotation.beanName();
Class<?> dependencyType = field.getType();
Optional<Object> dependencyInstance;
if (TextUtil.isPresent(dependencyName)) {
dependencyInstance = appContext.getComponent(dependencyName, dependencyType);
} else {
dependencyInstance = appContext.getComponent(dependencyType);
}
dependencyInstance.ifPresentOrElse(dep -> {
try {
field.setAccessible(true); // Permitir acesso a campos privados
field.set(instanceToInjectInto, dep);
System.out.println("DEBUG: Injetada dependência '" + field.getName() + "' (" + dep.getClass().getSimpleName() + ") em " + instanceToInjectInto.getClass().getSimpleName());
} catch (IllegalAccessException e) {
System.err.println("ERRO: Falha ao injetar dependência no campo " + field.getName() + " de " + instanceToInjectInto.getClass().getName() + " - " + e.getMessage());
}
}, () -> System.err.println("ERRO: Dependência não encontrada para o campo " + field.getName() + " de tipo " + dependencyType.getName() + " em " + instanceToInjectInto.getClass().getName()));
}
}
}
}
/**
* Cria um proxy dinâmico para um objeto, usando o InvocationLogger.
* @param target A instância do objeto a ser proxy.
* @return A instância proxy.
*/
private Object createProxyForComponent(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationLogger(target)
);
}
/**
* Recupera um componente do contêiner por seu tipo.
* @param type O tipo do componente.
* @param <T> O tipo esperado do componente.
* @return Um Optional contendo a instância do componente, se encontrada.
*/
public <T> Optional<T> getComponent(Class<T> type) {
return appContext.getComponent(type);
}
/**
* Recupera um componente do contêiner por seu nome e tipo.
* @param name O nome do componente.
* @param type O tipo esperado do componente.
* @param <T> O tipo esperado do componente.
* @return Um Optional contendo a instância do componente, se encontrada.
*/
public <T> Optional<T> getComponent(String name, Class<T> type) {
return appContext.getComponent(name, type);
}
}
- Exemplo de Uso
Vamos criar um pequeno conjunto de classes para demonstrar o funcionamento do nosso contêiner IoC.
3.1. Modelo de Dados: AccountHolder
package org.simpledi.example.model;
/**
* Representa um titular de conta simples.
*/
public class AccountHolder {
private Long accountId;
private String fullName;
public Long getAccountId() {
return accountId;
}
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
@Override
public String toString() {
return "AccountHolder{" +
"accountId=" + accountId +
", fullName='" + fullName + '\'' +
'}';
}
}
3.2. Interface de Serviço: AccountService
package org.simpledi.example.service;
import org.simpledi.example.model.AccountHolder;
/**
* Interface para serviço de gerenciamento de contas.
*/
public interface AccountService {
AccountHolder findAccountById(Long id);
}
3.3. Implementação do Serviço: AccountServiceImpl
package org.simpledi.example.service;
import org.simpledi.core.annotation.Component;
import org.simpledi.example.model.AccountHolder;
/**
* Implementação do serviço de contas, marcada como um componente.
*/
@Component(name = "accountService")
public class AccountServiceImpl implements AccountService {
@Override
public AccountHolder findAccountById(Long id) {
AccountHolder holder = new AccountHolder();
if (id == 101) {
holder.setAccountId(id);
holder.setFullName("Alice Smith");
} else if (id == 102) {
holder.setAccountId(id);
holder.setFullName("Bob Johnson");
} else {
holder.setAccountId(id);
holder.setFullName("Unknown Holder");
}
return holder;
}
}
3.4. Componente de Controle: AccountController
package org.simpledi.example.controller;
import org.simpledi.core.annotation.Component;
import org.simpledi.core.annotation.Inject;
import org.simpledi.example.model.AccountHolder;
import org.simpledi.example.service.AccountService;
/**
* Componente de controle para gerenciar operações de conta, com dependência injetada.
*/
@Component(name = "accountController")
public class AccountController {
@Inject
private AccountService accountService;
public AccountHolder retrieveAccountDetails(Long id) {
System.out.println("Controller: Solicitando detalhes da conta para ID: " + id);
return accountService.findAccountById(id);
}
}
3.5. Inicializador da Aplicação
package org.simpledi.example;
import org.simpledi.core.ContainerBuilder;
import org.simpledi.example.controller.AccountController;
import org.simpledi.example.model.AccountHolder;
import java.util.Optional;
/**
* Classe principal para inicializar o contêiner DI e executar a aplicação de exemplo.
*/
public class AppInitializer {
public static void main(String[] args) {
// Define o pacote base onde o contêiner deve procurar por componentes
String basePackage = "org.simpledi.example";
System.out.println("Iniciando o construtor do contêiner DI...");
// Instancia o ContainerBuilder, que automaticamente escaneia, instancia e injeta
ContainerBuilder container = new ContainerBuilder(basePackage);
System.out.println("Contêiner DI inicializado com sucesso.");
// Obtém uma instância do AccountController do contêiner
Optional<AccountController> controllerOptional = container.getComponent(AccountController.class);
if (controllerOptional.isPresent()) {
AccountController accountController = controllerOptional.get();
System.out.println("\nBuscando detalhes da conta para ID 101:");
// Chama um método no controller, que por sua vez usará o serviço injetado
AccountHolder account = accountController.retrieveAccountDetails(101L);
System.out.println("Detalhes da conta recuperados: " + account);
System.out.println("\nBuscando detalhes da conta para ID 102:");
AccountHolder anotherAccount = accountController.retrieveAccountDetails(102L);
System.out.println("Detalhes da outra conta: " + anotherAccount);
} else {
System.err.println("ERRO: AccountController não pôde ser encontrado no contêiner.");
}
}
}
- Resultado da Execução Ao executar a classe
AppInitializer, você verá uma saída semelhante a esta, demonstrando a injeção de dependência e a interceptação do proxy:
Iniciando o construtor do contêiner DI...
DEBUG: Iniciando descoberta e instanciação de componentes no pacote: org.simpledi.example
DEBUG: Componente registrado: accountservice de tipo org.simpledi.example.service.AccountService
DEBUG: Componente registrado: accountcontroller de tipo org.simpledi.example.controller.AccountController
DEBUG: Iniciando resolução de dependências para componentes.
DEBUG: Injetada dependência 'accountService' (AccountServiceImpl) em AccountController
DEBUG: Contêiner totalmente inicializado.
Buscando detalhes da conta para ID 101:
Controller: Solicitando detalhes da conta para ID: 101
LOG >> Antes da execução do método: findAccountById
LOG << Depois da execução do método: findAccountById (0.123 ms)
Detalhes da conta recuperados: AccountHolder{accountId=101, fullName='Alice Smith'}
Buscando detalhes da conta para ID 102:
Controller: Solicitando detalhes da conta para ID: 102
LOG >> Antes da execução do método: findAccountById
LOG << Depois da execução do método: findAccountById (0.057 ms)
Detalhes da outra conta: AccountHolder{accountId=102, fullName='Bob Johnson'}