CodeGym /Corsi /JAVA 25 SELF /Riflessione e Dependency Injection

Riflessione e Dependency Injection

JAVA 25 SELF
Livello 62 , Lezione 4
Disponibile

1. Serializzazione tramite riflessione

Sapete già che la serializzazione è il processo di trasformazione di un oggetto in un flusso di byte o in una rappresentazione testuale. In Java esiste la serializzazione standard (Serializable), ma più spesso si usano formati JSON/XML tramite librerie come Jackson e Gson.

Perché qui serve la riflessione?
Per serializzare un oggetto bisogna conoscere i suoi campi e i relativi valori. I campi possono essere private e il codice normale non vi può accedere — mentre la riflessione sì. Perciò le librerie JSON attraversano dinamicamente la struttura dell’oggetto e leggono/scrivono i campi tramite Field e Method.

Esempio: semplice serializzazione di un oggetto in una stringa

Classe di dati:

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;
    }
}

Un serializzatore elementare con la riflessione:

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); // Apriamo i campi privati
            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();
    }
}

Utilizzo:

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

Come funziona?

  • Otteniamo i campi dichiarati tramite getDeclaredFields().
  • Li rendiamo accessibili con setAccessible(true).
  • Leggiamo i nomi e i valori dei campi e costruiamo la stringa.

Limitazioni dell’esempio: nessuna gestione di oggetti annidati, collezioni, array e riferimenti ciclici — è solo una dimostrazione del principio.

Come fanno le librerie complete?

Jackson/Gson sanno lavorare con oggetti annidati e collezioni, tengono conto delle annotazioni (@JsonIgnore, @SerializedName), dei formati delle date e molto altro — tutto questo basato sulla riflessione.

Esempio con 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 riflessione

Dependency Injection — un pattern in cui le dipendenze vengono «iniettate» dall’esterno invece di essere create all’interno della classe. Questo rende il codice flessibile, testabile ed estendibile. In Java lo fanno framework come Spring, Guice, Dagger, marcando i punti di iniezione con annotazioni come @Autowired, @Inject.

Perché qui serve la riflessione?
Un container DI deve trovare campi/costruttori, leggere le annotazioni e creare istanze a runtime — questo si fa tramite le API Class/Constructor/Field.

Esempio: mini-DI con la riflessione

Annotazione di iniezione:

import java.lang.annotation.*;

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

Classi con dipendenza:

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-container:

import java.lang.reflect.*;

public class MiniDIContainer {
    // Crea un oggetto dal tipo e inietta le dipendenze nei campi annotati con @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()); // ricorsivamente
                field.setAccessible(true);
                field.set(obj, dependency);
            }
        }
        return obj;
    }
}

Utilizzo:

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

Come funziona? Il container cerca i campi con @Inject, crea le dipendenze in base al loro tipo e, tramite riflessione, le imposta nei campi privati.

Importante: questa è una versione semplificata. I container DI reali supportano scope, singleton, configurazione, proxy, gestione delle dipendenze cicliche, ecc.

3. Proxy dinamici

Un proxy è un oggetto «sostituto» che intercetta le chiamate e aggiunge comportamento: logging, sicurezza, transazioni, ecc. In Java questo è realizzato da java.lang.reflect.Proxy insieme a InvocationHandler. È alla base di molte funzionalità di Spring AOP, dei mock in Mockito, ecc.

Esempio: proxy con 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("Chiamata del metodo: " + method.getName());
                    return method.invoke(target, args);
                }
            }
        );
    }
}

Utilizzo:

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

proxy.sayHello("World");
Chiamata del metodo: sayHello
Hello, World!

Come funziona? Proxy.newProxyInstance crea un oggetto che implementa l’interfaccia, e tutte le chiamate ai metodi passano a InvocationHandler.invoke, dove potete eseguire qualsiasi codice «trasversale» prima/dopo il delegare.

4. Scenari reali e limitazioni

Dove la riflessione viene usata «sul campo»?

  • JUnit — trova i metodi annotati con @Test e li invoca tramite riflessione.
  • Spring — crea bean, inietta dipendenze, esegue lo scanning delle annotazioni, genera proxy.
  • Jackson/Gson — serializzano/deserializzano leggendo anche i campi privati.
  • Hibernate — costruisce i modelli ORM a partire dalla struttura delle classi, gestisce campi e oggetti proxy.
  • Mockito — crea mock e intercetta le chiamate tramite proxy.

Perché non conviene sempre usare la riflessione?

  • Prestazioni. In generale è più lenta delle chiamate normali (mitigabile con caching/generazione di bytecode).
  • Sicurezza. Rompe l’incapsulamento; accesso a dati privati.
  • Modularità Java 9+. Può comparire InaccessibleObjectException senza un’apertura esplicita di pacchetti/moduli.

5. Errori tipici

Errore n°1: ignorare le eccezioni checked. I metodi di riflessione lanciano NoSuchFieldException, IllegalAccessException, InvocationTargetException, ecc. Gestitele o incapsulatele in vostre eccezioni.

Errore n°2: setAccessible(true) non funziona sempre. Su Java 9+ nelle applicazioni modulari è possibile ottenere InaccessibleObjectException. Servono parametri della JVM/del modulo (--add-opens) o API pubbliche.

Errore n°3: dipendenze cicliche nel DI. Con ricorsione ingenua (A dipende da B e B da A) si ottiene StackOverflowError. I container reali tracciano il grafo delle dipendenze e risolvono i cicli con tecniche specifiche.

Errore n°4: serializzazione incompleta degli oggetti. I campi-riferimento vengono serializzati come ClassName@hash se non c’è attraversamento ricorsivo. Per una serializzazione corretta servono la gestione di oggetti/collezioni annidati e la protezione dai cicli.

Errore n°5: perdita di prestazioni. Operazioni riflessive frequenti (in cicli, su hot path) diventano un collo di bottiglia. Usate il caching di Field/Method, la generazione di bytecode, MethodHandle o l’annotation processing.

Errore n°6: violazione dell’incapsulamento. Modificare i campi privati tramite riflessione porta a fragilità e bug difficili da rilevare. Preferite i contratti pubblici.

1
Compito
JAVA 25 SELF, livello 62, lezione 4
Bloccato
Assemblaggio automatico di robot con componenti intelligenti 🤖
Assemblaggio automatico di robot con componenti intelligenti 🤖
1
Compito
JAVA 25 SELF, livello 62, lezione 4
Bloccato
Intercettazione spionistica dei messaggi nel decodificatore 🕵️
Intercettazione spionistica dei messaggi nel decodificatore 🕵️
1
Sondaggio/quiz
Riflessione, livello 62, lezione 4
Non disponibile
Riflessione
Riflessione e funzionalità dinamiche
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION