CodeGym /Kurse /Spring Core /Proxy-Bean-Inspektion im Spring-Kontext

Proxy-Bean-Inspektion im Spring-Kontext

Spring Core
Level 21, Lektion 4
Verfügbar

1. Bean-Inspektion im ApplicationContext

Wenn Sie zum ersten Mal damit konfrontiert werden, dass Spring „nicht das richtige Objekt zurückgegeben“ hat, fühlt es sich an wie ein Zirkustrick: Sie haben erwartet, ReportingService zu sehen, und plötzlich steht im Debugger ein jdk.proxy2.$Proxy42. An dieser Stelle ist es wichtig, ruhig zu bleiben: Der Container macht keine Scherze, er tut lediglich das, worum wir ihn (explizit oder implizit) über seine Erweiterungsmechanismen gebeten haben.

Hier ist es nützlich, eine kleine „Röntgenaufnahme“ des Containers zu machen und zu sehen, wie das Proxy innerhalb des ApplicationContext auftaucht.

Die Schlüsselidee hier ist: Spring speichert und gibt nicht das „Objekt, das Sie erstellt haben“ zurück, sondern das „Objekt, das nach der gesamten Containerverarbeitung entstanden ist“. Und genau deshalb ist die Inspektion eine nützliche Fähigkeit: Wenn das Verhalten seltsam ist, möchte man als Erstes verstehen – „Wer steht wirklich zwischen mir und der Logik?“

Zur Veranschaulichung ist es hilfreich, folgende Kette im Kopf zu behalten:

flowchart TD
  A["Bean wird vom Container erstellt"] --> B["BeanPostProcessor(s)"]
  B --> C["Endgültige Bean-Instanz im Kontext (kann Proxy sein)"]
  C --> D["Diese Instanz erhalten andere Beans und Ihr Code"]

BeanPostProcessor ist bereits als Teil der Startup-Pipeline bekannt. Hier wird er als legaler Haken benötigt, an dem der Container das ursprüngliche Objekt durch eine Hülle ersetzen kann.

2. Vertrag für das Proxy: Interface

Damit JDK dynamic proxy überhaupt existieren kann, benötigt er ein Interface, weil er nur vorgeben kann, ein Interface zu sein, aber nicht zu einer konkreten Klasse werden kann. Also beginnen wir mit einem ganz kleinen, vorsichtigen Schritt: Wir heben den Vertrag für den Reporting-Service hervor. Selbst wenn Sie bereits ReportingService haben, ist das Hinzufügen eines Interfaces eine normale Evolution und nicht „Interface um des Interfaces willen“ (hier haben wir ein angewandtes Ziel: das Proxy-Modell).

Erstellen wir das Interface ReportOperations (Pakete wie in unserem Projekt ContextFlow):

package com.example.contextflow.application.reporting;

public interface ReportOperations {
    // Vertrag: Der aufrufende Code hängt vom Interface ab, nicht von der konkreten Implementierung
    String generate();
}

Jetzt erstellen wir das Target-Objekt – eine normale Implementierung. Hier gibt es bewusst keine Magie: einfach eine Methode, die einen String zurückgibt.

package com.example.contextflow.application.reporting;

public class ReportingService implements ReportOperations {
    @Override
    public String generate() {
        // Einfache Implementierung, damit im Beispiel sichtbar ist: Die Logik lebt im Target
        return "daily-report";
    }
}

Beachten Sie die subtile, aber wichtige Bedeutung: Der Vertrag für den aufrufenden Code ist ReportOperations, und ReportingService ist nur eine konkrete Implementierung. Falls ReportingService morgen proxied wird, sollte der aufrufende Code nicht darunter leiden. Er lebt ohnehin über das Interface.

Jetzt brauchen wir eine möglichst transparente Möglichkeit, die Substitution der Bean-Instanz selbst zu sehen. Daher gehen wir über den BeanPostProcessor: Er steht bereits im Pfad der Container-Verarbeitung und zeigt genau den Moment, in dem die ursprüngliche Bean aufhört, das Objekt zu sein, das der Container nach außen gibt. Dies ist kein neuer alltäglicher Rezept „Proxys manuell in Spring erstellen“, sondern eine lehrreiche Röntgenaufnahme desselben Substitutionsmodells, auf dem auch die AOP-Infrastruktur aufbaut.

3. Proxy über BeanPostProcessor

Jetzt kommt der „Aha!“-Moment. Wir schreiben unseren eigenen BeanPostProcessor, der nach der Initialisierung einer bestimmten Bean nicht das ursprüngliche Objekt zurückgibt, sondern ein JDK proxy darum herum. Dies wird ein lehrreiches, maximal transparentes Beispiel: Das Proxy gibt einfach „before“ aus und misst die Ausführungszeit. Die Geschäftslogik bleibt dabei im Target-Objekt.

Das Gerüst des BeanPostProcessor sieht so aus: Wir filtern nur die gewünschte Bean nach Name und Typ, den Rest lassen wir unberührt.

package com.example.contextflow.support.postprocessor;

import com.example.contextflow.application.reporting.ReportOperations;
import org.springframework.beans.factory.config.BeanPostProcessor;

import java.lang.reflect.Proxy;

public class ProxyWrappingBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // Wichtig: BPP sieht *alle* Beans, daher ist Filterung zwingend erforderlich
        if (!beanName.equals("reportingService") || !(bean instanceof ReportOperations target)) {
            return bean; // Alle anderen Beans lassen wir unberührt
        }

        // Geben Proxy statt des ursprünglichen Objekts zurück (Target lebt "innerhalb")
        return Proxy.newProxyInstance(
                ReportOperations.class.getClassLoader(),
                new Class
  []{ReportOperations.class},
                (proxy, method, args) -> {
                    // Technische Umhüllung des Aufrufs: Zeit messen
                    long started = System.nanoTime();
                    try {
                        // Wichtig: Die eigentliche Logik führt das Target aus
                        return method.invoke(target, args);
                    } finally {
                        // Protokollieren "von außen" – das ist Proxy-Verhalten, nicht Target
                        System.out.println("Took(ns): " + (System.nanoTime() - started));
                    }
                }
        );
    }
}

Wenn Sie das Wort invoke und Reflection verunsichert – das ist normal. Es ist nicht nötig, ein Reflection-Experte zu werden. Das Wichtige ist die Idee: Es gibt ein Interceptor-Objekt, das entscheidet, was um den Aufruf herum passiert. Spring AOP macht dasselbe, nur ordentlicher und massenhaft.

Und noch ein wichtiges Detail. Wir verwenden die Methode postProcessAfterInitialization, weil wir zeigen wollen: Der Bean ist bereits erstellt, Abhängigkeiten sind injiziert, Init-Logik ist ausgeführt, und erst dann sagt der Container: „Und jetzt wickel ich dich ein“. Das fügt sich gut in das mentale Modell des Lebenszyklus ein.

4. Demo in der Spring-Konfiguration einbinden

Hier ist uns Transparenz wichtiger als „Schönheit“ der Konfiguration, daher erstellen wir eine separate kleine Konfiguration für die Proxy-Inspektion. So ist einfacher zu sehen, dass die Bean-Instanz tatsächlich ersetzt wird.

Hier ist die Konfiguration, die sowohl den Target-Bean als auch unseren BeanPostProcessor registriert:

package com.example.contextflow.config.core;

import com.example.contextflow.application.reporting.ReportOperations;
import com.example.contextflow.application.reporting.ReportingService;
import com.example.contextflow.support.postprocessor.ProxyWrappingBeanPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ProxyInspectionConfig {

    @Bean
    ReportOperations reportingService() {
        // Geben Vertrag (Interface) zurück, nicht Implementierung – so ist das Proxy-Modell natürlicher
        return new ReportingService();
    }

    @Bean
    static BeanPostProcessor proxyWrappingBeanPostProcessor() {
        // BPP registrieren: Er ersetzt den Bean nach der Initialisierung durch ein Proxy
        return new ProxyWrappingBeanPostProcessor();
    }
}

BeanPostProcessor ist hier als static @Bean deklariert: Für die frühe Infrastruktur passt dies gut zur bereits bekannten Logik der Startup-Pipeline und verwischt nicht die Grenze zwischen normalen Beans und speziellen Container-Mechanismen.

Hier gibt es einen nützlichen Trick: @Bean gibt ReportOperations zurück, nicht ReportingService. Wir legen vorab den Referenztyp fest – das Interface. Wenn in Ihrem Projekt Komponentenscan und @Service verwendet werden, bleibt das Prinzip dasselbe: Wir injizieren über den Vertrag, nicht über die konkrete Klasse, wenn das sinnvoll ist.

5. Laufzeittyp des Beans aus dem Kontext prüfen

Hier benötigen wir dieselben grundlegenden Prüfungen wie bei jedem Proxy: die tatsächliche Laufzeitklasse, den Interface-Vertrag und einen schnellen Test auf JDK proxy. Nicht raten anhand des Bean-Namens – wir müssen sehen, was der Container tatsächlich ausgegeben hat.

Erstellen wir eine kleine Anwendung zum Ausführen. Es kann eine separate main()-Methode sein, nicht notwendigerweise den Hauptstart von ContextFlow umschreiben.

package com.example.contextflow;

import com.example.contextflow.application.reporting.ReportOperations;
import com.example.contextflow.config.core.ProxyInspectionConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ProxyInspectionApp {

    public static void main(String[] args) {
        // Kontext manuell erstellen, da dies ein lehrreicher Start/Inspektion ist
        try (var ctx = new AnnotationConfigApplicationContext(ProxyInspectionConfig.class)) {
            // Holen Bean über Vertrag – so sollte es in DI sein
            ReportOperations ops = ctx.getBean(ReportOperations.class);

            // Diagnose: Laufzeitklasse anzeigen (höchstwahrscheinlich ein Proxy)
            System.out.println(ops.getClass().getName()); // jdk.proxy2.$Proxy...
            // Geschäftsaufruf läuft durch Proxy und wird an Target delegiert
            System.out.println(ops.generate());           // daily-report
        }
    }
}

Mit hoher Wahrscheinlichkeit sehen Sie etwas wie jdk.proxy2.$Proxy12 (die genaue Nummer variiert) und dann die Zeile daily-report. Aber wichtiger ist: Zwischen diesen Zeilen erscheint die Zeitausgabe des Proxys:

Took(ns): 12345
daily-report

Das bedeutet, der Aufruf ging tatsächlich durch das Proxy und nicht direkt zum Target.

Jetzt fügen wir ein paar Typprüfungen hinzu. Genau das, was für eine echte Inspektion nötig ist: zu verstehen, welches Objekt im Kontext liegt, und den Vertrag nicht mit der Implementierung zu verwechseln.

import com.example.contextflow.application.reporting.ReportOperations;
import com.example.contextflow.application.reporting.ReportingService;

import java.lang.reflect.Proxy;

// Prüfen: Ist es überhaupt eine Proxy-Klasse?
System.out.println(Proxy.isProxyClass(ops.getClass()));     // true
// Vertrag (Interface) eingehalten – das ist das Wichtigste
System.out.println(ops instanceof ReportOperations);        // true
// Implementierung jedoch nicht, da es nicht ReportingService ist, sondern eine Hülle
System.out.println(ops instanceof ReportingService);        // false

Genau das haben wir erreicht: Die Logik bleibt in ReportingService, aber aus dem Kontext kommt eine Proxy-Instanz. Wenn Abhängigkeiten über das Interface entworfen werden, arbeitet der Geschäftscode ruhig über den Vertrag weiter.

6. getBean() nach Name und Laufzeittyp

Der Bean-Name garantiert nicht die genaue Klasse des Objekts. In Spring ist es eher ein „Slot“ als ein „Instanzpass“.

Zeigen wir dies ohne unnötige Philosophie: Holen wir den Bean per Name und vergleichen das Verhalten.

// Holen Bean per Name – es ist genau ein "Slot", kein Versprechen über die genaue Klasse
Object bean = ctx.getBean("reportingService");

System.out.println(bean.getClass().getName()); // jdk.proxy2.$Proxy...
System.out.println(bean instanceof ReportOperations); // true

Und jetzt der verführerischste (und gefährlichste) Schritt: „Na, wenn das reportingService ist, dann caste ich es mal auf ReportingService“. Machen wir das vorsichtig, mit try/catch, um die Realität zu sehen, nicht nur einen roten Bildschirm.

try {
    // Gefährlicher Cast: Bei JDK Proxy führt dies fast sicher zu ClassCastException
    ReportingService impl = (ReportingService) bean;
    System.out.println(impl.generate());
} catch (ClassCastException ex) {
    // Hier dokumentieren wir die Tatsache: Container hat eine Hülle zurückgegeben, nicht die Implementierung
    System.out.println("Cast failed: " + ex.getClass().getSimpleName()); // ClassCastException
}

Es geht nicht darum, sich zu merken, „es wird eine ClassCastException geben“. Es geht um Disziplin: Wenn Sie auf Code-Ebene anfangen, Beans auf Implementierungen zu casten, brechen Sie selbst Ihr Proxy-Modell. Der Container kann eine Hülle zurückgeben, und Sie müssen das im Hinterkopf behalten. Hier gilt dieselbe Regel wie bei jedem Proxy: Die Prüfung über den Vertrag (instanceof ReportOperations) ist stabil, während der Versuch, sich auf die exakte Klasse zu stützen, schnell bricht.

7. Wo die Container-Diagnose aufbewahren

Es ist sehr leicht, nach solchen Experimenten etwas Schreckliches zu tun: anfangen, ApplicationContext durch die gesamte Anwendung zu schleppen und in jedem Service ctx.getBean() zu schreiben, „um sicherzustellen, dass genau das kommt, was benötigt wird“. Das ist der bekannte Geruch aus Tag 9: Service Locator statt DI.

Um im Rahmen einer gesunden Architektur von ContextFlow zu bleiben, kann man sich an eine einfache Regel halten. Container-Inspektion ist Infrastruktur oder Diagnose, keine Geschäftslogik. Also gehört sie entweder in support.* oder in Start-/Szenariocode, nicht aber in application.service.

Falls Sie bereits einen separaten Infrastruktur-Bean für die Kontextdiagnose haben, lässt er sich bequem mit einem minimalen Utility „Zeige Laufzeitklasse“ erweitern. Zum Beispiel in dieser Art:

package com.example.contextflow.support.diagnostics;

import org.springframework.context.ApplicationContext;

public class ProxyDiagnostics {

    private final ApplicationContext ctx;

    public ProxyDiagnostics(ApplicationContext ctx) {
        // Kontext speichern nur für Infrastruktur-Diagnose, nicht für Geschäftslogik
        this.ctx = ctx;
    }

    public void printRuntimeType(String beanName) {
        // Inspektion: Bean per Name holen und Laufzeittyp ausgeben
        Object bean = ctx.getBean(beanName);
        System.out.println(beanName + " -> " + bean.getClass().getName());
    }
}

Dies ist kein ideales „produktives“ Utility (und wir machen kein separates Produkt daraus), aber methodisch ist es wichtig: Sie halten die „Fähigkeit, in den Container zu schauen“ an einem Ort und verteilen getBean() nicht über die Geschäftsservices. Die Geschäftsservices erhalten weiterhin Abhängigkeiten über den Konstruktor, wie wir früher gelernt haben.

Und wenn Sie ganz ohne ApplicationContext im Konstruktor auskommen möchten (was oft angenehmer ist), können Sie die Diagnose auf der Ebene von main/Scenario Runner belassen, wo getBean() ein akzeptables Werkzeug genau als Bootstrap/Inspection-API ist.

8. Schema: Proxy und Target

Manchmal ist es nützlich, einen Moment anzuhalten und einfach auf das Bild zu schauen. Wir haben nicht „die Berichterstattung durch Spring-Magie beschleunigt“, sondern eine einfache, fast mechanische Sache gemacht: ein Objekt in den Aufrufpfad eingefügt.

flowchart LR
  A["Ihr Code: ctx.getBean(ReportOperations)"] --> B["Proxy (JDK dynamic proxy)"]
  B -->|Aufrufdelegierung| C["Target: ReportingService"]
  B --> D["Technische Umhüllung: Timing/Logging"]

Wenn Sie sich dieses Schema merken und den Gedanken hinzufügen, dass „das Proxy über Container-Erweiterungspunkte (z. B. BPP) erscheint“, dann hören 80 % der „Spring-Magie“ auf, Magie zu sein, und werden zur Ingenieurskunst.

9. Typische Fehler bei der Inspektion von proxied Beans

Fehler Nr. 1: Typ über getClass() == ... prüfen und darauf Logik aufbauen.
Wenn Sie anfangen, die exakte Klasse eines Objekts zu vergleichen, verbieten Sie dem Container faktisch, dieses Objekt zu umhüllen. In der Welt der Proxys ist das fast immer eine schlechte Idee: Der Container kann einen anderen Laufzeittyp zurückgeben, aber das Objekt ist dennoch vollständig korrekt gemäß Vertrag. Besser, sich auf das Interface oder den Basistyp zu stützen und getClass() als Diagnosewerkzeug zu belassen, nicht als Bedingung in der Geschäftslogik.

Fehler Nr. 2: Versuchen, Interface-Proxy auf konkrete Implementierung zu casten.
Für JDK dynamic proxy ist das eine garantierte Falle: Der Proxy implementiert das Interface, ist aber keine Instanz der Implementierungsklasse. Der korrekteste Weg, dies zu vermeiden, ist, keine Implementierung zu verlangen, wo Sie den Vertrag benötigen. Wenn Sie unbedingt „das Target herausholen“ wollen, ist das eine separate Infrastrukturgeschichte und sollte nicht zur täglichen Gewohnheit werden.

Fehler Nr. 3: Einen BeanPostProcessor schreiben, der „alles und jeden“ proxied.
Ein Anfänger denkt: „Das ist cool, lassen wir alle Beans umhüllen“. Danach verhält sich die Anwendung seltsam, Typen brechen, es treten viele unerwartete Effekte auf, und das Debuggen wird zum Horror. In Lehr- (und echten) Projekten sollte das Proxying punktuell sein: Filter nach Name, Typ, Annotation, Paket – was auch immer, nur um den Container nicht in einen Fleischwolf zu verwandeln.

Fehler Nr. 4: Container-Diagnose in Geschäftsservices verschieben.
Sobald in OrderPlacementService ein ApplicationContext ctx „der Bequemlichkeit halber“ auftaucht, haben Sie einen Schritt zum Service Locator und versteckten Abhängigkeiten gemacht. In ContextFlow halten wir solche Dinge bewusst in support.* oder im Bootstrap-Code. Andernfalls verlieren Sie den Hauptvorteil von DI: Transparenz der Abhängigkeiten durch den Konstruktor.

Fehler Nr. 5: Annehmen, dass das Proxy das Target-Objekt „überschreibt“ oder „Code hinzufügt“.
Das Proxy muss das Target nicht ändern. Es kann das ursprüngliche Objekt völlig unberührt lassen und einfach zwischen Ihnen und ihm stehen. Daher ist es beim Debuggen wichtig, zu unterscheiden: „Innen gibt es ein Objekt mit Logik“ und „Im Container wurde Ihnen eine Hülle ausgehändigt“. Diese Klarheit spart Stunden des Lebens – und manchmal ganze Nervenzellen (die, wie man sagt, sich nicht regenerieren, aber Programmierer versuchen es trotzdem).

1
Aufgabe
Spring Core, Level 21, Lektion 4
Gesperrt
Proxy bean via `BeanPostProcessor`
Proxy bean via `BeanPostProcessor`
1
Aufgabe
Spring Core, Level 21, Lektion 4
Gesperrt
Inspektion von Bean nach Namen aus `ApplicationContext`
Inspektion von Bean nach Namen aus `ApplicationContext`
1
Umfrage/Quiz
Spring-Proxy, Level 21, Lektion 4
Nicht verfügbar
Spring-Proxy
Zielobjekt und Aufrufe
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION