CodeGym /Cours /Spring Core /JDK et proxy basé sur les classes

JDK et proxy basé sur les classes

Spring Core
Niveau 21, Leçon 1
Disponible

1. Deux types de proxy dans Spring

Quand un débutant entend « proxy », il imagine souvent une sorte d’« objet magique que Spring a mis en place ». Mais en pratique, un proxy est simplement un objet Java ordinaire, et il existe deux façons de base en Java de créer un « objet enveloppe ». Une méthode repose sur les interfaces (intégrée dans le JDK), l’autre repose sur les classes (et conceptuellement similaire à l’héritage avec redéfinition de méthodes). Spring sait utiliser les deux, et selon le type de proxy apparu, vos attentes changent : quel contrat vous pouvez utiliser, et où vous attend une « surprise » sous la forme d’un type d’objet inattendu.

Pour garder une image en tête, il est utile de dessiner à nouveau le flux d’appel minimal :

%% Schéma minimal : l’appel passe par le proxy vers la cible (c’est toute l’idée)
flowchart LR
    Caller["Code appelant"] --> P["Proxy (enveloppe)"]
    P --> T["Objet cible (objet d’origine)"]

Le reste de la leçon porte sur comment exactement se présente le bloc Proxy en Java dans deux variantes différentes.

2. JDK dynamic proxy : proxy sur interfaces

Le JDK dynamic proxy est, en gros, la technologie « officielle » intégrée à Java qui permet de créer à l’exécution un objet implémentant une ou plusieurs interfaces et d’intercepter les appels aux méthodes de ces interfaces via un gestionnaire spécial. Ce n’est pas spécifique à Spring, cela fait partie de la bibliothèque standard (java.lang.reflect.Proxy). Il est important de retenir immédiatement une condition simple : le JDK proxy ne fonctionne qu’avec des interfaces. Si vous avez un bon contrat d’interface, c’est un moyen très pratique et transparent qui s’intègre bien au style DI « dépendre des abstractions ».

Fonctionnement du JDK proxy « à la main »

Un JDK proxy est créé via Proxy.newProxyInstance(...). Vous lui fournissez :

1) un ClassLoader (généralement celui de l’interface),

2) la liste des interfaces que le proxy doit implémenter,

3) un gestionnaire d’appels (InvocationHandler) — une fonction qui recevra la Method et les arguments et décidera quoi faire.

Dans notre ContextFlow, nous avons un port d’interface classique — NotificationSender. Prenons l’objet cible (target) et enveloppons-le avec un objet proxy qui affichera « before/after » autour de l’envoi.

import java.lang.reflect.Proxy;
import com.example.contextflow.domain.ports.NotificationSender;
import com.example.contextflow.infrastructure.notification.ConsoleNotificationSender;

// Cible : l’implémentation réelle qui fait le travail
NotificationSender target = new ConsoleNotificationSender();

// Proxy : objet enveloppe qui ressemble à NotificationSender,
// mais intercepte en réalité les appels via InvocationHandler
NotificationSender proxy = (NotificationSender) Proxy.newProxyInstance(
        NotificationSender.class.getClassLoader(),              // Prend le ClassLoader de l’interface
        new Class
  []{NotificationSender.class},               // Dit : le proxy implémente cette interface
        (p, method, args) -> {
            // Nous pouvons ajouter une enveloppe technique AVANT l’appel
            System.out.println("[before] " + method.getName()); // [before] send

            // Déléguons l’appel à la cible réelle
            Object result = method.invoke(target, args);

            // Et ajoutons une enveloppe APRÈS l’appel
            System.out.println("[after] " + method.getName());  // [after] send
            return result;                                      // Retournons le résultat au code appelant
        }
);

// Important : le code appelant utilise le proxy comme un NotificationSender normal
proxy.send("Order created"); // SEND: Order created

Ce qu’il faut comprendre ici, ce n’est pas la syntaxe (on peut la « regarder »), mais le modèle. L’appel proxy.send("Order created") ne va pas directement dans ConsoleNotificationSender. Il passe d’abord par le gestionnaire, et c’est le gestionnaire qui décide « de déléguer ou non ». S’il délègue, il appelle method.invoke(target, args).

Si vous avez l’impression que « c’est presque un décorateur ! », vous êtes sur la bonne voie. Oui, du point de vue du sens, c’est très proche du pattern Decorator, sauf qu’au lieu d’une classe d’enveloppe manuelle, vous utilisez le mécanisme JDK qui crée une classe proxy pour vous.

Les interfaces dans Spring : quand sont-elles utiles

Dans ContextFlow, de nombreux points clés de variabilité sont justement exprimés sous forme d’interfaces : NotificationSender, AuditWriter, DiscountPolicy, OrderStore, OrderIdGenerator. Ce n’est pas parce que « c’est comme ça dans Spring », mais parce que ce sont de véritables frontières architecturales entre l’application et l’infrastructure/stratégies. Et cela fait automatiquement du JDK proxy un candidat naturel : il fonctionne parfaitement lorsque le code appelant communique avec la dépendance via une interface.

Mais il est important de ne pas tomber dans « une interface pour chaque pet ». Si vous créez une interface pour chaque classe juste au cas où « un proxy serait nécessaire », vous obtiendrez un projet où la navigation dans le code ressemblera à une excursion dans un labyrinthe : « l’implémentation » à un endroit, « l’interface » à un autre, et peu de sens. Dans notre projet, les interfaces sont des ports et des stratégies, pas une « décoration ».

Un détail à noter

Le JDK proxy implémente une ou plusieurs interfaces, mais n’est pas obligé d’être une instance de la classe d’implémentation concrète. Vous conservez le contrat de l’interface, mais la « classe exacte » peut être surprenante. De là découle presque immédiatement une question pratique : que donneront exactement getClass() et instanceof lorsque la variable contient un proxy et non une implémentation familière.

3. Class-based proxy : proxy sur classe

Parfois, on veut envelopper un objet, mais il n’a pas d’interface. Ou bien une interface existe, mais historiquement on dépend de la classe (cela arrive dans du code legacy, dans des exemples pédagogiques, ou « parce que c’est comme ça »). Dans ce cas, une deuxième approche apparaît : le proxy « sur classe ». Conceptuellement, il repose sur l’héritage : le proxy est une sous-classe qui redéfinit les méthodes et ajoute un comportement avant/après l’appel de la logique originale. Spring génère ces proxys dynamiquement (via des outils de bytecode), mais ce qui nous importe maintenant, c’est le principe, non la cuisine de la génération de classes.

Pour le ressentir concrètement, faisons une illustration « manuelle » la plus honnête possible sur une classe sans interface. Supposons que nous ayons un service qui génère une chaîne de rapport (simplifié — « report-as-string », comme dans les premières étapes de notre projet).

public class ReportingService {

    public String generateDailyReport() {
        // Ici se trouve la logique « réelle » du service (simplifié)
        return "daily-report";
    }
}

Maintenant, « l’enveloppe sur classe » ressemblera à une sous-classe avec redéfinition :

public class TimedReportingService extends ReportingService {

    @Override
    public String generateDailyReport() {
        // Enveloppe technique : mesure du temps AVANT l’appel
        long started = System.nanoTime();

        // Important : appel de l’implémentation originale via super
        String result = super.generateDailyReport();

        // Enveloppe technique : journalisation APRÈS l’appel
        System.out.println("Took: " + (System.nanoTime() - started)); // Took: 12345
        return result;
    }
}

Et l’utilisation reste « sur le type de base », comme le veut le polymorphisme :

// Référence du type de base, mais l’objet réel est une sous-classe (proxy par classe)
ReportingService service = new TimedReportingService();

System.out.println(service.generateDailyReport()); // daily-report

Le sens du class-based proxy : vous conservez une référence de type ReportingService, mais l’objet réel est un descendant qui ajoute un comportement.

Quand utiliser le class-based proxy

Il serait très pratique de dire : « faites toujours des interfaces, et vous serez heureux ». Mais Spring vit dans un monde où :

- il existe des classes legacy sans interfaces (qu’on ne peut pas réécrire rapidement) ;

- il existe des classes tierces que vous ne contrôlez pas ;

- il existe des classes où un contrat d’interface serait artificiel et ne ferait que gêner la lecture.

C’est pourquoi Spring sait créer des class-based proxy. Mais ce confort a un coût : l’héritage a des règles et des limitations en Java. Ce sont elles qui déterminent ensuite quelles méthodes peuvent réellement être enveloppées et lesquelles ne le peuvent pas. Pour l’instant, il est seulement important de retenir : si un proxy est construit sur une classe, il est conceptuellement « similaire à une sous-classe ».

4. Choix du proxy dans ContextFlow

Dans les explications pédagogiques, on a souvent envie de choisir « la bonne réponse pour tous les cas ». Mais en ingénierie, la vraie « bonne réponse » est presque toujours : « ça dépend ». Dans ContextFlow, nous avons spécialement construit l’architecture de manière à ce que les points de dépendance clés soient exprimés sous forme d’interfaces. Cela nous donne de la flexibilité pour l’injection de dépendances, la testabilité et (en bonus) pour les proxies sur interfaces. Donc la recommandation pratique ici est simple : là où vous avez un port/stratégie, tenez-vous-en à l’interface ; là où vous avez un simple service interne sans variabilité, vous n’êtes pas obligé de créer une interface « au cas où ».

Regardez un exemple tiré du projet (très typique pour Spring et très utile pour la réflexion sur les proxys). Le service ne dépend pas de ConsoleNotificationSender, mais du contrat NotificationSender :

import org.springframework.stereotype.Service;
import com.example.contextflow.domain.ports.NotificationSender;

@Service
public class NotificationDispatchService {

    // Dépendance par interface : c’est un point clé pour le proxy JDK et l’injection de dépendances en général
    private final NotificationSender sender;

    public NotificationDispatchService(NotificationSender sender) {
        // Injection de dépendance via le constructeur :
        // le conteneur peut fournir soit l’implémentation réelle, soit une enveloppe proxy
        this.sender = sender;
    }
}

Pourquoi est-ce important dans le contexte des proxys ? Parce que si le conteneur (ou la couche d’infrastructure) vous retourne un JDK proxy, il sera « vu » comme NotificationSender. Et tout va bien. En revanche, si vous aviez voulu dépendre du ConsoleNotificationSender concret, vous vous seriez fortement lié à la classe — et toute enveloppe d’interface pourrait devenir un problème.

Je ne prône pas de créer « une interface pour OrderPricingService uniquement parce que c’est un service ». Les services applicatifs internes peuvent très bien rester des classes, et si un jour un class-based proxy apparaît autour d’eux, c’est normal. Il est simplement important de comprendre ce que vous obtenez en échange : le JDK proxy s’accorde mieux avec les dépendances d’interface, le class-based proxy avec les dépendances de classe.

Comparaison JDK proxy et class-based proxy

Quand les termes « proxy », « target », « interfaces », « héritage », « conteneur » cohabitent dans votre tête, le cerveau essaie parfois de partir en vacances sans prévenir. Le tableau ci-dessous n’est pas une « boîte à mémoriser », mais une antisèche utile à consulter lorsque vous lisez du code étranger ou une stack trace étrange. Pour l’instant, il est important de fixer les différences clés au niveau du comportement et des exigences, sans bytecode ni détails internes de Spring.

Critère JDK dynamic proxy Class-based proxy
Sur quoi repose le contrat Sur les interfaces Sur la classe (et ses méthodes)
Ce qu’il faut pour créer un proxy Au moins une interface est nécessaire L’interface n’est pas obligatoire
Comment les appels sont interceptés Via InvocationHandler (réflexion Method) Conceptuellement via redéfinition (dans Spring, via une sous-classe générée)
Quel type est « plus pratique » en DI NotificationSender ReportingService
Ce qu’il est bon de concevoir ainsi dans un projet Ports et stratégies (*Sender, *Policy, *Store) Services internes sans interfaces, classes legacy
L’erreur de pensée la plus fréquente « On peut créer un JDK proxy pour une classe sans interface » « C’est juste un objet de la même classe, rien n’a changé »

Ce tableau suffit à ne plus confondre les deux familles de proxys et à comprendre pourquoi Spring « n’a pas pu choisir une seule option et vivre tranquille ». Il l’aurait bien choisi, mais le monde réel ne le permet pas.

5. Mini-pratique sans Spring

Il est très facile de lire sur les proxys et de faire semblant de « oui, j’ai compris ». Puis de voir pour la première fois Proxy.newProxyInstance — et de sentir que Java s’est soudainement transformé en film d’espionnage. C’est pourquoi il est utile de faire une petite pratique « sur le coin de la table, mais honnête » : réaliser deux enveloppes, l’une avec un JDK dynamic proxy pour une interface, l’autre avec un class-based proxy via héritage. Pour l’instant, les formes elles-mêmes des proxys nous importent, sans l’intervention du conteneur.

Imaginons que nous voulions obtenir facilement un « notifieur enveloppé ». On peut créer une petite fabrique (dans notre projet, cela pourrait vivre dans support.*, mais pour l’instant la forme importe plus que l’emplacement exact).

import java.lang.reflect.Proxy;
import com.example.contextflow.domain.ports.NotificationSender;

public class NotificationSenderProxies {

    public static NotificationSender withTiming(NotificationSender target) {
        return (NotificationSender) Proxy.newProxyInstance(
                NotificationSender.class.getClassLoader(),
                new Class
  []{NotificationSender.class},
                (p, method, args) -> {
                    // Nous pouvons ajouter une enveloppe technique AVANT l’appel
                    long started = System.nanoTime();
                    Object result = method.invoke(target, args);
                    // Et ajoutons une enveloppe APRÈS l’appel
                    System.out.println("Took: " + (System.nanoTime() - started));
                    return result;
                }
        );
    }
}

Et l’utilisation est simple et « par interface » :

import com.example.contextflow.domain.ports.NotificationSender;
import com.example.contextflow.infrastructure.notification.ConsoleNotificationSender;

NotificationSender sender =
        NotificationSenderProxies.withTiming(new ConsoleNotificationSender());

sender.send("Order cancelled"); // SEND: Order cancelled

Maintenant, la pratique « similaire » pour un class-based proxy consiste simplement à créer une sous-classe (comme nous l’avons déjà montré) :

ReportingService service = new TimedReportingService();

System.out.println(service.generateDailyReport()); // daily-report

Dans les deux cas, vous obtenez une enveloppe autour de l’appel de méthode. Mais le « point d’appui » est différent : d’un côté vous vous tenez sur l’interface, de l’autre sur la classe. Et cette différence, nous la rencontrerons constamment dans le code Spring par la suite.

6. Erreurs typiques lors du choix du modèle de proxy

Erreur n°1 : attendre un JDK proxy là où il n’y a pas d’interface.
C’est un piège classique : « je peux bien envelopper un objet… pourquoi ne pas simplement créer un proxy ? ». On peut, mais un JDK dynamic proxy ne sait être un proxy que pour des interfaces. S’il n’y a pas d’interface, Java ne peut pas créer un objet « du même type » sans recourir à l’héritage/génération de sous-classe. La conclusion pratique est simple : si vous voulez un modèle basé sur les interfaces, donnez-lui un contrat d’interface, mais faites-le là où le contrat est réellement nécessaire.

Erreur n°2 : dépendre d’une classe concrète là où une interface suffit.
Dans le code DI, cela semble anodin : « j’ai bien ConsoleNotificationSender, pourquoi aurais-je besoin de votre NotificationSender ? ». Et puis soudainement une enveloppe apparaît, et la dépendance à une classe concrète devient un problème. Dans ContextFlow, nous avons spécialement fait dépendre des ports–interfaces pour ne pas lier la couche application aux détails de l’infrastructure et pour que de telles enveloppes ne cassent pas le code.

Erreur n°3 : essayer « d’ajouter du comportement » et accidentellement déplacer la logique métier dans le proxy.
Un proxy est un mauvais endroit pour les règles métier. Aujourd’hui vous ajoutez du logging, demain « vérifions un peu le statut de la commande », après-demain « et si on recalculait la remise ici… » et vous vous retrouvez avec deux sources de logique métier : dans le target et dans le proxy. Résultat, le débogage devient un jeu « devinez où l’action s’est réellement produite ». Un proxy doit faire une enveloppe technique et laisser le sens au target.

Erreur n°4 : écrire du code proxy qui devient plus complexe que le target.
C’est particulièrement visible dans InvocationHandler : on peut très vite le transformer en monstre avec une multitude de if, la gestion d’une dizaine de méthodes et un pilotage manuel de tout. Si l’enveloppe devient complexe, c’est un signal que vous avez soit choisi le mauvais niveau d’abstraction, soit que vous avez besoin d’un mécanisme plus structuré qu’« une lambda pour tous les cas ». Dans les exemples pédagogiques, nous gardons le code proxy court et prévisible — et c’est une bonne habitude.

Erreur n°5 : oublier que le class-based proxy repose sur la redéfinition de méthodes.
Au niveau de l’idée, tout semble simple : « on crée un héritier — on redéfinit la méthode — on enveloppe ». Mais Java a des limitations : tout ne peut pas être redéfini, tout n’est pas « visible » via l’héritage, et il existe des appels qui contournent complètement l’enveloppe externe. Ce ne sont pas des « caprices de Spring », mais des règles du langage. Par conséquent, le modèle de proxy a de vraies limites, et il est important de les garder en tête à l’avance, plutôt que de les découvrir seulement dans le débogueur.

1
Mission
Spring Core, niveau 21, leçon 1
Bloqué
JDK dynamic proxy pour le service de salutations
JDK dynamic proxy pour le service de salutations
1
Mission
Spring Core, niveau 21, leçon 1
Bloqué
Proxy par classe via héritage
Proxy par classe via héritage
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION