Die meisten Programmierer kommen mit Hibernate Proxies in Berührung, wenn sie mit der ominösen LazyInitializationException konfrontiert werden. Der schlechte Geschmack ist schwer abzuwaschen, aber Proxies sind ein notwendiges "Übel" bei der Arbeit mit Hibernate. In diesem Artikel erfahren Sie, warum Proxies wichtig sind. Er wird auch auf einige Fallstricke hinweisen, die bei der Arbeit mit Proxies häufig auftreten.
Was sind Proxys?
Proxies sind der Mechanismus, der es Hibernate ermöglicht, die zusammenhängende Wolke von Objekten in der Datenbank in kleinere Brocken aufzuteilen, die leicht in den Speicher passen.
Sehen wir uns ein Beispiel an.
| PERSON |
|---|
| ID | NAME | BOSS |
|---|
| 1 | Daan | NULL |
| 2 | Vincent | 1 |
| 3 | Eelco | 1 |
| 4 | Jan | 2 |
| 5 | Maarten | 3 |
| 6 | Erik | 3 |
public class Person {
private Long id;
private String name;
Privatperson Chef;
privat eingestellte Mitarbeiter;
}
Dies ist ein sehr einfaches Beispiel. Die Klasse auf der rechten Seite ist der Tabelle auf der linken Seite zugeordnet. Was passiert also, wenn Hibernate die Zeile mit NAME='Maarten' lädt? Um die Daten im Speicher zu speichern, erstellt Hibernate ein neues Person-Objekt und setzt die Spalten auf die Felder. Es speichert die Spalte ID im Feld id und die Spalte NAME im Feld name.
Bei der Spalte BOSS steht Hibernate vor einem Problem: Der Typ der Spalte ist NUMERIC, aber der Typ der Eigenschaft ist Person. Dies ist natürlich ein Fremdschlüssel zu einer anderen Zeile in der Tabelle PERSON. Hibernate könnte nun einfach die Daten aus der zugehörigen Zeile laden, ein neues Person-Objekt erstellen und in dem Feld speichern. Dies würde sich jedoch auf alle Zeilen in der Tabelle (und möglicherweise auch auf andere Tabellen in der Datenbank) auswirken, insbesondere wenn man auch die Eigenschaft employees berücksichtigt.

Anstatt die zugehörige Zeile zu laden, erstellt Hibernate ein Person-Objekt und setzt die Eigenschaft id auf den in der Spalte BOSS gefundenen Wert. Dieses Person-Objekt ist ein spezialisiertes Person-Objekt, das bei Bedarf die zugehörigen Daten lädt. Zu Beginn sind die Felder dieses neuen Person-Objekts nicht gesetzt, da die Daten noch nicht geladen sind. Wenn eine Methode für das Objekt aufgerufen wird, holt Hibernate die Daten aus der Spalte und füllt das Objekt auf. Dies ist der Proxy-Mechanismus.
Um dieses neue Verhalten (das Laden der Daten beim Aufruf einer Methode) hinzuzufügen, erstellt Hibernate mit CGLib eine dynamische Unterklasse von Person und fügt die gewünschte Funktionalität hinzu. Wir benötigen hier eine Unterklasse von Person, um dem Typ des Feldes Chef zu entsprechen.

Wie sieht das also aus? Die Betrachtung eines Proxys in einem Debugger ist recht interessant. Links sehen Sie ein Bild des Eclipse-Debuggers, das ein aus der Datenbank geladenes Person-Objekt anzeigt, wobei die Eigenschaft boss auf einen Hibernate-Proxy eingestellt ist. Ein Hibernate-Proxy lässt sich leicht am Klassennamen erkennen, der die Markierung "$EnhancerByCGLIB$" enthält. Das liegt daran, dass die Klasse zur Laufzeit von CGLib generiert wird.
Die Proxy-Klasse ist eine Unterklasse der Klasse Person und enthält daher dieselben Felder (wie Sie im Debugger sehen können), aber sie enthält auch einige zusätzliche Felder. Das wichtigste ist CGLIB$CALLBACK_0, das auf eine Instanz von CGLIBLazyInitializer, einer Hibernate-Klasse, gesetzt ist. Dies ist das Objekt, das es dem Proxy ermöglicht, die Daten bei Bedarf zu laden. Es enthält ein Feld "target", das das von Hibernate geladene Objekt enthält, wenn die Daten geladen werden.
Im Debugger können wir mit diesem Proxy lustige Dinge tun. Wenn Sie auf den Proxy selbst klicken, ruft der Debugger die
toString() -Methode für das Objekt auf, wodurch die Daten effektiv selbst geladen werden und das Zielfeld im LazyInitializer auf ein Person-Objekt mit den geladenen Daten gesetzt wird. Dies kann das Verhalten von Anwendungen und Unittests stark verändern! Dasselbe gilt übrigens auch für Hibernate-Sammlungen, die noch nicht geladen sind; wenn Sie in einem Debugger darauf klicken, wird die Sammlung geladen.
Proxy-Fallstrick 1: Feldzugang
Kommen wir zum ersten Fallstrick bei Proxies: Wenn ein Proxy geladen wird, werden die Daten nicht im Proxy-Objekt gespeichert, sondern im "Ziel"-Objekt. Die Felder im Proxy-Objekt bleiben für immer
null (oder ein Wert, mit dem sie initialisiert wurden), da alle Methoden, die auf dem Proxy aufgerufen werden, an das Zielobjekt delegiert werden.
Der Hauptbereich, in dem dieser Fallstrick auftritt, ist die Methode
equals(..). Eine sehr gängige Implementierung sieht etwa so aus:
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
true zurückgeben;
}
if (!(obj instanceof Person)) {
return false;
}
return name.equals((Person)obj).name);
}
um zu prüfen, ob das Namensfeld der beiden Person-Objekte identisch ist. Dies wird jedoch fehlschlagen, wenn wir die Methode wie
personObject.equals(personProxy) aufrufen. In der letzten Zeile der Methode verwenden wir den direkten Feldzugriff auf das andere Objekt, um das Feld name zu prüfen. Da es sich bei dem anderen Objekt um einen Proxy handelt, wird das Feld immer
Null sein, auch wenn der Proxy tatsächlich das personObject selbst kapselt. Obwohl die Methode
equals(..) ein typisches Problemfeld für diesen Fallstrick ist, ist er definitiv nicht darauf beschränkt. Jede Methode, die die deklarierende Klasse als Parameter nimmt (und somit den Zugriff auf Felder dieses Parameters erlaubt), hat das gleiche Problem.
Um dies zu verhindern, verwenden Sie in diesen Fällen Getter und Setter. Der Proxy wird geladen, wenn der Getter aufgerufen wird, und die Daten sind dann zugänglich.
Proxy-Fallstrick 2: instanceOf
Ein Proxy ist eine Unterklasse des Feldtyps, der benötigt wird. Hibernate sieht sich den Typ des Feldes an und erstellt eine dynamische Klasse mit diesem Typ, die die erforderlichen Funktionen hinzufügt. Das bedeutet, wenn der Feldtyp nicht der eigentliche Implementierungstyp ist, sondern eine Schnittstelle oder Superklasse, wird der Typ des Proxys anders sein als der Typ des eigentlichen Objekts: Wenn der Feldtyp die Superklasse der eigentlichen Implementierung ist, sind der Proxy-Typ und der Implementierungstyp Geschwister in der Typenhierarchie, die beide die Superklasse erweitern.
Das Proxy-Objekt ist also keine Instanz des implementierenden Typs, Anwendungscode, der von dieser Prüfung abhängt, wird fehlschlagen.
Um diesen Fallstrick zu vermeiden, schalten Sie das Proxying für die Hierarchie aus, indem Sie
lazy="false" auf der obersten Ebene der Klasse einstellen. Dadurch wird verhindert, dass Hibernate den Proxy-Mechanismus verwendet und immer Objekte dieses Typs oder von Untertypen lädt.
Fazit
Proxying ist eine der wichtigsten Funktionen von Hibernate. Auch wenn es einige Zeit dauert, bis Sie seine Bedeutung erkennen und mit seinen Funktionen arbeiten können, wird Ihnen dies die Entwicklung von Anwendungen mit Hibernate erleichtern. Wenn Sie die beiden in diesem Blog besprochenen Fallstricke im Auge behalten, werden Sie schwer zu findende Fehler in Ihren Anwendungen vermeiden.