CodeGym /Cours /JAVA 25 SELF /Réflexion et Dependency Injection

Réflexion et Dependency Injection

JAVA 25 SELF
Niveau 62, Leçon 4
Disponible

1. Sérialisation via la réflexion

Vous savez déjà que la sérialisation est le processus de transformation d’un objet en un flux d’octets ou en une représentation textuelle. En Java, il existe une sérialisation standard (Serializable), mais on utilise plus souvent des formats JSON/XML via des bibliothèques comme Jackson et Gson.

Pourquoi la réflexion est-elle nécessaire ici ?
Pour sérialiser un objet, il faut connaître ses champs et leurs valeurs. Les champs peuvent être private, et le code classique ne peut pas y accéder — la réflexion, si. C’est pourquoi les bibliothèques JSON parcourent dynamiquement la structure de l’objet, lisent/écrivent les champs via Field et Method.

Exemple : sérialiser un objet en chaîne

Classe de données :

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 sérialiseur minimal basé sur la réflexion :

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); // On rend les champs privés accessibles
            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();
    }
}

Utilisation :

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

Comment cela fonctionne-t-il ?

  • On récupère les champs déclarés via getDeclaredFields().
  • On les rend accessibles via setAccessible(true).
  • On lit les noms et les valeurs des champs et on forme la chaîne.

Limites de l’exemple : pas de prise en charge des objets imbriqués, des collections, des tableaux ni des références cycliques — il ne s’agit que d’une démonstration du principe.

Comment font les bibliothèques complètes ?

Jackson/Gson savent travailler avec des objets imbriqués et des collections, prennent en compte des annotations (@JsonIgnore, @SerializedName), des formats de date et bien plus — le tout sur la base de la réflexion.

Exemple avec 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) et réflexion

Dependency Injection est un patron où les dépendances sont « injectées » de l’extérieur, et non créées à l’intérieur de la classe. Cela rend le code flexible, testable et extensible. En Java, cela est géré par des frameworks comme Spring, Guice, Dagger, en marquant les points d’injection avec des annotations comme @Autowired, @Inject.

Pourquoi la réflexion est-elle nécessaire ici ?
Le conteneur DI doit trouver les champs/constructeurs, lire les annotations et créer des instances au runtime — cela se fait via les API Class/Constructor/Field.

Exemple : mini-DI basé sur la réflexion

Annotation d’injection :

import java.lang.annotation.*;

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

Classes avec dépendance :

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

import java.lang.reflect.*;

public class MiniDIContainer {
    // Crée un objet à partir de la classe et injecte les dépendances dans les champs annotés @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()); // récursivement
                field.setAccessible(true);
                field.set(obj, dependency);
            }
        }
        return obj;
    }
}

Utilisation :

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

Comment cela fonctionne-t-il ? Le conteneur cherche les champs avec @Inject, crée les dépendances selon leur type et, via la réflexion, les affecte aux champs privés.

Important : il s’agit d’un schéma simplifié. Les conteneurs DI réels gèrent des scopes, des singletons, de la configuration, des proxies, le traitement des dépendances cycliques, etc.

3. Proxies dynamiques

Un proxy est un « objet de substitution » qui intercepte les appels et ajoute un comportement : journalisation, sécurité, transactions, etc. En Java, cela se fait avec java.lang.reflect.Proxy associé à InvocationHandler. C’est la base de nombreuses capacités de Spring AOP, des mocks dans Mockito, etc.

Exemple : proxy de journalisation

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("Appel de méthode: " + method.getName());
                    return method.invoke(target, args);
                }
            }
        );
    }
}

Utilisation :

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

proxy.sayHello("World");
Appel de méthode: sayHello
Hello, World!

Comment cela fonctionne-t-il ? Proxy.newProxyInstance crée un objet qui implémente l’interface, et tous les appels de méthodes sont redirigés vers InvocationHandler.invoke, où vous pouvez exécuter n’importe quel code « transversal » avant/après la délégation.

4. Cas d’usage réels et limites

Où la réflexion est-elle utilisée « en production » ?

  • JUnit — trouve les méthodes avec @Test et les appelle via la réflexion.
  • Spring — crée des beans, injecte des dépendances, scanne des annotations, génère des proxies.
  • Jackson/Gson — sérialisent/désérialisent, y compris des champs privés.
  • Hibernate — construit des modèles ORM à partir de la structure des classes, gère les champs et les objets proxy.
  • Mockito — crée des mocks et intercepte les appels via des proxies.

Pourquoi ne faut-il pas toujours utiliser la réflexion ?

  • Performances. En règle générale, plus lent que les appels directs (atténué par la mise en cache/la génération de bytecode).
  • Sécurité. Brise l’encapsulation ; accès aux données privées.
  • Modularité Java 9+. Un InaccessibleObjectException peut survenir sans ouverture explicite des paquets/modules.

5. Erreurs typiques

Erreur n°1 : Ignorer les exceptions vérifiées (checked). Les méthodes de réflexion lèvent NoSuchFieldException, IllegalAccessException, InvocationTargetException, etc. Gérez-les ou encapsulez-les dans vos propres exceptions.

Erreur n°2 : setAccessible(true) ne fonctionne pas toujours. Sur Java 9+ dans des applications modulaires, un InaccessibleObjectException est possible. Il faut des paramètres de la JVM/du module (--add-opens) ou utiliser des API publiques.

Erreur n°3 : Dépendances cycliques dans le DI. Avec une récursion naïve (A dépend de B, et B de A), vous obtiendrez un StackOverflowError. Les conteneurs réels suivent le graphe des dépendances et résolvent les cycles par des techniques dédiées.

Erreur n°4 : Sérialisation incomplète des objets. Les champs de type référence sont sérialisés comme ClassName@hash en l’absence de parcours récursif. Pour une sérialisation correcte, il faut gérer les objets/collections imbriqués et se protéger contre les cycles.

Erreur n°5 : Perte de performances. Des opérations réflexives fréquentes (dans des boucles, sur des chemins chauds) deviennent un goulot d’étranglement. Utilisez la mise en cache des Field/Method, la génération de bytecode, MethodHandle ou le traitement d’annotations.

Erreur n°6 : Violation de l’encapsulation. La modification de champs privés via la réflexion conduit à de la fragilité et à des bugs difficiles à traquer. Préférez autant que possible les contrats publics.

1
Mission
JAVA 25 SELF, niveau 62, leçon 4
Bloqué
Assemblage automatique de robots avec composants intelligents 🤖
Assemblage automatique de robots avec composants intelligents 🤖
1
Mission
JAVA 25 SELF, niveau 62, leçon 4
Bloqué
Interception des messages dans le déchiffreur 🕵️
Interception des messages dans le déchiffreur 🕵️
1
Étude/Quiz
Réflexion, niveau 62, leçon 4
Indisponible
Réflexion
Réflexion et fonctionnalités dynamiques
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION