CodeGym /Cursos /JAVA 25 SELF /Reflexão e Injeção de Dependências

Reflexão e Injeção de Dependências

JAVA 25 SELF
Nível 62 , Lição 4
Disponível

1. Serialização via reflexão

Você já sabe que serialização é o processo de converter um objeto em um fluxo de bytes ou em uma representação textual. Em Java há a serialização padrão (Serializable), mas com mais frequência usam-se formatos JSON/XML por meio de bibliotecas como Jackson e Gson.

Por que a reflexão é necessária aqui?
Para serializar um objeto, é preciso descobrir seus campos e seus valores. Os campos podem ser private, e o código comum não consegue alcançá-los — mas a reflexão consegue. Por isso, bibliotecas JSON percorrem dinamicamente a estrutura do objeto, lendo/gravando campos via Field e Method.

Exemplo: serialização simples de um objeto em uma string

Classe de dados:

public class Person {
    private String name;
    private int age;
    private boolean active;

    public Person(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }
}

Um serializador básico usando reflexão:

import java.lang.reflect.Field;

public class SimpleSerializer {
    public static String serialize(Object obj) {
        StringBuilder sb = new StringBuilder();
        Class<?> clazz = obj.getClass();
        sb.append(clazz.getSimpleName()).append("{");
        Field[] fields = clazz.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            field.setAccessible(true); // Tornamos os campos privados acessíveis
            try {
                sb.append(field.getName()).append("=")
                  .append(field.get(obj));
            } catch (IllegalAccessException e) {
                sb.append(field.getName()).append("=<?>");
            }
            if (i < fields.length - 1) sb.append(", ");
        }
        sb.append("}");
        return sb.toString();
    }
}

Uso:

Person p = new Person("Alice", 30, true);
System.out.println(SimpleSerializer.serialize(p));
Person{name=Alice, age=30, active=true}

Como isso funciona?

  • Obtemos os campos declarados via getDeclaredFields().
  • Tornamos esses campos acessíveis por meio de setAccessible(true).
  • Lemos os nomes e valores dos campos e montamos a string.

Limitações do exemplo: não há tratamento de objetos aninhados, coleções, arrays e referências cíclicas — é apenas uma demonstração do princípio.

Como as bibliotecas completas fazem?

Jackson/Gson conseguem trabalhar com objetos aninhados e coleções, levam em conta anotações (@JsonIgnore, @SerializedName), formatos de data e muito mais — tudo isso com base em reflexão.

Exemplo com Jackson:

import com.fasterxml.jackson.databind.ObjectMapper;

Person p = new Person("Bob", 25, false);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(p);
// {"name":"Bob","age":25,"active":false}

2. Dependency Injection (DI) e reflexão

Dependency Injection — padrão no qual as dependências são “injetadas” de fora, e não criadas dentro da classe. Isso torna o código flexível, testável e extensível. Em Java, isso é feito por frameworks Spring, Guice, Dagger, marcando pontos de injeção com anotações como @Autowired, @Inject.

Por que a reflexão é necessária aqui?
O contêiner de DI precisa localizar campos/construtores, ler anotações e criar instâncias em tempo de execução — isso é feito por meio das APIs Class/Constructor/Field.

Exemplo: mini-DI com reflexão

Anotação de injeção:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {
}

Classes com dependência:

public class Service {
    public void doWork() {
        System.out.println("Service is working!");
    }
}

public class Client {
    @Inject
    private Service service;

    public void useService() {
        service.doWork();
    }
}

Mini contêiner:

import java.lang.reflect.*;

public class MiniDIContainer {
    // Cria um objeto pelo tipo e injeta dependências nos campos com @Inject
    public static Object createObject(Class<?> clazz) throws Exception {
        Object obj = clazz.getDeclaredConstructor().newInstance();

        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(Inject.class)) {
                Object dependency = createObject(field.getType()); // recursivamente
                field.setAccessible(true);
                field.set(obj, dependency);
            }
        }
        return obj;
    }
}

Uso:

public class Main {
    public static void main(String[] args) throws Exception {
        Client client = (Client) MiniDIContainer.createObject(Client.class);
        client.useService(); // Service is working!
    }
}

Como isso funciona? O contêiner procura campos com @Inject, cria dependências pelo seu tipo e, via reflexão, define-os em campos privados.

Importante: isto é um esquema simplificado. Contêineres de DI reais oferecem escopos, singletons, configuração, proxies, tratamento de dependências cíclicas e mais.

3. Proxies dinâmicos

Um proxy é um objeto “intermediário” que intercepta chamadas e adiciona comportamento: logging, segurança, transações etc. Em Java isso é feito por java.lang.reflect.Proxy junto com InvocationHandler. Base de muitos recursos do Spring AOP, mocks no Mockito etc.

Exemplo: proxy de logging

public interface HelloService {
    void sayHello(String name);
}
public class HelloServiceImpl implements HelloService {
    public void sayHello(String name) {
        System.out.println("Hello, " + name + "!");
    }
}
import java.lang.reflect.*;

public class LoggingProxy {
    @SuppressWarnings("unchecked")
    public static <T> T createProxy(T target, Class<T> iface) {
        return (T) Proxy.newProxyInstance(
            iface.getClassLoader(),
            new Class<?>[]{iface},
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("Chamada de método: " + method.getName());
                    return method.invoke(target, args);
                }
            }
        );
    }
}

Uso:

HelloService original = new HelloServiceImpl();
HelloService proxy = LoggingProxy.createProxy(original, HelloService.class);

proxy.sayHello("World");
Chamada de método: sayHello
Hello, World!

Como isso funciona? Proxy.newProxyInstance cria um objeto que implementa a interface, e todas as chamadas de métodos vão para InvocationHandler.invoke, onde você pode executar qualquer código “transversal” antes/depois da delegação.

4. Cenários reais e limitações

Onde a reflexão é usada “em produção”?

  • JUnit — encontra métodos com @Test e os invoca via reflexão.
  • Spring — cria beans, injeta dependências, faz varredura de anotações, gera proxies.
  • Jackson/Gson — serializam/desserializam, lendo até campos privados.
  • Hibernate — constrói modelos ORM a partir da estrutura das classes, gerencia campos e objetos proxy.
  • Mockito — cria mocks e intercepta chamadas por meio de proxies.

Por que nem sempre vale usar reflexão?

  • Desempenho. Em geral é mais lento que chamadas comuns (mitigado por cache/geração de bytecode).
  • Segurança. Quebra a encapsulação; acesso a dados privados.
  • Modularidade no Java 9+. Pode ocorrer InaccessibleObjectException sem abrir explicitamente pacotes/módulos.

5. Erros típicos

Erro nº 1: Ignorar exceções checked. Métodos de reflexão lançam NoSuchFieldException, IllegalAccessException, InvocationTargetException etc. Trate-as ou encapsule-as em suas próprias exceções.

Erro nº 2: setAccessible(true) nem sempre funciona. No Java 9+ em aplicativos modulares pode ocorrer InaccessibleObjectException. São necessários parâmetros da JVM/módulo (--add-opens) ou APIs públicas.

Erro nº 3: Dependências cíclicas no DI. Com recursão ingênua (A depende de B, e B de A) você obterá StackOverflowError. Contêineres reais rastreiam o grafo de dependências e resolvem ciclos com técnicas especiais.

Erro nº 4: Serialização incompleta de objetos. Campos de referência são serializados como ClassName@hash se não houver percurso recursivo. Para serializar corretamente, é preciso tratar objetos/coleções aninhados e proteger contra ciclos.

Erro nº 5: Perda de desempenho. Operações reflexivas frequentes (em loops, em caminhos críticos) tornam-se gargalos. Use cache de Field/Method, geração de bytecode, MethodHandle ou processamento de anotações.

Erro nº 6: Violação de encapsulamento. Alterar campos privados via reflexão leva à fragilidade e a bugs difíceis de rastrear. Prefira contratos públicos sempre que possível.

1
Pesquisa/teste
Reflexão, nível 62, lição 4
Indisponível
Reflexão
Reflexão e recursos dinâmicos
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION