Construindo um Contêiner de Inversão de Controle (IoC) Básico em Java para Entender os Princípios do Spring

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.

  1. 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 "";
}

  1. 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);
    }
}

  1. 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.");
        }
    }
}

  1. 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'}

Tags: Inversão de Controle Injeção de Dependência Reflexão Java Proxy Dinâmico Contêiner IoC

Publicado em 7-27 00:44