Komplexe konfigurierbare Webanwendungen mit der JBoss Suite

Lessons Learned

Christian Groth
Der Conversation-Kontext und Bijection

Behält man die vorherigen Gedanken im Hinterkopf und überlegt sich, in welchen Situationen eine Conversation typischerweise verwendet wird, so wird klar, dass das immer für einen bestimmten Workflow in der Applikation der Fall sein wird. Ein Workflow besteht also aus mehreren Seiten, die zusammenhängend ein Feature der Anwendung realisieren. Entscheidend ist, dass ein Workflow in der Anwendung einen definierten Eingangszustand haben muss. Dieser Zustand wird sich im Wesentlichen in bestimmten bereits geladenen Daten, die im Kontext liegen, widerspiegeln. Versucht man nun dieses Konzept ein wenig zu abstrahieren, könnte man sagen, dass es dem Workflow egal sein kann, woher die Daten kommen, er muss sie einfach nur benutzen und seine Arbeit tun. Leider ist das mit eben genau den Gedankengängen von oben nicht möglich. Dieses Problem führte dazu, dass während der Projektlaufzeit verschiedene Varianten ausprobiert wurden, um solch einen Workflow zu initialisieren bzw. ihm die Daten zur Verfügung zu stellen. Daraus ergab sich in einem schnell (vor allem in die Breite) wachsenden Projekt die Situation, dass verschiedene Lösungen parallel existierten. Zudem kam die Problematik hinzu, dass in einigen Situationen nicht ganz klar war, welche Objekte nun von welchem Workflow (und damit auch mit welcher Session) in den Kontext geladen wurden. Rückblickend haben sich im Wesentlichen zwei Ansätze im Projekt etabliert:

  • Nimm den Initialzustand aus dem Kontext und lade alle Objekte mit der aktuellen Session neu
  • Übergebe keine Objekte, sondern z. B. nur die Datenbank-IDs, und lade die Objekte immer aus der Datenbank

Letzterer Ansatz ist mit Sicherheit der „besser gekapselte“. Hier wird der Zustand direkt zu Beginn des Workflows aufgebaut und nicht „zusammengesammelt“. Jedoch muss man hier auch Nachteile in Kauf nehmen bzw. sich Sonderregelungen einfallen lassen:

  • Wie möchte man transiente Objekte übergeben? Als Objektreferenz über den Kontext? Dann haben wir eine Mischform beider Lösungen.
  • Durch das explizite Laden der Objekte aus der Datenbank werden unter Umständen unnötige Datenbankzugriffe erzeugt, was aus Sicht der Performance nicht wünschenswert ist.
  • Wie sollen bereits manipulierte Entitäten übergeben werden, die noch nicht in der Datenbank gespeichert sind und es zu diesem Zeitpunkt auch noch nicht sein sollen?

Aus diesen Gründen hat sich im Projekt schließlich die erste der beiden Lösungen durchgesetzt. Um unnötige Last auf der Datenbank zu verhindern, werden nur ausgewählte Objekte neu geladen. Das hat aber wiederum den Nachteil, dass Änderungen im Workflow dazu führen können, dass eventuell weitere Objekte neu geladen werden müssen, z. B. weil diese vorher nur zur Anzeige dienten, nun aber mit verarbeitet werden.

Seam Events

Der letzte Punkt, der im Zusammenhang mit Conversations angesprochen werden soll, ist das Event Handling. Seam bietet einen Mechanismus, parametrisierte Events zwischen Seam-Komponenten auszutauschen. Hierzu muss eine Methode einer Seam-Komponente als @Observer annotiert werden, um entsprechende Events zu empfangen. Auf Probleme mit diesem Mechanismus sind wir im Projekt gestoßen, wenn Sender und Empfänger der Events in verschiedenen Conversations leben und somit nach den obigen Überlegungen einen unterschiedlichen Persistence Context benutzen. Leider ist es so, dass bei der Verarbeitung eines Events nicht die Conversation, in der der Empfänger eigentlich lebt, aktiviert wird, sondern das Empfängerobjekt in diesem Fall einfach in der Conversation ausgeführt wird, in der das Event auch generiert wurde. Das führt genau dann wieder zu den obigen Problemen, wenn aufgrund der Nachrichtenverarbeitung weitere Daten geladen werden müssen, was dann mit der „falschen“ Session geschieht. Abhilfe schaffte uns hier ein üblicher Event Handler. Statt ein Event direkt zu verarbeiten, werden alle Events in einer Liste gesammelt. Zur eigentlichen Verarbeitung wird nun bei jedem Seitenzugriff, hinter dem die entsprechende Komponente steht, eine Methode aufgerufen, die gegebenenfalls vorliegende Events verarbeitet und anschließend aus der Liste löscht. Somit haben wir die Abarbeitung der Events solange verzögert, bis wir die Empfängerkomponente wieder in ihrer korrekten Umgebung benutzen. Im Prinzip haben wir damit das synchrone Event Handling für bestimmte Komponenten in eine asynchrone Variante überführt.

Konfigurierbare Anwendungslogik mit JBoss Rules

Neben bereits angesprochenen Fallstricken im Umgang mit Seam bestand die zweite große Herausforderung darin, einen Großteil der Anwendungslogik zur Laufzeit konfigurierbar zu halten. Wir haben uns dazu entschieden, für diesen Zweck JBoss Rules einzusetzen, um entsprechende Logik aus den Java-Klassen auszulagern. Natürlich ergeben sich alleine durch diese Anforderung einige Probleme während der Entwicklung, z. B.:

  • Welche konfigurierbaren Teile der Logik sollen überhaupt ausgelagert werden?
  • Es darf nicht zu generisch werden, da das deutlich auf die Komplexität schlägt.

Diese eher „allgemeinen und organisatorischen“ Probleme sollen hier aber nicht weiter im Fokus stehen. Stattdessen möchten wir einige konkrete Problemfelder rund um das JBoss Rules Framework und dessen Integration in die Applikation ansprechen.

JBoss-Rules-Performance

Bevor eine Regeldatei auf einem konkreten Faktenraum ausgewertet werden kann, muss diese zunächst übersetzt werden. Das Ergebnis solch einer Übersetzung ist eine Instanz der Klasse RuleBase. Die Übersetzung der Regel dauert um ein Vielfaches länger als die nachfolgende Auswertung der darin enthaltenen Businesslogik, auch bei großen und komplexen Regeldateien. Wir haben also einen Regel-Cache implementiert, der nur geänderte Dateien neu übersetzt. Außerdem haben wir den Übersetzungsprozess mit dem CachingContextClassLoader aus [3] deutlich beschleunigen können.

Ein weiterer kleiner Performance-Impact versteckt sich in der Programmierung der Regeln. Hierzu muss man unterscheiden, was eine Regel leisten soll. Soll sie eine Ergebnismenge liefern oder bestehende Objekte manipulieren? Für den ersten Fall kann man schnell auf die Idee kommen, die neu erstellten Objekte innerhalb der Regel in den Regelkontext einzufügen und diese anschließend durch eine einfach und elegant zu formulierende Query abzufragen. Jedoch führt jede Veränderung des Faktenraumes, bei der ein Objekt eingefügt oder gelöscht wird, zu einer erneuten Evaluierung aller Regeln in dieser Datei. Die bessere Lösung stellt ein von vornherein existierendes Objekt im Regelkontext dar, das die Ergebnismenge aufsammelt, im Prinzip reicht hier eine beliebige Listeninstanz des Collection API.

Insgesamt haben wir in diesem Projekt die Erfahrung gemacht, dass JBoss Rules auch sehr große Regeldateien mit mehr als 8 000 Zeilen sowie knapp über 430 Regeln schnell auswertet, sodass die Gesamtperformance dieser Lösung keine Kopfschmerzen verursacht.

Probleme mit MVEL und Hibernate Proxies

Auf ein unerwartetes Problem stießen wir, als wir das erste Mal stolz unsere Anwendung einem Integrationstest unterzogen. Jetzt zogen wir Objekte per Hibernate aus der Datenbank an und übergaben sie in den Auswertungskontext von JBoss Rules. Plötzlich kam es zu schwer reproduzierbaren Fehlern des Typs ClassCastException. Eine Weile lief das System ordentlich, bei einer bestimmten Nutzung trat der Fehler auf. Nach Neustart und Reproduktion war der Fehler weg, trat dafür aber an anderer Stelle in ähnlicher Weise wieder auf. Natürlich erst beim Kunden.

Es fiel auf, dass öfter das von JBoss Rules für die Regelauswertung genutzte MVEL im Stacktrace auftauchte. Nach einiger Analyse wurde das Problem klar. Bei der ersten Nutzung einer Regel wird sie kompiliert und dabei im Wesentlichen ein Syntaxbaum mit per Reflection ermittelten Methodenobjekten erzeugt. Diese beziehen sich auf die Typen der Fakten, wie sie beim ersten Aufruf der Regel verwendet wurden. Wurde dort ein POJO als Fakt genutzt, wurde z. B. die Getter-Methode der POJO-Klasse gespeichert. Kam der Fakt jedoch per Hibernate aus der Datenbank, so handelt es sich um einen Hibernate Proxy, z. B. JavAssist (das ist der Preis, den man für einen O/R Mapper nun mal zahlt).

Gespeichert wird also die Getter-Methode des Proxies. Wird dieselbe Regel später mit einem POJO gefüttert, wird die gespeicherte Proxy-Methode darauf angewendet, was nicht funktioniert: Der Proxy ist von der POJO-Klasse abgeleitet. Da es auf die Reihenfolge bei der ersten Regelanwendung ankommt, war der Fehler schwer nachzustellen.

Die im Internet zum Thema vertretenen Meinungen reichen von „Don’t use Rules“ bis zu „Hibernate, get out of my POJO!“ [4]. Alles für uns nicht wirklich hilfreich. Also haben wir getrickst und an den neuralgischen Stellen in MVEL vorsorglich den Hibernate Proxy entsorgt (Listing 2).

Listing 2
if (ctx instanceof HibernateProxy) {
  HibernateProxy proxy = (HibernateProxy) ctx;
  ctx = proxy.getHibernateLazyInitializer().getImplementation();
}

Es bleibt festzuhalten, dass die gleichzeitige Verwendung auch abgehangener Komponenten zu überraschenden Problemen führen kann und dass das Eindringen von Frameworks in Anwendungsobjekte zwar bequem ist, manchmal aber ganz schön anstrengend werden kann.

Geschrieben von
Christian Groth
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.