Skip to content


Fehlersuche mit dem Wicket PageSerializer

Die Entwicklung zustandsbehafteter Komponenten mit Wicket ist einfach und fühlt sich recht natürlich an, da man die meisten Sprachfeatures von Java benutzen kann. Trotzdem gibt es verschiedene Dinge zu beachten, damit die damit realisierten Anwendungen performant und effizient arbeiten. Damit eine Wicket-Anwendung auch mit sehr vielen Nutzern und mit sehr vielen zustandsbehafteten Komponenten umgehen kann, werden Seiten serialisiert (und bei Bedarf wieder deserialisiert) und verschwinden für die Zwischenzeit aus dem Speicher. Dieser Prozess ist wenig transparent und man erhält selten die Gelegenheit, sich mal anzusehen, was genau eigentlich alles serialisiert wird.

In diesem Artikel soll es darum gehen, die Serialisierung als Hilfsmittel zur Fehlersuche zu benutzen. Die Fehler, die damit auffindbar sind, beziehen sich z.B. einen auf Probleme, wenn Komponenten nicht alle temporären Daten freigeben (und damit unnötig Speicher verbrauchen und diese Daten natürlich auch veralten können) oder wenn bestimmte Daten nicht serialisiert werden dürfen, weil der Zustand nach dem zurückserialisieren nicht definiert ist.

Seit Wicket 1.5.9 und 6.1.x kann man mehr Einfluss auf die Serialisierung nehmen. Dabei kann man verschiedene Checks konfigurieren, die beim Serialisieren die Objektinstanzen prüfen, die serialisiert werden sollen. Wir möchten folgende Prüfungen vornehmen (ob Objekte überhaupt serialisierbar sind, wird von Wicket selbst geprüft):

  • Entities dürfen nicht serialisiert werden (diese enthalten neben ORM-Magie meist vergängliche Daten)
  • Modelle, die nicht detached sind (diese enthalten veralteted Daten, die zwar nach dem zurücklesen weg sind, aber so lange die Seite noch im Speicher liegt, verwendet werden)
  • Komponenten, die nicht im Komponentenbaum hängen (da Wicket diese nicht erreichen kann, werden z.B. auch für keine Model-Instanz detach() aufgerufen)

Vorraussetzungen

Für unser Beispiel sind alle Entity-Instanzen mit folgendem Interface markiert:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public interface IEntity {
  2. }

Der Check gestaltet sich relativ einfach:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public class EntitySerializationNotAllowedChecker extends AbstractObjectChecker {
  2.   @Override
  3.   protected Result doCheck(Object object) {
  4.     if (object instanceof IEntity)
  5.       return new Result(Result.Status.FAILURE, "entity serialization not allowed");
  6.     return Result.SUCCESS;
  7.   }
  8. }

Jetzt müssen wir uns noch einen SerializerCheck konfigurieren:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public class DevelopmentSerializerCheck extends CheckingObjectOutputStream {
  2.   public DevelopmentSerializerCheck() throws IOException {
  3.     super(new ByteArrayOutputStream(),new OrphanComponentChecker(),new NotDetachedModelChecker(), new EntitySerializationNotAllowedChecker());
  4.   }
  5. }

In diesem Beispiel sind unsere verschiedenen Checks enthalten. Die ersten beiden (OrphanComponentChecker und NotDetachedModelChecker) bringt Wicket bereits mit, den letzten Check haben wir selbst geschrieben. Da im Wicket-eigenene Serializer der Check erst durchgeführt wird, wenn ein Objekt nicht durch Java serialisiert werden konnte (z.B. weil das Objekt nicht das passende Interface implementiert hat), müssen wir unseren Check so einbinden, dass das schon im Vorfeld und jedes mal passiert. Dazu überschreiben wir den JavaSerializer (wer viel IO-Last hat, kann sich auch mal den DeflatedJavaSerializer ansehen).

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public class DevelopmentJavaSerializer extends JavaSerializer {
  2.   private static final Logger log = LoggerFactory.getLogger(DevelopmentJavaSerializer.class);
  3.   public DevelopmentJavaSerializer(String applicationKey) {
  4.     super(applicationKey);
  5.   }
  6.   @Override
  7.   public byte[] serialize(Object object) {
  8.     try {
  9.       new DevelopmentSerializerCheck().writeObject(object);
  10.       
  11.       return super.serialize(object);
  12.     } catch (IOException e) {
  13.       log.error("error writing object " + object + ": " + e.getMessage(), e);
  14.       throw new WicketRuntimeException(e);
  15.     }
  16.   }
  17. }

Wie man vielleicht bemerkt hat, habe ich dem ganzen einen Namen gegeben, der darauf hindeutet, dass man den Serializer besser nicht in einer Produktivumgebung benutzt. Wenn eine Seite einen Fehler wirft, konnte sie nicht serialisiert werden. Somit kann später nicht auf diese zurückgegriffen werden. Da wir strengere Regeln als Wicket selbst aufstellen, kann die Anwendung ohne unseren Check durchaus für viele Nutzer funktionieren, obwohl wir vielleicht noch nicht alle Fehler gefunden haben.

In der Wicket-Application konfigurieren wir den Serializer wie folgt:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. @Override
  2. public void init()
  3. {
  4.   super.init();
  5.   ...
  6.   if (getConfigurationType()==RuntimeConfigurationType.DEVELOPMENT) {
  7.     getFrameworkSettings().setSerializer(new DevelopmentJavaSerializer(getApplicationKey()));
  8.   }
  9.   ...
  10. }

Damit sind die Vorbereitungen abgeschlossen. Wichtiger Hinweis: Zustandslose Seiten werden nicht serialisiert, sondern immer als neue Instanzen erzeugt. Für diese Seiten werden diese Prüfungen nicht durchgeführt.

Beispiele für Fehlerquellen

Das einfachste Beispiel ist das für unsere Entities, die nicht serialisiert werden sollen. Ich verzichte hier auf das Markup, weil das für das Ergebnis nicht relevant ist. Der Aufruf von setStatelessHint(false) ist notwendig, damit Wicket annimmt, das diese Seite serialisert werden muss. Es ist normalerweise nicht einfach, zustandlose Seiten zu erstellen, diesen Aufruf sollte man nur vornehmen, wenn man einen sehr guten Grund hat.

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public class EntityAsFieldPage extends WebPage {
  2.   MyEntity shouldNotBeSerialized=new MyEntity();
  3.   
  4.   public EntityAsFieldPage() {
  5.     
  6.     setStatelessHint(false);
  7.   }
  8.   static class MyEntity implements IEntity {
  9.   }
  10. }

Wenn diese Seite aufgerufen wird, werden wir im Logfile folgendes finden (Wicket wird die Seite normal anzeigen, den Fehler bekommt ein Nutzer nicht zu sehen):

ERROR - ListenerCollection         - Error invoking listener: org.apache.wicket.Application$2@5a388c74
org.apache.wicket.util.objects.checker.CheckingObjectOutputStream$ObjectCheckException: entity serialization not allowed
A problem occurred while checking object with type: de.wicketpraxis.usecase.entities.EntityAsFieldPage$MyEntity
Field hierarchy is:
  0 [class=de.wicketpraxis.usecase.entities.EntityAsFieldPage, path=0]
    de.wicketpraxis.usecase.entities.EntityAsFieldPage$MyEntity de.wicketpraxis.usecase.entities.EntityAsFieldPage.shouldNotBeSerialized [class=de.wicketpraxis.usecase.entities.EntityAsFieldPage$MyEntity] <----- field that is causing the problem
  at org.apache.wicket.util.objects.checker.CheckingObjectOutputStream.internalCheck(CheckingObjectOutputStream.java:370)
  at org.apache.wicket.util.objects.checker.CheckingObjectOutputStream.check(CheckingObjectOutputStream.java:344)
  ...

Egal wie tief diese Instanz im Komponentenbaum hängen würde, durch die Serialisierung wird jedes Vorkommen geprüft und gefunden.

Models, Models, Models

Eine der häufigsten Fehlerquellen sind Wicket-Model-Instanzen. Das liegt sicher daran, dass man Models überall benutzt und man recht einfach Fehler einbauen kann. Ein einfaches Beispiel für einen Fehler:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public class DirectModelUsagePage extends WebPage {
  2.   public DirectModelUsagePage() {
  3.     final IModel<Date> dateModel = new LoadableDetachableModel<Date>() {
  4.       @Override
  5.       protected Date load() {
  6.         return new Date();
  7.       }
  8.     };
  9.     add(new WebMarkupContainer("now") {
  10.       @Override
  11.       public void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {
  12.         replaceComponentTagBody(markupStream, openTag, ""+dateModel.getObject());
  13.       }
  14.     });
  15.     setStatelessHint(false);
  16.   }
  17. }

In dem Beispiel wird ein Model verwendet, das temporär Daten erzeugt. Genauso gut könnten hier viele Datensätze geladen werden. Würden wir das Model z.B. einer Label-Komponente als Model mitgeben, würde sich das Label darum kümmern, das detach() auch für das Model aufgerufen wird. In diesem Beispiel wird allerdings das direkt Model benutzt. Das passiert recht häufig (z.B. Javascript-Code dynamisch erstellen möchte) und sieht auf den ersten Blick nicht wie ein Problem aus. Hier hilft ein einfaches überschreiben von detach() im WebMarkupContainer, der dann für das Model detach() aufruft. Daher sollte man soetwas immer in Komponenten auslagern, die sich darum kümmern, sonst läuft man Gefahr, dass man es mal vergisst. Wenn detach() nicht aufgerufen wird und die Seite noch im Speicher liegt, sieht man mit hoher Wahrscheinlichkeit alte Daten.

Im Log sehen wir folgendes:

ERROR - ListenerCollection         - Error invoking listener: org.apache.wicket.Application$2@312cfd62
org.apache.wicket.util.objects.checker.CheckingObjectOutputStream$ObjectCheckException: Not detached model found!
A problem occurred while checking object with type: org.apache.wicket.model.LoadableDetachableModel
Field hierarchy is:
  8 [class=de.wicketpraxis.usecase.models.DirectModelUsagePage, path=8]
    private java.lang.Object org.apache.wicket.MarkupContainer.children [class=org.apache.wicket.markup.html.WebMarkupContainer, path=8:now]
      private final org.apache.wicket.model.IModel de.wicketpraxis.usecase.models.DirectModelUsagePage$2.val$dateModel [class=org.apache.wicket.model.LoadableDetachableModel] <----- field that is causing the problem
  at org.apache.wicket.util.objects.checker.CheckingObjectOutputStream.internalCheck(CheckingObjectOutputStream.java:370)
  ...

Ein anderes Beispiel, bei dem nicht für alle Models detach aufgerufen wird, ist auch recht häufig:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public class ModelReadsModelPage extends WebPage
  2. {
  3.   public ModelReadsModelPage()
  4.   {
  5.     final IModel<Date> dateModel = new LoadableDetachableModel<Date>()
  6.     {
  7.       @Override
  8.       protected Date load()
  9.       {
  10.         return new Date();
  11.       }
  12.     };
  13.     
  14.     IModel<String> dateAsString = new AbstractReadOnlyModel<String>() {
  15.       
  16.       @Override
  17.       public String getObject() {
  18.         return ""+dateModel.getObject();
  19.       }
  20.     };
  21.     add(new Label("now", dateAsString));
  22.     
  23.     setStatelessHint(false);
  24.   }
  25. }

Das zweite Model greift auf das erste Model zu. Das zweite Model wird dem Label mitgegeben. Wenn nun Wicket für alle Komponenten detach() aufruft, dann gibt das Label das an das zweite Model weiter, doch das erste Model bleibt für Wicket unsichtbar.

Im Log sehen wir folgendes:

ERROR - ListenerCollection         - Error invoking listener: org.apache.wicket.Application$2@7f0ab78a
org.apache.wicket.util.objects.checker.CheckingObjectOutputStream$ObjectCheckException: Not detached model found!
A problem occurred while checking object with type: org.apache.wicket.model.LoadableDetachableModel
Field hierarchy is:
  10 [class=de.wicketpraxis.usecase.models.ModelReadsModelPage, path=10]
    private java.lang.Object org.apache.wicket.MarkupContainer.children [class=org.apache.wicket.markup.html.basic.Label, path=10:now]
      java.lang.Object org.apache.wicket.Component.data [class=org.apache.wicket.model.AbstractReadOnlyModel]
        private final org.apache.wicket.model.IModel de.wicketpraxis.usecase.models.ModelReadsModelPage$2.val$dateModel [class=org.apache.wicket.model.LoadableDetachableModel] <----- field that is causing the problem
  at org.apache.wicket.util.objects.checker.CheckingObjectOutputStream.internalCheck(CheckingObjectOutputStream.java:370)
  ...

Auch hier kann man im zweiten Model detach() überschreiben und an das erste Model weitergeben. Allerdings sollte man diese Fehlerquelle aktiv vermeiden. Dazu gibt es verschiedene Ansätze. Eine Lösungsmöglichkeit habe ich unter http://www.wicket-praxis.de/blog/2009/10/28/wicket-model-transformation/ beschrieben, eine Implementierung kann man unter https://github.com/flapdoodle-oss/de.flapdoodle.wicket (wicket 6 – https://github.com/flapdoodle-oss/de.flapdoodle.wicket/tree/wicket6) finden und in eigene Projekte einbauen.

Komponenten ersetzen ist nicht immer evil

Es gibt sinnvolle Anwendungen, wo es notwendig ist, Komponenten aus dem Komponentenbaum zu entfernen um sie später, so wie sie sind, wieder einzufügen. Dabei bleibt der Zustand der Komponente erhalten. Allerdings sind diese Fälle sehr selten. Meist gibt es bessere Lösungen (Komponenten können auch unsichtbar sein, eine neuen Instanz einer Komponenten ist meist NICHT teuer, Komponenten-Factories kann man durchaus benutzen). In folgendem Beispiel wird durch einen Klick auf den Link labelA durch labelB ersetzt:

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
  1. public class HiddenComponentPage extends WebPage {
  2.   private static final String WICKET_COMP_ID = "label";
  3.   Label labelA = new Label(WICKET_COMP_ID,Model.of("A"));
  4.   Label labelB = new Label(WICKET_COMP_ID,Model.of("B"));
  5.   
  6.   public HiddenComponentPage() {
  7.     add(labelA);
  8.     add(new Link<Void>("link") {
  9.       @Override
  10.       public void onClick() {
  11.         HiddenComponentPage.this.replace(labelB);
  12.       }
  13.     });
  14.     setStatelessHint(false);
  15.   }
  16. }

Somit hängt einer der Komponenten nicht im Komponentenbaum. Wenn man jetzt auf Funktionen der Komponente, die für Wicket nun unsichtbar geworden ist, zurückgreift, dann können Models geladen und nicht wieder aufgeräumt werden oder Fehler auftreten, die durch den fehlenden Wicket-Zyklus ausgelöst werden.

Im Log sehen wir folgendes:

ERROR - ListenerCollection         - Error invoking listener: org.apache.wicket.Application$2@175b28d8
org.apache.wicket.util.objects.checker.CheckingObjectOutputStream$ObjectCheckException: A component without a parent is detected.
A problem occurred while checking object with type: org.apache.wicket.markup.html.basic.Label
Field hierarchy is:
  4 [class=de.wicketpraxis.usecase.replacements.HiddenComponentPage, path=4]
    org.apache.wicket.markup.html.basic.Label de.wicketpraxis.usecase.replacements.HiddenComponentPage.labelB [class=org.apache.wicket.markup.html.basic.Label, path=label] <----- field that is causing the problem
  at org.apache.wicket.util.objects.checker.CheckingObjectOutputStream.internalCheck(CheckingObjectOutputStream.java:370)
  at org.apache.wicket.util.objects.checker.CheckingObjectOutputStream.check(CheckingObjectOutputStream.java:344)
  ...

Diese Fehler würde man durch andere Methoden sicher viel schwerer entdecken. Aus meiner Erfahrung hat sich dieser Ansatz recht gut bewährt, gerade auch wenn es darum geht, dass sich solche Fehler nicht wieder einschleichen. Ich bin gespannt, ob jemand noch andere Ideen hat, wie man das Konzept auf andere Anwendungsfälle erweitern kann.

 

Posted in Refactoring, Wicket.


2 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. Christoph says

    Hallo Michael,

    danke fuer den informativen Beitrag. Ich habe aktuell einen Usecase fuer serialisierte Entities: Seiten mit AJAX. Benutzer haben eine Vielzahl von Controls zur Verfuegung, mit denen sie das aktuelle Entity bearbeiten. Dies geschieht ueber AJAX-Calls, da man den Benutzer direkt auf Fehler hinweisen oder Komponenten aktualisieren moechte. Das Entity soll aber erst in der DB gespeichert werden, wenn der Benutzer auf den Button “Speichern” klickt.

    Verwende ich an dieser Stelle ein LDM fuer das Entity, so wird es fuer jeden AJAX-Request neu von der DB geladen und alle vorherigen Aenderungen gehen verloren.

    Wuerdest du in diesem Fall auch die Entities serialisieren oder gibt es eine bessere Loesung?

    Christoph

    • michael says

      In dem Fall würde ich eine serialisierbare Bean initial mit Werten aus der DB befüllen und dann unabhängig davon ändern. Ohne Ajax und mit klassischen Formularen funktioniert es eigentlich ähnlich, weil mit dem ersten Form-Submit die Werte aus der Eingabe maßgeblich sind. Interessant wird es allerdings dann, wenn jemand anderes in der DB ebenfalls Änderungen an den selben Daten vornehmen kann. Man sollte unter diesen Umständen vielleicht mit Versionen arbeiten.



Some HTML is OK

or, reply to this post via trackback.