Sie sind hier: Hibernate Proxies

Hibernate Proxies

In den folgenden 4 Kapiteln erläutere ich kurz das Proxyobjekt und die damit verbundenen Fallen für einen Sorftwareentwickler in der Projektpraxis.

1. Was sind Proxies?
2. Falle Attributdirektzugriff
3. Falle instanceOf
4. Falle getClass() equals getClass()

1. Was sind Proxies?

Ein Proxyobjekt ist ein Stellvertreter, der den Zugriff auf das eigentliche Objekt kontrolliert und die gleiche oder eine erweiterte Schnittstelle bietet. Operationen werden an dem Stellvertreter aufgerufen, der dadurch in der Lage ist, die Operation an das eigentliche Objekt zu delegieren und um zusätzliche Funktionalitäten zu erweitern. Ein typischer Einsatz dieses Pattern ist z.B. der Securitycheck auf Methodenebene, das Logging oder Initialisieren im Falle von Lazyloading.

In Hibernate ist das Layzyloading mittels des Entwurfsmusters Proxy / Stellvertreter implementiert und spielt dadurch eine zentrale Rolle bei der Verwendung von Hibernate. Das Ziel des Lazyloadings, Ressourcen zu sparen, wird dadurch erreicht, dass Daten erst aus der Datenbank geladen werden, wenn sie auch tatsächlich benötigt werden. Realisiert wird das Lazyloading indem beim Instanzieren eines Objektes zwar alle referenzierten Objekte ebenfalls instanziert werden, jedoch jeweils nur die ObjektId gesetzt wird. Erst beim ersten Zugriff auf ein Attribute diese Objekte (ausser auf die ObjektId) werden die tatsächlichen Daten geladen und am Objekt gesetzt.
Die hierfür notwendige Statusüberwachung (loaded, dirty...) dieser Objekte erfolgt über Proxyobjekte, welche die eigentlichen Objekte um interne Attribute erweitern.

Dieses Vorgehen von Hibernate führt in der Praxis sehr häufig zu Programmfehlern, erfordert es doch vom Softwareentwickler genaue Kenntnisse über den Sachverhalt und eine entsprechende Berücksichtigung bei der Implementierung.

Nachfolgend zeige ich die typischen Probleme und deren Lösungen beispielhaft an der einfachen Klasse Person auf.

Datenbanktabelle und Javaklasse:

Person.hbm.xml:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE hibernate-mapping PUBLIC
  3. "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
  4. "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
  5. <hibernate-mapping>
  6. <class name="de.velacon.proxydemo.Person" table="person" >
  7. <id name="id" column="id" type="long" >
  8. <generator class="sequence">
  9. <param name="sequence">seq_person</param>
  10. </generator>
  11. </id>
  12. <property name="name" column="name" type="string" insert="true" update="true" />
  13. <many-to-one name="mutter" column="mutter_id" class="de.velacon.proxydemo.Person"/>
  14. </class>
  15. </hibernate-mapping>

Der Hibernate-Proxy von Person:

Der Proxy wird von Hibernate zur Laufzeit als Subklasse von Person angelegt und um weitere Attribute (handler) für die Statusüberwachung erweitert. Erkennen kann man eine Proxyinstanz durch das _$$_javaassist im Klassennamen. Der Debugger ermöglicht einen Blick auf die Objektstruktur des Proxy. Dabei ist sehr schön zu sehen, dass der Proxy die gleichen Attribute wie die eigentliche Zielklasse hat. Er bietet somit die gleiche Schnittstelle des vertretenden Objektes an. Erweitert wurde die Klasse durch das Attribut handler, in dem Hibernate unter anderem auch die eigentliche Zielklasse (target), ablegt.

Wichtige Erkenntnisse:

  • Objektdaten werden nicht an den Attributen des Proxy gespeichert, sondern an dem target! Das Proxyobjekt delegiert also alle Setter und Getter an das Target.
  • Die Klasse des Proxy entspricht nicht der Klasse der vertretenden Klasse, sondern einer Subklasse!

Die Attribute am Proxy bleiben null, obwohl das Objekt im obigen Fall bereits mit id=1 und name="mutter" geladen wurde!

2. Falle Attributdirektzugriff

Besonders die in der Praxis üblichen Arten der Implementierung von equals() und hashCode() führen durch die Verwendung von Proxyobjekten in der Praxis zu gravierenden Fehlern. Gravierenden Fehlern, weil Hibernate die Methoden equals() und hashCode() für die Verwaltung des First- und Secondlevelcaches nutzt und diese ebenfalls im Zusammenhang mit Sets und Maps eine sehr große Rolle spielen.

Nachfolgend die übliche Art einer Implementierung von person.equals(), die zu Fehlern führt.

  1. @Override
  2. public boolean equals(Object obj) {
  3. if (obj == null) return false;
  4. if (obj == this) return true;
  5. if (!(obj instanceof Person)) return false;
  6. Person other = (Person) obj;
  7. return new EqualsBuilder()
  8. .append(this.id, other.id)
  9. .append(this.name, other.name)
  10. .append(this.mutter, other.mutter).isEquals();
  11. }

Im folgende JUnittest laden wir die Mutter in Zeile 3 als Person und in Zeile 4 als Assoziation zum Sohn als PersonProxy (lazy per default = true). Eigentlich sollten beide Mütter identisch sein, die Prüfung in Zeile 4 liefert jedoch einen AssertionFailedError!

  1. public void _testEquals() throws Exception {
  2. Person sohn = (Person)getHibernateTemplate().get(Person.class,SOHN_ID);
  3. Person mutter = (Person)getHibernateTemplate().load(Person.class,MUTTER_ID);
  4. assertEquals(mutter,sohn.getMutter());
  5. }

Die Lösung des Problems ist ganz einfach. Nutzen Sie niemals Direktzugriffe auf Attribute sondern nutzen Sie immer die Gettermethoden. Sicherlich kann man auch einfach auf Lazyloading verzichten und für alle Klassen lazy=false setzen. Meiner Meinung nach ist dies jedoch keine sinnvolle Lösung.

Die korrekte Implementierung von equals() sollte wie folgt aussehen:

In Zeile 8-10 vergleichen wir die jeweiligen Gettermethoden und erhalten dadurch auch bei einem equals von Proxy und NichtProxy eine Gleichheit.

  1. @Override
  2. public boolean equals(Object obj) {
  3. if (obj == null) return false;
  4. if (obj == this) return true;
  5. if (!(obj instanceof Person)) return false;
  6. Person other = (Person) obj;
  7. return new EqualsBuilder()
  8. .append(this.getId(), other.getId())
  9. .append(this.getName(), other.getName())
  10. .append(this.getMutter(), other.getMutter()).isEquals();
  11. }

Der obige JUnittest wird auf Grund der Änderung keinen AssertionFailedError mehr werfen und Sie sehen, die Objekte mutter und sohn.getMutter() sind nun equal.

3. Falle instanceOf

In Hibernate existierte eine Zeit lang ein Bug der dazu führte, dass der Vergleich von (PersonProxy instanceOf Person) false lieferte. Dieses Problem wurde zwar mittlerweile beseitigt, ich empfehle Ihnen jedoch die Funktion von instanceOf in Verbindung mit Proxyobjekten unbedingt zu testen.

Für den Fall, dass Sie eine Hibernateversion einsetzen bei der der obige instanceOf Vergleich fehltschlägt, können Sie das Problem wie folgt umgehen:
Nutzen Sie statt instanceOf den Vergleich der Klassen wie er im folgendem Kapitel beschrieben wird.

4. Falle getClass() equals getClass()

Vergleichen Sie möglichst niemals die Klassen von Objekten mittels getClass(). Nutzen Sie lieber instanceOf. Es sei denn, Sie nutzen die in Kapitel 3 erwähnte und fehlerhafte Hibernateversion. Ein Vergleich von Proxy.getClass().equals(NichtProxy.getClass()) wird false liefern! Dies führt in der Praxis dazu, dass der Vergleich unter Umständen mal true und mal false liefert. Je nachdem, ob ihr Objekt als Person oder als PersonProxy instantiiert wurde.
Nachfolgend die fehlerhafte Implementierung.

  1. public boolean verarbeite(Person person) {
  2. final boolean retVal;
  3. if (this.getClass().equals(person.getClass()) {
  4. // do something ...
  5. retVal = true;
  6. } else {
  7. retval = false;
  8. }
  9. return retVal;
  10. }

Die Lösung ist wiederum recht einfach, hat jedoch den großen Nachteil, dass Sie durch den Import einer Hibernateklasse eine Abhängigkeit ihrer Beans zu Hibernate eingehen.

  1. public boolean verarbeite(Person person) {
  2. final boolean retVal;
  3. if (HibernateProxyHelper.getClassWithoutInitializingProxy(this)
  4. .equals(HibernateProxyHelper.getClassWithoutInitializingProxy(person))) {
  5. // do something ...
  6. retVal = true;
  7. } else {
  8. retval = false;
  9. }
  10. return retVal;
  11. }