Skip to content


Wicket AjaxBookmarkablePageLink - Ajax und SEO

Nutzer haben eine hohe Erwartung an die Geschwindigkeit, mit der auch Webanwendungen auf Nutzereingaben reagieren sollen. Nicht erst seit gestern wird deshalb auf verschiedene Arten versucht, die Antwortzeit auf ein Minimum zu drücken. Häufig werden dann Seiteninhalte per Ajax ausgetauscht. Das ist angenehm für den Nutzer, Suchmaschinen sind aber (zur Zeit) nicht in der Lage, über solche Interaktionshürden zu springen und damit diese Inhalte in den Suchindex aufzunehmen.

Bisher hat man sich dann oft für das eine (kein Ajax) oder das andere (Ajax, versteckte SEO-Links) entschieden. Mit Wicket ist es nicht so schwer, einen sehr mächtigen Ansatz zu wählen, der beides kombiniert und das Leben des Entwicklers vereinfachen kann.

Wir zäumen das Pferd etwas von hinten aus, aber am Ende wird das alles einen Sinn ergeben. Versprochen. Zuerst brauchen wir etwas, was eine Seitenklasse und PageParameter in einen Wert vereint:

public class BookmarkablePageDestination<T extends WebPage> {

  private final Class<? extends T> _pageClass;
  private final PageParameters _pageParameters;

  public BookmarkablePageDestination(Class<? extends T> pageClass, PageParameters pageParameters) {
    _pageClass = pageClass;
    _pageParameters = new PageParameters(pageParameters);
  }

  public Class<? extends T> getPageClass() {
    return _pageClass;
  }

  public PageParameters getPageParameters() {
    // not immutable, so we have to copy
    return new PageParameters(_pageParameters);
  }

  public String asUrl() {
    return RequestCycle.get().urlFor(_pageClass, _pageParameters).toString();
  }

  public static <T extends WebPage> BookmarkablePageDestination<T> with(Class<T> pageClass) {
    return new BookmarkablePageDestination<T>(pageClass, new PageParameters());
  }

  public static <T extends WebPage> BookmarkablePageDestination<T> with(Class<T> pageClass,
      PageParameters pageParameters) {
    return new BookmarkablePageDestination<T>(pageClass, pageParameters);
  }
}

Da die Klasse PageParameters nicht unveränderlich ist, müssen wir eine Kopie anfertigen, um unerwünschte Nebeneffekte zu vermeiden. Als nächstes erweitern wir die AjaxLink-Klasse um eine Funktion, die sie aus Suchmaschinensicht zu einem normalen Link macht.

public abstract class AjaxBookmarkablePageLink<T, P extends WebPage> extends AjaxLink<T> {

  public AjaxBookmarkablePageLink(String id, IModel<T> model) {
    super(id, model);
  }

  @Override
  protected void onInitialize() {
    super.onInitialize();

    add(new AttributeModifier("href", new LoadableDetachableModel<String>() {

      @Override
      protected String load() {
        return getDestination(getModelObject()).asUrl();
      }
    }));
  }

  protected abstract BookmarkablePageDestination<P> getDestination(T modelValue);
}

Da sich das Ziel jederzeit ändern kann, muss das href-Attribut immer wieder erneuert werden. Dabei machen wir das Ergebnis abhängig vom Wert aus dem Model. Wichtiger Hinweis an dieser Stelle: normalerweise müsste man innerhalb der LoadableDetachableModel-Klasse für das verwendete Model die detach()-Methode aufrufen. Da das Model bereits durch die Link-Komponente in den Wicket-Renderzyklus eingebunden ist, ist das hier nicht notwendig. Man sollte das aber immer im Hinterkopf behalten, wenn man die Funktionsweise ändert.

Wir benötigen noch etwas, wass den Zustand der Seite abbildet und das man in Seitenparameter konvertieren und aus Seitenparametern auslesen kann. Wir nehmen ein einfaches Beispiel und verändern einen Zähler.

public interface IState extends Serializable {
  IState oneUp();
  IState oneDown();
  int getCounter();
}

public class StateConverter {

  private StateConverter() {
    // no instance
  }

  enum StateParameter {
    Count,
  };

  public static IModel<IState> asModel(PageParameters pageParameters) {
    return new Model<IState>(asState(pageParameters));
  }

  private static State asState(PageParameters pageParameters) {
    int count = pageParameters.get(StateParameter.Count.name()).toInt(0);
    return new State(count);
  }

  public static PageParameters asPageParameters(IState state) {
    return new PageParameters().add(StateParameter.Count.name(), state.getCounter());
  }

  static class State implements IState, Serializable {

    final int _count;

    public State(int count) {
      _count = count;
    }
    
    @Override
    public IState oneUp() {
      return new State(_count + 1);
    }
    
    @Override
    public IState oneDown() {
      return new State(_count - 1);
    }

    @Override
    public int getCounter() {
      return _count;
    }
  }
}

Jetzt brauchen wir eine Linkklasse, die den Zähler verändert.

public abstract class CountLink extends AjaxBookmarkablePageLink<IState, SeoPage> {

  private final WebMarkupContainer _ajaxBorder;

  public CountLink(String id, IModel<IState> model, WebMarkupContainer ajaxBorder) {
    super(id, model);
    _ajaxBorder = ajaxBorder;
  }

  @Override
  protected BookmarkablePageDestination<SeoPage> getDestination(IState state) {
    return BookmarkablePageDestination.with(SeoPage.class, StateConverter.asPageParameters(nextState(state)));
  }

  @Override
  public void onClick(AjaxRequestTarget target) {
    setModelObject(nextState(getModelObject()));
    target.add(_ajaxBorder);
  }

  protected abstract IState nextState(IState state);
}

Bei einem Klick auf den Link wird der Wert im Model verändert, der Wert aus getDestination() zeigt auf das potentielle Ziel, der Einfachheit halber wird außerdem eine Komponente übergeben, die per Ajax aktualisiert werden soll. Jetzt fügen wir alles zusammen:

public class SeoPage extends WebPage {

  public SeoPage(PageParameters pageParameters) {
    final IModel<IState> stateModel = StateConverter.asModel(pageParameters);

    final WebMarkupContainer ajaxUpdate = new WebMarkupContainer("ajaxUpdate");
    ajaxUpdate.setOutputMarkupId(true);

    ajaxUpdate.add(new Label("count", new PropertyModel<Integer>(stateModel, "counter")));

    ajaxUpdate.add(new CountLink("up", stateModel, ajaxUpdate) {

      @Override
      protected IState nextState(IState state) {
        return state.oneUp();
      }
    });

    ajaxUpdate.add(new CountLink("down", stateModel, ajaxUpdate) {

      @Override
      protected IState nextState(IState state) {
        return state.oneDown();
      }
    });

    add(ajaxUpdate);
  }
}

Zuerst wird aus den PageParametern der aktuelle Zustand der Seite ermittelt und in ein Model gepackt. Das Label zeigt den aktuellen Wert an, die zwei Links verändern den Wert. Das passende Markup dazu:

<html>
<head>
  <title>Seo Ajax Bookmarkable Page Links</title>
</head>
<body>
  <div wicket:id="ajaxUpdate">
    <span wicket:id="count"></span><br> 
    <a wicket:id="up">Up</a>
     | 
    <a wicket:id="down">Down</a>
  </div>
</body>
</html>

Und dann erhält man folgendes Ergebnis:

<body>
  <div wicket:id="ajaxUpdate" id="ajaxUpdate1">
    <span wicket:id="count">0</span><br> 
    <a wicket:id="up" id="up2" href="./seoPage?Count=1" onclick="var wcall=wicketAjaxGet(&#039;./seoPage?0-1.IBehaviorListener.1-ajaxUpdate-up&#039;,function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$(&#039;up2&#039;) != null;}.bind(this));return !wcall;">Up</a>
     | 
    <a wicket:id="down" id="down3" href="./seoPage?Count=-1" onclick="var wcall=wicketAjaxGet(&#039;./seoPage?0-1.IBehaviorListener.1-ajaxUpdate-down&#039;,function() { }.bind(this),function() { }.bind(this), function() {return Wicket.$(&#039;down3&#039;) != null;}.bind(this));return !wcall;">Down</a>
  </div>
</body>

Wie man sieht, kann man beide Anforderungen mit etwas Aufwand vereinen. Je nach Anforderung kann man das Konzept in verschiedenste Richtungen erweitern. Ich hoffe, ich konnte ein wenig die Richtung zeigen:)

Tags:

Veröffentlicht in Wicket, .

Wicket Page Serializer und Performanceoptimierung – Teil 3 - Unerwünschte Gäste

Hier folgt der 3. Teil der Serie (Wicket Page Serializer Teil 1). Dabei nähern wir uns dem Thema von vielleicht unerwarteter Seite.

Downside of final

Normalerweise kann ich die Verwendung von final in Variablendeklarationen immer begrüßen. Allerdings gibt es Nebeneffekte, die bekannt sein sollten und die unter Umständen unerwünschte Effekte in der Verwendung mit Wicket-Komponenten haben. Stellen wir uns folgendes Beispiel vor:

public class FinalsV1Page extends WebPage
{
  public FinalsV1Page()
  {
    final Irrelevant irrelevantToLabel=new Irrelevant();
    
    add(new Label("label", "Fun"){
      
      @Override
      protected void onInitialize()
      {
        super.onInitialize();
        if (false) {
          setDefaultModelObject(irrelevantToLabel.text);
        }
      }
    });
    setStatelessHint(false);
  }
  
  static class Irrelevant implements Serializable {
    String text="this could be something big.";
    byte[] veryBig=new byte[1024];
  }
}

Das Beispiel ist natürlich konstruiert und kommt so in der freien Wildbahn nicht direkt vor. Allerdings ist das Muster häufiger anzutreffen. Man verwendet für Methodenparamter den Zusatz final oder man möchte auf eine Methode einer anderen Klasseninstanz in einer anonymen Klasse zugreifen und markiert die Instanzvariable absichtlich mit final.

Das ich die Verwendung des Zugriffs auf die Instanz verhindert habe (if false), soll verdeutlichen, dass kein Zugriff auf die Instanz stattfinden muss, sondern das es reicht, das das theoretisch möglich wäre.

Das Markup ist auch für dieses Beispiel irrelevant. Wenn die Seite nun Serialisiert wird, bekommen wir folgende Ausgabe:

DEBUG - KryoSerializer             - Going to serialize: '[Page class = de.wicketpraxis.usecase.finals.FinalsV1Page, id = 1, render count = 1]'
DEBUG - TypeSizeReport             - 
===================================================================
|Type........................................................bytes|
-------------------------------------------------------------------
|[B...........................................................1027|
|de.wicketpraxis.usecase.finals.FinalsV1Page...................157|
|de.wicketpraxis.usecase.finals.FinalsV1Page$1..................92|
|java.lang.String...............................................39|
|java.lang.Integer..............................................15|
|org.apache.wicket.request.mapper.parameter.PageParameters.......3|
|org.apache.wicket.model.Model...................................2|
|de.wicketpraxis.usecase.finals.FinalsV1Page$Irrelevant..........1|
===================================================================

DEBUG - TreeSizeReport             - 
============================================================================================
|  #|Type.............................................................%|..sum|.local|.child|
--------------------------------------------------------------------------------------------
| #0|de.wicketpraxis.usecase.finals.FinalsV1Page(1)................100%|.1336|...155|..1181|
| #4|  org.apache.wicket.markup.html.basic.Label(label).............87%|.1169|....92|..1077|
|#13|    de.wicketpraxis.usecase.finals.FinalsV1Page$Irrelevant.....79%|.1057|.....1|..1056|
|#15|      [B.......................................................76%|.1027|..1027|.....0|
|#14|      java.lang.String("this...")...............................2%|...29|....29|.....0|
| #6|    org.apache.wicket.model.Model...............................0%|....6|.....2|.....4|
| #8|      java.lang.String("Fun")...................................0%|....4|.....4|.....0|
|#11|    java.lang.String("labe...").................................0%|....6|.....6|.....0|
| #9|    java.lang.Integer(=1343434906)..............................0%|....5|.....5|.....0|
| #0|    de.wicketpraxis.usecase.finals.FinalsV1Page(1)..............0%|....2|.....2|.....0|
|#10|    java.lang.Integer(=-1)......................................0%|....1|.....1|.....0|
|#16|  java.lang.Integer(=1342845082)................................0%|....5|.....5|.....0|
|#20|  org.apache.wicket.request.mapper.parameter.PageParameters.....0%|....3|.....3|.....0|
| #2|  java.lang.Integer(=1).........................................0%|....1|.....1|.....0|
|#17|  java.lang.Integer(=-1)........................................0%|....1|.....1|.....0|
|#18|  java.lang.Integer(=1).........................................0%|....1|.....1|.....0|
|#21|  java.lang.Integer(=1).........................................0%|....1|.....1|.....0|
============================================================================================

DEBUG - TreeSizeReport             - 
=============================================================================================
|   #|Type.............................................................%|..sum|.local|.child|
---------------------------------------------------------------------------------------------
|  #0|de.wicketpraxis.usecase.finals.FinalsV1Page(1)................100%|.1336|...155|..1181|
|  #4|  org.apache.wicket.markup.html.basic.Label(label).............87%|.1169|....92|..1077|
| #13|    de.wicketpraxis.usecase.finals.FinalsV1Page$Irrelevant.....79%|.1057|.....1|..1056|
| #15|      [B.......................................................76%|.1027|..1027|.....0|
| #14|      java.lang.String("this...")...............................2%|...29|....29|.....0|
|  #6|    org.apache.wicket.model.Model...............................0%|....6|.....2|.....4|
|  #8|      java.lang.String("Fun")...................................0%|....4|.....4|.....0|
|null|    java.lang.Integer(=1343434906|=-1)..........................0%|....6|.....6|.....0|
| #11|    java.lang.String("labe...").................................0%|....6|.....6|.....0|
|  #0|    de.wicketpraxis.usecase.finals.FinalsV1Page(1)..............0%|....2|.....2|.....0|
|null|  java.lang.Integer(=1|=1342845082|=-1).........................0%|....9|.....9|.....0|
| #20|  org.apache.wicket.request.mapper.parameter.PageParameters.....0%|....3|.....3|.....0|
=============================================================================================

Wie man deutlich sieht, hat er alles, was in der eigentlich unbenutzen Instanz enhalten war mit serialisiert. Das kann oft unumgänglich sein, allerdings glaube ich, dass man eben so oft zu besseren Lösungen kommt. Zum Vergleich ändern wir mal die eine Kleinigkeit:

public class FinalsV2Page extends WebPage
{
  public FinalsV2Page()
  {
    final Irrelevant irrelevantToLabel=new Irrelevant();
    
    add(new Label("label", "Fun"){
      
      @Override
      protected void onInitialize()
      {
        super.onInitialize();
        if (false) {
          setDefaultModelObject("this could be something big.");
        }
      }
    });
    setStatelessHint(false);
  }
  
  static class Irrelevant implements Serializable {
    String text="this could be something big.";
    byte[] veryBig=new byte[1024];
  }
}

Wenn man sich jetzt ansieht, was davon in der Serialisierung übrig bleibt, dann erkennt man recht leicht den Unterschied:

DEBUG - KryoSerializer             - Going to serialize: '[Page class = de.wicketpraxis.usecase.finals.FinalsV2Page, id = 2, render count = 1]'
DEBUG - TypeSizeReport             - 
===================================================================
|Type........................................................bytes|
-------------------------------------------------------------------
|de.wicketpraxis.usecase.finals.FinalsV2Page...................157|
|de.wicketpraxis.usecase.finals.FinalsV2Page$1..................36|
|java.lang.Integer..............................................15|
|java.lang.String...............................................10|
|org.apache.wicket.request.mapper.parameter.PageParameters.......3|
|org.apache.wicket.model.Model...................................2|
===================================================================

DEBUG - TreeSizeReport             - 
===========================================================================================
|  #|Type.............................................................%|.sum|.local|.child|
-------------------------------------------------------------------------------------------
| #0|de.wicketpraxis.usecase.finals.FinalsV2Page(2)................100%|.223|...155|....68|
| #4|  org.apache.wicket.markup.html.basic.Label(label).............25%|..56|....36|....20|
| #6|    org.apache.wicket.model.Model...............................2%|...6|.....2|.....4|
| #8|      java.lang.String("Fun")...................................1%|...4|.....4|.....0|
|#11|    java.lang.String("labe...").................................2%|...6|.....6|.....0|
| #9|    java.lang.Integer(=1343434906)..............................2%|...5|.....5|.....0|
| #0|    de.wicketpraxis.usecase.finals.FinalsV2Page(2)..............0%|...2|.....2|.....0|
|#10|    java.lang.Integer(=-1)......................................0%|...1|.....1|.....0|
|#12|  java.lang.Integer(=1342845082)................................2%|...5|.....5|.....0|
|#16|  org.apache.wicket.request.mapper.parameter.PageParameters.....1%|...3|.....3|.....0|
| #2|  java.lang.Integer(=1).........................................0%|...1|.....1|.....0|
|#13|  java.lang.Integer(=-1)........................................0%|...1|.....1|.....0|
|#14|  java.lang.Integer(=2).........................................0%|...1|.....1|.....0|
|#17|  java.lang.Integer(=1).........................................0%|...1|.....1|.....0|
===========================================================================================

DEBUG - TreeSizeReport             - 
============================================================================================
|   #|Type.............................................................%|.sum|.local|.child|
--------------------------------------------------------------------------------------------
|  #0|de.wicketpraxis.usecase.finals.FinalsV2Page(2)................100%|.223|...155|....68|
|  #4|  org.apache.wicket.markup.html.basic.Label(label).............25%|..56|....36|....20|
|  #6|    org.apache.wicket.model.Model...............................2%|...6|.....2|.....4|
|  #8|      java.lang.String("Fun")...................................1%|...4|.....4|.....0|
|null|    java.lang.Integer(=1343434906|=-1)..........................2%|...6|.....6|.....0|
| #11|    java.lang.String("labe...").................................2%|...6|.....6|.....0|
|  #0|    de.wicketpraxis.usecase.finals.FinalsV2Page(2)..............0%|...2|.....2|.....0|
|null|  java.lang.Integer(=1|=1342845082|=-1|=2)......................4%|...9|.....9|.....0|
| #16|  org.apache.wicket.request.mapper.parameter.PageParameters.....1%|...3|.....3|.....0|
============================================================================================ 

An diesem Beispiel kann man vielleicht sehen, dass es eine gute Strategie sein kann, Komponenten so klein wie möglich zu schneiden. Zum einen wird die Klasse bei mehrfacher Verwendung nur einmal serialisiert und außerdem läuft man nicht so große Gefahr, dass viele unerwünschte Dinge ebenfalls mitserialisiert werden. Und natürlich hat das auch Auswirkungen auf den Speicherverbrauch: solange eine Referenz auf ein Objekt gehalten wird, kann es nicht durch die Garbage-Collection aufgeräumt werden.

Wie kann man nun verhindern, dass so etwas unabsichtlich passiert: am einfachsten ist es, wenn man versucht, so wenig wie möglich anonyme Klassen zu verwenden. Das sollte sich schnell in vielfacher Hinsicht auszahlen.

Veröffentlicht in Refactoring, Wicket, .

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:

public interface IEntity {
}

Der Check gestaltet sich relativ einfach:

public class EntitySerializationNotAllowedChecker extends AbstractObjectChecker {

  @Override
  protected Result doCheck(Object object) {
    if (object instanceof IEntity)
      return new Result(Result.Status.FAILURE, "entity serialization not allowed");
    return Result.SUCCESS;
  }
}

Jetzt müssen wir uns noch einen SerializerCheck konfigurieren:

public class DevelopmentSerializerCheck extends CheckingObjectOutputStream {

  public DevelopmentSerializerCheck() throws IOException {
    super(new ByteArrayOutputStream(),new OrphanComponentChecker(),new NotDetachedModelChecker(), new EntitySerializationNotAllowedChecker());
  }
}

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).

public class DevelopmentJavaSerializer extends JavaSerializer {

  private static final Logger log = LoggerFactory.getLogger(DevelopmentJavaSerializer.class);

  public DevelopmentJavaSerializer(String applicationKey) {
    super(applicationKey);
  }

  @Override
  public byte[] serialize(Object object) {
    try {
      new DevelopmentSerializerCheck().writeObject(object);
      
      return super.serialize(object);
    } catch (IOException e) {
      log.error("error writing object " + object + ": " + e.getMessage(), e);
      throw new WicketRuntimeException(e);
    }
  }
}

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:

@Override
public void init()
{
  super.init();
  ...
  if (getConfigurationType()==RuntimeConfigurationType.DEVELOPMENT) {
    getFrameworkSettings().setSerializer(new DevelopmentJavaSerializer(getApplicationKey()));
  }
  ...
}

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.

public class EntityAsFieldPage extends WebPage {

  MyEntity shouldNotBeSerialized=new MyEntity();
  
  public EntityAsFieldPage() {
    
    setStatelessHint(false);
  }

  static class MyEntity implements IEntity {
  }
}

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:

public class DirectModelUsagePage extends WebPage {

  public DirectModelUsagePage() {

    final IModel<Date> dateModel = new LoadableDetachableModel<Date>() {

      @Override
      protected Date load() {
        return new Date();
      }
    };

    add(new WebMarkupContainer("now") {
      @Override
      public void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {
        replaceComponentTagBody(markupStream, openTag, ""+dateModel.getObject());
      }
    });

    setStatelessHint(false);
  }
}

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:

public class ModelReadsModelPage extends WebPage
{
  public ModelReadsModelPage()
  {
    final IModel<Date> dateModel = new LoadableDetachableModel<Date>()
    {
      @Override
      protected Date load()
      {
        return new Date();
      }
    };
    
    IModel<String> dateAsString = new AbstractReadOnlyModel<String>() {
      
      @Override
      public String getObject() {
        return ""+dateModel.getObject();
      }
    };

    add(new Label("now", dateAsString));
    
    setStatelessHint(false);
  }
}

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:

public class HiddenComponentPage extends WebPage {

  private static final String WICKET_COMP_ID = "label";

  Label labelA = new Label(WICKET_COMP_ID,Model.of("A"));
  Label labelB = new Label(WICKET_COMP_ID,Model.of("B"));
  
  public HiddenComponentPage() {

    add(labelA);
    add(new Link<Void>("link") {
      @Override
      public void onClick() {
        HiddenComponentPage.this.replace(labelB);
      }
    });
    setStatelessHint(false);
  }
}

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.

Veröffentlicht in Refactoring, Wicket, .

Wicket Page Serializer und Performanceoptimierung – Teil 2 - DateFormat

Wie im Teil 1 versprochen, werde ich im zweiten Teil an einem konkreten Beispiel demonstrieren, welche Fragen man vielleicht mit diesem neuen Werkzeug etwas besser beantworten kann.

Beispiel Datumsanzeige

Früher (und eher nicht später) wird man in einer Wicket-Anwendung ein Datum zur Anzeige bringen. Die einfachste Methode ist natürlich ein Label und ein IModelals Model-Instanz. Normalerweise möchte man aber eine ganz bestimmte Darstellung, so dass man nicht umhin kommt, das Datum irgendwie zu konvertieren. Ein möglicher Ansatz könnte in Abwandlungen wie folgt aussehen:

public class DateModel extends LoadableDetachableModel<Date>
{
  @Override
  protected Date load()
  {
    return new Date();
  }
}
public class DateFormatV1Page extends WebPage
{
  public DateFormatV1Page()
  {
    add(new Label("now", new DateToStringModel(new DateModel())));
    add(new Label("now2", new DateToStringModel(new DateModel())));

    setStatelessHint(false);
  }

  static class DateToStringModel extends LoadableDetachableModel<String>
  {
    SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
    private final IModel<Date> dateModel;

    public DateToStringModel(IModel<Date> dateModel)
    {
      this.dateModel = dateModel;
    }

    @Override
    protected String load()
    {
      return dateFormat.format(dateModel.getObject());
    }

    @Override
    protected void onDetach()
    {
      super.onDetach();
      dateModel.detach();
    }
  }
}

Das Markup spielt keine große Rolle, sollte aber nicht unerwähnt bleiben:

<html>
  <head>
    <title>Date Format</title>
  </head>
  <body>
    <h1>Date Format</h1>
    Now: <span wicket:id="now"></span><br>
    Now: <span wicket:id="now2"></span><br>
  </body>
</html>

Der Aufruf von setStatelessHint(false) im Konstruktor sorgt in diesem und in den folgenden Beispielen dafür, das Wicket annehmen muss, dass die Seite nicht zustandslos ist. Zustandslose Seiten werden nicht serialisiert, weil nicht zu erwarten ist, dass man diese Instanz später noch einmal benötigt.

Wenn diese Seite nun serialisiert wird, ergibt sich folgendes Bild:

DEBUG - KryoSerializer             - Going to serialize: '[Page class = de.wicketpraxis.usecase.dateformat.DateFormatV1Page, id = 0, render count = 1]'
DEBUG - TypeSizeReport             - 
===============================================================================
|Type....................................................................bytes|
-------------------------------------------------------------------------------
|[Ljava.lang.String;.......................................................287|
|de.wicketpraxis.usecase.dateformat.DateFormatV1Page.......................139|
|java.lang.String..........................................................112|
|org.apache.wicket.markup.html.basic.Label..................................83|
|java.text.SimpleDateFormat.................................................80|
|de.wicketpraxis.usecase.dateformat.DateFormatV1Page$DateToStringModel......80|
|java.lang.Integer..........................................................58|
|java.util.GregorianCalendar................................................46|
|java.lang.Character........................................................40|
|java.text.DecimalFormat....................................................36|
|java.util.Date.............................................................20|
|java.lang.Byte.............................................................12|
|java.lang.Boolean..........................................................10|
|java.util.Locale............................................................6|
|java.text.DateFormatSymbols.................................................4|
|java.math.RoundingMode......................................................3|
|org.apache.wicket.request.mapper.parameter.PageParameters...................3|
|[Ljava.lang.Object;.........................................................2|
|de.wicketpraxis.usecase.dateformat.DateModel................................2|
|java.text.DecimalFormatSymbols..............................................2|
===============================================================================

DEBUG - TreeSizeReport             - 
=============================================================================================================
|   #|Type.............................................................................%|..sum|.local|.child|
-------------------------------------------------------------------------------------------------------------
|  #0|de.wicketpraxis.usecase.dateformat.DateFormatV1Page(0)........................100%|.1025|...137|...888|
|  #4|  [Ljava.lang.Object;..........................................................85%|..876|.....2|...874|
|  #5|    org.apache.wicket.markup.html.basic.Label(now).............................64%|..658|....77|...581|
|  #8|      de.wicketpraxis.usecase.dateformat.DateFormatV1Page$DateToStringModel....55%|..570|....75|...495|
| #10|        java.text.SimpleDateFormat.............................................48%|..494|....72|...422|
| #16|          java.text.DateFormatSymbols..........................................23%|..242|.....2|...240|
| #25|            [Ljava.lang.String;.................................................7%|...76|....76|.....0|
| #28|            [Ljava.lang.String;.................................................5%|...56|....56|.....0|
| #26|            [Ljava.lang.String;.................................................3%|...39|....39|.....0|
| #19|            java.lang.String("GuMt...").........................................1%|...20|....20|.....0|
| #27|            [Ljava.lang.String;.................................................1%|...17|....17|.....0|
| #18|            [Ljava.lang.String;.................................................1%|...16|....16|.....0|
| #20|            java.util.Locale....................................................0%|...10|.....1|.....9|
| #21|              java.lang.String("DE")............................................0%|....3|.....3|.....0|
| #23|              java.lang.String("de")............................................0%|....3|.....3|.....0|
| #24|              java.lang.String("")..............................................0%|....2|.....2|.....0|
| #22|              java.lang.Integer(=-1)............................................0%|....1|.....1|.....0|
| #17|            [Ljava.lang.String;.................................................0%|....6|.....6|.....0|
| #30|          java.text.DecimalFormat..............................................12%|..125|....33|....92|
| #62|            java.text.DecimalFormatSymbols......................................4%|...45|.....1|....44|
| #63|              java.lang.String("�").............................................0%|....5|.....5|.....0|
| #64|              java.lang.String("€").............................................0%|....5|.....5|.....0|
| #70|              java.lang.String("∞").............................................0%|....5|.....5|.....0|
| #71|              java.lang.String("EUR")...........................................0%|....4|.....4|.....0|
| #68|              java.lang.String("E").............................................0%|....3|.....3|.....0|
| #65|              java.lang.Character...............................................0%|....2|.....2|.....0|
| #66|              java.lang.Character...............................................0%|....2|.....2|.....0|
| #67|              java.lang.Character...............................................0%|....2|.....2|.....0|
| #69|              java.lang.Character...............................................0%|....2|.....2|.....0|
| #72|              java.lang.Character...............................................0%|....2|.....2|.....0|
| #73|              java.lang.Character...............................................0%|....2|.....2|.....0|
| #74|              java.lang.Character...............................................0%|....2|.....2|.....0|
| #75|              java.lang.Character...............................................0%|....2|.....2|.....0|
| #76|              java.lang.Character...............................................0%|....2|.....2|.....0|
| #78|              java.lang.Character...............................................0%|....2|.....2|.....0|
| #20|              java.util.Locale..................................................0%|....1|.....1|.....0|
| #77|              java.lang.Integer(=3).............................................0%|....1|.....1|.....0|
| #38|            java.lang.Integer(=MAX).............................................0%|....5|.....5|.....0|
| #48|            java.lang.String("'-")..............................................0%|....3|.....3|.....0|
| #50|            java.lang.String("-")...............................................0%|....3|.....3|.....0|
| #39|            java.lang.Integer(=309).............................................0%|....2|.....2|.....0|
| #49|            java.lang.String("")................................................0%|....2|.....2|.....0|
| #51|            java.lang.String("")................................................0%|....2|.....2|.....0|
| #54|            java.lang.String("")................................................0%|....2|.....2|.....0|
| #55|            java.lang.String("")................................................0%|....2|.....2|.....0|
| #56|            java.lang.String("")................................................0%|....2|.....2|.....0|
| #57|            java.lang.String("")................................................0%|....2|.....2|.....0|
| #58|            java.math.RoundingMode..............................................0%|....2|.....2|.....0|
| #31|            java.lang.Boolean...................................................0%|....1|.....1|.....0|
| #32|            java.lang.Byte......................................................0%|....1|.....1|.....0|
| #33|            java.lang.Boolean...................................................0%|....1|.....1|.....0|
| #34|            java.lang.Byte......................................................0%|....1|.....1|.....0|
| #35|            java.lang.Byte......................................................0%|....1|.....1|.....0|
| #36|            java.lang.Integer(=0)...............................................0%|....1|.....1|.....0|
| #37|            java.lang.Integer(=0)...............................................0%|....1|.....1|.....0|
| #40|            java.lang.Byte......................................................0%|....1|.....1|.....0|
| #41|            java.lang.Byte......................................................0%|....1|.....1|.....0|
| #42|            java.lang.Byte......................................................0%|....1|.....1|.....0|
| #43|            java.lang.Integer(=0)...............................................0%|....1|.....1|.....0|
| #44|            java.lang.Integer(=0)...............................................0%|....1|.....1|.....0|
| #45|            java.lang.Integer(=1)...............................................0%|....1|.....1|.....0|
| #46|            java.lang.Integer(=1)...............................................0%|....1|.....1|.....0|
| #47|            java.lang.Integer(=1)...............................................0%|....1|.....1|.....0|
| #52|            java.lang.Boolean...................................................0%|....1|.....1|.....0|
| #53|            java.lang.Boolean...................................................0%|....1|.....1|.....0|
| #59|            java.lang.Integer(=4)...............................................0%|....1|.....1|.....0|
| #60|            java.lang.Integer(=1)...............................................0%|....1|.....1|.....0|
| #79|            java.lang.Boolean...................................................0%|....1|.....1|.....0|
| #12|          java.util.GregorianCalendar...........................................2%|...23|....23|.....0|
| #80|          java.lang.String("dd.M...")...........................................1%|...20|....20|.....0|
| #14|          java.util.Date........................................................0%|...10|....10|.....0|
| #20|          java.util.Locale......................................................0%|....1|.....1|.....0|
| #81|          java.lang.Integer(=1).................................................0%|....1|.....1|.....0|
| #83|        de.wicketpraxis.usecase.dateformat.DateModel............................0%|....1|.....1|.....0|
| #84|      java.lang.Integer(=1343434906)............................................0%|....5|.....5|.....0|
| #86|      java.lang.String("now")...................................................0%|....4|.....4|.....0|
| #85|      java.lang.Integer(=-1)....................................................0%|....1|.....1|.....0|
|  #0|      de.wicketpraxis.usecase.dateformat.DateFormatV1Page(0)....................0%|....1|.....1|.....0|
| #87|    org.apache.wicket.markup.html.basic.Label(now2)............................21%|..216|.....6|...210|
| #88|      de.wicketpraxis.usecase.dateformat.DateFormatV1Page$DateToStringModel....19%|..198|.....5|...193|
| #89|        java.text.SimpleDateFormat.............................................18%|..192|.....8|...184|
| #92|          java.text.DateFormatSymbols...........................................7%|...81|.....2|....79|
| #94|            [Ljava.lang.String;.................................................5%|...56|....56|.....0|
| #93|            [Ljava.lang.String;.................................................1%|...17|....17|.....0|
| #17|            [Ljava.lang.String;.................................................0%|....1|.....1|.....0|
| #18|            [Ljava.lang.String;.................................................0%|....1|.....1|.....0|
| #19|            java.lang.String("GuMt...").........................................0%|....1|.....1|.....0|
| #20|            java.util.Locale....................................................0%|....1|.....1|.....0|
| #25|            [Ljava.lang.String;.................................................0%|....1|.....1|.....0|
| #26|            [Ljava.lang.String;.................................................0%|....1|.....1|.....0|
| #95|          java.text.DecimalFormat...............................................6%|...67|.....3|....64|
|#117|            java.text.DecimalFormatSymbols......................................2%|...28|.....1|....27|
|#118|              java.lang.Character...............................................0%|....2|.....2|.....0|
|#119|              java.lang.Character...............................................0%|....2|.....2|.....0|
|#120|              java.lang.Character...............................................0%|....2|.....2|.....0|
|#121|              java.lang.Character...............................................0%|....2|.....2|.....0|
|#122|              java.lang.Character...............................................0%|....2|.....2|.....0|
|#123|              java.lang.Character...............................................0%|....2|.....2|.....0|
|#124|              java.lang.Character...............................................0%|....2|.....2|.....0|
|#125|              java.lang.Character...............................................0%|....2|.....2|.....0|
|#126|              java.lang.Character...............................................0%|....2|.....2|.....0|
|#128|              java.lang.Character...............................................0%|....2|.....2|.....0|
| #63|              java.lang.String("�").............................................0%|....1|.....1|.....0|
| #64|              java.lang.String("€").............................................0%|....1|.....1|.....0|
| #68|              java.lang.String("E").............................................0%|....1|.....1|.....0|
| #70|              java.lang.String("∞").............................................0%|....1|.....1|.....0|
| #71|              java.lang.String("EUR")...........................................0%|....1|.....1|.....0|
| #20|              java.util.Locale..................................................0%|....1|.....1|.....0|
|#127|              java.lang.Integer(=3).............................................0%|....1|.....1|.....0|
|#103|            java.lang.Integer(=MAX).............................................0%|....5|.....5|.....0|
|#104|            java.lang.Integer(=309).............................................0%|....2|.....2|.....0|
| #96|            java.lang.Boolean...................................................0%|....1|.....1|.....0|
| #97|            java.lang.Byte......................................................0%|....1|.....1|.....0|
| #98|            java.lang.Boolean...................................................0%|....1|.....1|.....0|
| #99|            java.lang.Byte......................................................0%|....1|.....1|.....0|
|#100|            java.lang.Byte......................................................0%|....1|.....1|.....0|
|#101|            java.lang.Integer(=0)...............................................0%|....1|.....1|.....0|
|#102|            java.lang.Integer(=0)...............................................0%|....1|.....1|.....0|
|#105|            java.lang.Byte......................................................0%|....1|.....1|.....0|
|#106|            java.lang.Byte......................................................0%|....1|.....1|.....0|
|#107|            java.lang.Byte......................................................0%|....1|.....1|.....0|
|#108|            java.lang.Integer(=0)...............................................0%|....1|.....1|.....0|
|#109|            java.lang.Integer(=0)...............................................0%|....1|.....1|.....0|
|#110|            java.lang.Integer(=1)...............................................0%|....1|.....1|.....0|
|#111|            java.lang.Integer(=1)...............................................0%|....1|.....1|.....0|
|#112|            java.lang.Integer(=1)...............................................0%|....1|.....1|.....0|
| #48|            java.lang.String("'-")..............................................0%|....1|.....1|.....0|
| #49|            java.lang.String("")................................................0%|....1|.....1|.....0|
| #50|            java.lang.String("-")...............................................0%|....1|.....1|.....0|
| #51|            java.lang.String("")................................................0%|....1|.....1|.....0|
|#113|            java.lang.Boolean...................................................0%|....1|.....1|.....0|
|#114|            java.lang.Boolean...................................................0%|....1|.....1|.....0|
| #54|            java.lang.String("")................................................0%|....1|.....1|.....0|
| #55|            java.lang.String("")................................................0%|....1|.....1|.....0|
| #56|            java.lang.String("")................................................0%|....1|.....1|.....0|
| #57|            java.lang.String("")................................................0%|....1|.....1|.....0|
| #58|            java.math.RoundingMode..............................................0%|....1|.....1|.....0|
|#115|            java.lang.Integer(=4)...............................................0%|....1|.....1|.....0|
|#116|            java.lang.Integer(=1)...............................................0%|....1|.....1|.....0|
|#129|            java.lang.Boolean...................................................0%|....1|.....1|.....0|
| #90|          java.util.GregorianCalendar...........................................2%|...23|....23|.....0|
| #91|          java.util.Date........................................................0%|...10|....10|.....0|
| #20|          java.util.Locale......................................................0%|....1|.....1|.....0|
| #80|          java.lang.String("dd.M...")...........................................0%|....1|.....1|.....0|
|#130|          java.lang.Integer(=1).................................................0%|....1|.....1|.....0|
|#131|        de.wicketpraxis.usecase.dateformat.DateModel............................0%|....1|.....1|.....0|
|#132|      java.lang.Integer(=1343434906)............................................0%|....5|.....5|.....0|
|#134|      java.lang.String("now2")..................................................0%|....5|.....5|.....0|
|#133|      java.lang.Integer(=-1)....................................................0%|....1|.....1|.....0|
|  #0|      de.wicketpraxis.usecase.dateformat.DateFormatV1Page(0)....................0%|....1|.....1|.....0|
|#135|  java.lang.Integer(=1342845082)................................................0%|....5|.....5|.....0|
|#139|  org.apache.wicket.request.mapper.parameter.PageParameters.....................0%|....3|.....3|.....0|
|  #2|  java.lang.Integer(=1).........................................................0%|....1|.....1|.....0|
|#136|  java.lang.Integer(=-1)........................................................0%|....1|.....1|.....0|
|#137|  java.lang.Integer(=0).........................................................0%|....1|.....1|.....0|
|#140|  java.lang.Integer(=1).........................................................0%|....1|.....1|.....0|
=============================================================================================================

DEBUG - TreeSizeReport             - 
=============================================================================================================
|   #|Type.............................................................................%|..sum|.local|.child|
-------------------------------------------------------------------------------------------------------------
|  #0|de.wicketpraxis.usecase.dateformat.DateFormatV1Page(0)........................100%|.1025|...137|...888|
|  #4|  [Ljava.lang.Object;..........................................................85%|..876|.....2|...874|
|  #5|    org.apache.wicket.markup.html.basic.Label(now).............................64%|..658|....77|...581|
|  #8|      de.wicketpraxis.usecase.dateformat.DateFormatV1Page$DateToStringModel....55%|..570|....75|...495|
| #10|        java.text.SimpleDateFormat.............................................48%|..494|....72|...422|
| #16|          java.text.DateFormatSymbols..........................................23%|..242|.....2|...240|
|null|            [Ljava.lang.String;................................................20%|..210|...210|.....0|
| #19|            java.lang.String("GuMt...").........................................1%|...20|....20|.....0|
| #20|            java.util.Locale....................................................0%|...10|.....1|.....9|
|null|              java.lang.String("DE"|"de"|"")....................................0%|....8|.....8|.....0|
| #22|              java.lang.Integer(=-1)............................................0%|....1|.....1|.....0|
| #30|          java.text.DecimalFormat..............................................12%|..125|....33|....92|
| #62|            java.text.DecimalFormatSymbols......................................4%|...45|.....1|....44|
|null|              java.lang.String("�"|"€"|"E"|"∞"|"EUR")...........................2%|...22|....22|.....0|
|null|              java.lang.Character...............................................1%|...20|....20|.....0|
| #20|              java.util.Locale..................................................0%|....1|.....1|.....0|
| #77|              java.lang.Integer(=3).............................................0%|....1|.....1|.....0|
|null|            java.lang.String("'-"|""|"-").......................................1%|...18|....18|.....0|
|null|            java.lang.Integer(=0|=MAX|=309|=1|=4)...............................1%|...16|....16|.....0|
|null|            java.lang.Byte......................................................0%|....6|.....6|.....0|
|null|            java.lang.Boolean...................................................0%|....5|.....5|.....0|
| #58|            java.math.RoundingMode..............................................0%|....2|.....2|.....0|
| #12|          java.util.GregorianCalendar...........................................2%|...23|....23|.....0|
| #80|          java.lang.String("dd.M...")...........................................1%|...20|....20|.....0|
| #14|          java.util.Date........................................................0%|...10|....10|.....0|
| #20|          java.util.Locale......................................................0%|....1|.....1|.....0|
| #81|          java.lang.Integer(=1).................................................0%|....1|.....1|.....0|
| #83|        de.wicketpraxis.usecase.dateformat.DateModel............................0%|....1|.....1|.....0|
|null|      java.lang.Integer(=1343434906|=-1)........................................0%|....6|.....6|.....0|
| #86|      java.lang.String("now")...................................................0%|....4|.....4|.....0|
|  #0|      de.wicketpraxis.usecase.dateformat.DateFormatV1Page(0)....................0%|....1|.....1|.....0|
| #87|    org.apache.wicket.markup.html.basic.Label(now2)............................21%|..216|.....6|...210|
| #88|      de.wicketpraxis.usecase.dateformat.DateFormatV1Page$DateToStringModel....19%|..198|.....5|...193|
| #89|        java.text.SimpleDateFormat.............................................18%|..192|.....8|...184|
| #92|          java.text.DateFormatSymbols...........................................7%|...81|.....2|....79|
|null|            [Ljava.lang.String;.................................................7%|...77|....77|.....0|
| #19|            java.lang.String("GuMt...").........................................0%|....1|.....1|.....0|
| #20|            java.util.Locale....................................................0%|....1|.....1|.....0|
| #95|          java.text.DecimalFormat...............................................6%|...67|.....3|....64|
|#117|            java.text.DecimalFormatSymbols......................................2%|...28|.....1|....27|
|null|              java.lang.Character...............................................1%|...20|....20|.....0|
|null|              java.lang.String("�"|"€"|"E"|"∞"|"EUR")...........................0%|....5|.....5|.....0|
| #20|              java.util.Locale..................................................0%|....1|.....1|.....0|
|#127|              java.lang.Integer(=3).............................................0%|....1|.....1|.....0|
|null|            java.lang.Integer(=0|=MAX|=309|=1|=4)...............................1%|...16|....16|.....0|
|null|            java.lang.String("'-"|""|"-").......................................0%|....8|.....8|.....0|
|null|            java.lang.Byte......................................................0%|....6|.....6|.....0|
|null|            java.lang.Boolean...................................................0%|....5|.....5|.....0|
| #58|            java.math.RoundingMode..............................................0%|....1|.....1|.....0|
| #90|          java.util.GregorianCalendar...........................................2%|...23|....23|.....0|
| #91|          java.util.Date........................................................0%|...10|....10|.....0|
| #20|          java.util.Locale......................................................0%|....1|.....1|.....0|
| #80|          java.lang.String("dd.M...")...........................................0%|....1|.....1|.....0|
|#130|          java.lang.Integer(=1).................................................0%|....1|.....1|.....0|
|#131|        de.wicketpraxis.usecase.dateformat.DateModel............................0%|....1|.....1|.....0|
|null|      java.lang.Integer(=1343434906|=-1)........................................0%|....6|.....6|.....0|
|#134|      java.lang.String("now2")..................................................0%|....5|.....5|.....0|
|  #0|      de.wicketpraxis.usecase.dateformat.DateFormatV1Page(0)....................0%|....1|.....1|.....0|
|null|  java.lang.Integer(=1|=1342845082|=-1|=0)......................................0%|....9|.....9|.....0|
|#139|  org.apache.wicket.request.mapper.parameter.PageParameters.....................0%|....3|.....3|.....0|
=============================================================================================================

Tabelle 1

Ich versuche mal, die obere Ausgabe etwas zu erklären. In der ersten Tabelle, die wir im Log finden, wird versucht, die Menge der geschriebenen Daten einem Typ zuzuordnen. Dabei kann es zu Ungenauigkeiten kommen, wenn Typen sich selbst enthalten können ( ArrayList von Objekten, die wieder eine ArrayList benutzen). Die Typbezeichnung ergibt sich meist aus getClass().toString() (für anonyme Klassen wird die Elternklasse ausgewählt). Was in dieser Tabelle schon auffällt: da werden viele Strings geschrieben.

Tabelle 2

In der zweiten Tabelle befinden sich 6 Spalten (5 und eine Doppelbelegung). Die erste Spalte (#) gibt eine ObjektID an. Wird die selbe ID in mehr als einer Zeile verwendet, dann wird an der zweiten Stelle ebenfalls eine Referenz auf das Objekt gehalten und natürlich mit serialisiert. Ein Objekt wird immer beim ersten Treffer vollständig serialisiert. Das bedeutet, wenn 3 Objekte eine Referenz auf A haben, bekommt das erste Objekt den größten Zuschlag. Das kann man sicher irgendwie korrigieren, aber meist reicht dieses Wissen aus, um trotzdem sehr viel (wenn auch erst mal etwas gefühlt unpräzise) Erkenntnisse zu gewinnen. Die zweite Spalte (Type) gibt wie in der ersten Tabelle den Typ an. Da hier immer die selben Rohdaten benutzt werden (die zweite Tabelle ist eigentlich die ungefilterte Datenquelle, die anderen Tabellen sind aggregierte Daten davon), gelten die selben Regeln. Hinter dem Typ wird in Klammern für Komponenten die WicketID und für einfache Datentypen der Wert angezeigt (bei Integer ist immer ein = davor). Dann gibt es die dritte Spalte (%, die Doppelbelegung). Dort wird anzeigt welchen Platzanteil das Objekt inklusive aller Unterobjekte einnimmt. Das soll helfen, die großen Fische zu finden. Die drei letzten Spalten geben von hinten nach vorne die Größe aller Kinder (child), den Datenumfang des Objektes selbst (local) und die Summe beider Zahlen (sum) an.

Was an der zweiten Tabelle auffällt ist die große Anzahl an Objekten unterhalb von java.text.SimpleDateFormat . Wenn man sich dann die Gesamtgröße anschaut, werden für dieses Beispiel rund 1kb an Daten serialisiert. Das hört sich nicht viel an, verändert sich aber dramatisch, wenn man sich das ganze in einer langen Tabelle vorstellt. Die erste Instanz kommt auf 48% und die zweite auf 18% des Platzbedarfs der Seite. Das ist eigentlich recht viel. Wenn man mal in die Untiefen der Klassen hinabsteigt, stößt man in dem Fall auf die Klasse java.text.DateFormatSymbols . Diese Klasse entfaltet recht interessant viele String-Arrays im Speicher, die dann auch mit der Seite serialisiert wird. In Anbetracht der Eingangsdaten darf man durchaus über den Umfang überrascht sein.

Tabelle 3

Die dritte Tabelle versucht folgendes: Wenn in der Baumstruktur gleiche Äste vorhanden sind, dann werden diese unter Berücksichtigung der Größen zusammengelegt. Das kann man an zwei Dingen gut erkennen: Bei einfach Datentypen und Wicket-Komponenten erscheinen mehrere Werte durch | getrennt innerhalb der Klammern und die ObjektID ist null. Dadurch wird der Baum kompakter und kann gerade bei ListView -Konstrukten sehr viel lesbarer werden.

Es scheint keine gute Idee zu sein, eine Instanz der Klasse SimpleDateFormat mit der Seite mitzuserialisieren. Doch welche Alternativen haben wir?

Alternative 1

In Wicket kann man über Konverter dafür sorgen, dass eine automatische Umwandlung eines Typs in einen String oder aus einem String transparent für die restliche Anwendung durchgeführt wird. Eine einfache Methode ist das erstellen einer eigenen Label -Komponente, die einen anderen Konverter benutzt.

public class DateFormatV2Page extends WebPage
{
  public DateFormatV2Page()
  {
    add(new DateLabel("now", new DateModel()));
    add(new DateLabel("now2", new DateModel()));

    setStatelessHint(false);
  }

  static class DateLabel extends Label
  {
    public DateLabel(String id, IModel<Date> model)
    {
      super(id, model);
    }

    @Override
    public <T> IConverter<T> getConverter(Class<T> type)
    {
      return (IConverter<T>)new DateConverter()
      {
        @Override
        public DateFormat getDateFormat(Locale locale)
        {
          return new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
        }
      };
    }
  }
}

Das Markup bleibt unverändert. Wenn diese Seite nun serialisiert wird, ergibt sich folgendes Bild:

DEBUG - KryoSerializer             - Going to serialize: '[Page class = de.wicketpraxis.usecase.dateformat.DateFormatV2Page, id = 2, render count = 1]'
DEBUG - TypeSizeReport             - 
=======================================================================
|Type............................................................bytes|
-----------------------------------------------------------------------
|de.wicketpraxis.usecase.dateformat.DateFormatV2Page...............139|
|de.wicketpraxis.usecase.dateformat.DateFormatV2Page$DateLabel.....119|
|java.lang.Integer..................................................21|
|java.lang.String....................................................9|
|org.apache.wicket.request.mapper.parameter.PageParameters...........3|
|[Ljava.lang.Object;.................................................2|
|de.wicketpraxis.usecase.dateformat.DateModel........................2|
=======================================================================

DEBUG - TreeSizeReport             - 
=======================================================================================================
|  #|Type.........................................................................%|.sum|.local|.child|
-------------------------------------------------------------------------------------------------------
| #0|de.wicketpraxis.usecase.dateformat.DateFormatV2Page(2)....................100%|.295|...137|...158|
| #4|  [Ljava.lang.Object;......................................................49%|.146|.....2|...144|
| #5|    de.wicketpraxis.usecase.dateformat.DateFormatV2Page$DateLabel(now).....42%|.124|...112|....12|
| #9|      java.lang.Integer(=1343434906)........................................1%|...5|.....5|.....0|
|#11|      java.lang.String("now")...............................................1%|...4|.....4|.....0|
| #8|      de.wicketpraxis.usecase.dateformat.DateModel..........................0%|...1|.....1|.....0|
|#10|      java.lang.Integer(=-1)................................................0%|...1|.....1|.....0|
| #0|      de.wicketpraxis.usecase.dateformat.DateFormatV2Page(2)................0%|...1|.....1|.....0|
|#12|    de.wicketpraxis.usecase.dateformat.DateFormatV2Page$DateLabel(now2).....6%|..20|.....7|....13|
|#14|      java.lang.Integer(=1343434906)........................................1%|...5|.....5|.....0|
|#16|      java.lang.String("now2")..............................................1%|...5|.....5|.....0|
|#13|      de.wicketpraxis.usecase.dateformat.DateModel..........................0%|...1|.....1|.....0|
|#15|      java.lang.Integer(=-1)................................................0%|...1|.....1|.....0|
| #0|      de.wicketpraxis.usecase.dateformat.DateFormatV2Page(2)................0%|...1|.....1|.....0|
|#17|  java.lang.Integer(=1342845082)............................................1%|...5|.....5|.....0|
|#21|  org.apache.wicket.request.mapper.parameter.PageParameters.................1%|...3|.....3|.....0|
| #2|  java.lang.Integer(=1).....................................................0%|...1|.....1|.....0|
|#18|  java.lang.Integer(=-1)....................................................0%|...1|.....1|.....0|
|#19|  java.lang.Integer(=2).....................................................0%|...1|.....1|.....0|
|#22|  java.lang.Integer(=1).....................................................0%|...1|.....1|.....0|
=======================================================================================================

DEBUG - TreeSizeReport             - 
============================================================================================================
|   #|Type.............................................................................%|.sum|.local|.child|
------------------------------------------------------------------------------------------------------------
|  #0|de.wicketpraxis.usecase.dateformat.DateFormatV2Page(2)........................100%|.295|...137|...158|
|  #4|  [Ljava.lang.Object;..........................................................49%|.146|.....2|...144|
|null|    de.wicketpraxis.usecase.dateformat.DateFormatV2Page$DateLabel(now|now2)....48%|.144|...119|....25|
|null|      java.lang.Integer(=1343434906|=-1)........................................4%|..12|....12|.....0|
|null|      java.lang.String("now"|"now2")............................................3%|...9|.....9|.....0|
|null|      de.wicketpraxis.usecase.dateformat.DateModel..............................0%|...2|.....2|.....0|
|null|      de.wicketpraxis.usecase.dateformat.DateFormatV2Page(2)....................0%|...2|.....2|.....0|
|null|  java.lang.Integer(=1|=1342845082|=-1|=2)......................................3%|...9|.....9|.....0|
| #21|  org.apache.wicket.request.mapper.parameter.PageParameters.....................1%|...3|.....3|.....0|
============================================================================================================

Der größte Unterschied: viel weniger Objekte, nur noch ein drittel der Größe der vorherigen Seite. Der meiste Platz wird von der Seite und der Komponente beansprucht. Das auffällige [Ljava.lang.Object; ist einfach erklärt: Wicket verwaltet die Kindkomponenten und andere Dinge in einem ObjectArray und nicht in einer Liste. In dem Beispiel kann man auch gut erkennen, dass die Baumstruktur der Serialisierung vom der ersten und zweiten Label-Komponente gleich sind und diese für die dritte Tabelle zusammengefasst wurden.

Alternative 2

Wenn wir schon auf Wicket-Konverter zurückgreifen, kann man das ganze noch ein wenig weiter treiben. Dazu müssen wir allerding etwas mehr Anpassungen vornehmen.

In unserer WebApplication-Klasse müssen wir folgende Methode implementieren:

@Override
protected IConverterLocator newConverterLocator()
{
  ConverterLocator ret = new ConverterLocator();
  ret.set(SmallDate.class, new DateContainerConverter<SmallDate>(SmallDate.class, "dd.MM.yyyy"));
  ret.set(FullDate.class, new DateContainerConverter<FullDate>(FullDate.class, "dd.MM.yyyy HH:mm:ss"));
  return ret;
}

Jetzt kommen noch ein paar Klassen, die notwendig sind, damit das ganze funktioniert. Die Idee dahinter ist folgende: Wicket sucht für eine Klasse den passenden Konverter über den ConverterLocator. Als Schlüssel dient der Typ. Wir benutzen einen Container-Typ von dem wir verschiedene Ableitungen erstellen (SmallDate, FullDate) und für diese Typen jeweils eigene Konverter registrieren. Doch hier erst einmal der Code:

public abstract class AbstractDateContainer
{
  private final Date value;

  public AbstractDateContainer(Date value)
  {
    this.value = value;
  }

  public Date getValue()
  {
    return value;
  }
}
public class FullDate extends AbstractDateContainer
{
  public FullDate(Date value)
  {
    super(value);
  }
}

public class SmallDate extends AbstractDateContainer
{
  public SmallDate(Date value)
  {
    super(value);
  }
}

Man sieht, am Date-Objekt, werden keine Anpassungen durch den Container vorgenommen.

public class DateContainerConverter<T extends AbstractDateContainer> extends AbstractConverter<T>
{
  private final Class<T> containerType;
  private final String pattern;
  private Constructor<T> constructor;

  public DateContainerConverter(Class<T> containerType,String pattern)
  {
    this.containerType = containerType;
    this.pattern = pattern;
    try
    {
      constructor = containerType.getConstructor(Date.class);
    }
    catch (SecurityException e)
    {
      throw new WicketRuntimeException(e);
    }
    catch (NoSuchMethodException e)
    {
      throw new WicketRuntimeException(e);
    }
  }

  

  
  /**
   * @see org.apache.wicket.util.convert.IConverter#convertToObject(java.lang.String,Locale)
   */
  @Override
  public T convertToObject(final String value, final Locale locale)
  {
    if ((value == null) || Strings.isEmpty(value))
    {
      return null;
    }
    else
    {
      try
      {
        return constructor.newInstance(parse(getDateFormat(locale), value, locale));
      }
      catch (IllegalArgumentException e)
      {
        throw new WicketRuntimeException(e);
      }
      catch (InstantiationException e)
      {
        throw new WicketRuntimeException(e);
      }
      catch (IllegalAccessException e)
      {
        throw new WicketRuntimeException(e);
      }
      catch (InvocationTargetException e)
      {
        throw new WicketRuntimeException(e);
      }
    }
  }

  /**
   * @see org.apache.wicket.util.convert.IConverter#convertToString(Object, java.util.Locale)
   */
  @Override
  public String convertToString(final T value, final Locale locale)
  {
    final Format dateFormat = getDateFormat(locale);
    if (dateFormat != null)
    {
      return dateFormat.format(value.getValue());
    }
    return value.toString();
  }

  private Format getDateFormat(Locale locale)
  {
    if (locale == null)
    {
      locale = Locale.getDefault();
    }
    return new SimpleDateFormat(this.pattern, locale);
  }

  @Override
  protected Class<T> getTargetType()
  {
    return containerType;
  }
}

Der Konverter ist so implementiert, dass er beide Konvertierungsrichtungen unterstützt. Wir benötigen jetzt für unser Beispiel allerdings noch ein Model, dass diese beiden Typen als Wert liefert.

public class DateContainerModel extends LoadableDetachableModel<AbstractDateContainer>
{
  private final boolean full;

  public DateContainerModel(boolean full)
  {
    this.full = full;
  }

  @Override
  protected AbstractDateContainer load()
  {
    return full ? new FullDate(new Date()) : new SmallDate(new Date());
  }
}

Und so wird das ganze benutzt:

public class DateFormatV3Page extends WebPage
{
  public DateFormatV3Page()
  {
    add(new Label("now", new DateContainerModel(false)));
    add(new Label("now2", new DateContainerModel(true)));

    setStatelessHint(false);
  }
}

Während bei den anderen Beispielen immer die gleiche Darstellung gewählt wurde, können wir bei diesem Beispiel an der Darstellung erkennen, ob es funktioniert hat:

Date Format

Now: 29.11.2012

Now: 29.11.2012 23:42:15

Auch gespannt, welche Auswirkung das auf die Serialisierung gehabt hat?

DEBUG - KryoSerializer             - Going to serialize: '[Page class = de.wicketpraxis.usecase.dateformat.DateFormatV3Page, id = 4, render count = 1]'
DEBUG - TypeSizeReport             - 
===================================================================
|Type........................................................bytes|
-------------------------------------------------------------------
|de.wicketpraxis.usecase.dateformat.DateFormatV3Page...........139|
|org.apache.wicket.markup.html.basic.Label......................65|
|java.lang.Integer..............................................21|
|java.lang.String................................................9|
|org.apache.wicket.request.mapper.parameter.PageParameters.......3|
|java.lang.Boolean...............................................2|
|[Ljava.lang.Object;.............................................2|
|de.wicketpraxis.usecase.dateformat.DateContainerModel...........2|
===================================================================

DEBUG - TreeSizeReport             - 
============================================================================================
|  #|Type..............................................................%|.sum|.local|.child|
--------------------------------------------------------------------------------------------
| #0|de.wicketpraxis.usecase.dateformat.DateFormatV3Page(4).........100%|.243|...137|...106|
| #4|  [Ljava.lang.Object;...........................................38%|..94|.....2|....92|
| #5|    org.apache.wicket.markup.html.basic.Label(now)..............29%|..72|....59|....13|
|#10|      java.lang.Integer(=1343434906).............................2%|...5|.....5|.....0|
|#12|      java.lang.String("now")....................................1%|...4|.....4|.....0|
| #8|      de.wicketpraxis.usecase.dateformat.DateContainerModel......0%|...2|.....1|.....1|
| #9|        java.lang.Boolean........................................0%|...1|.....1|.....0|
|#11|      java.lang.Integer(=-1).....................................0%|...1|.....1|.....0|
| #0|      de.wicketpraxis.usecase.dateformat.DateFormatV3Page(4).....0%|...1|.....1|.....0|
|#13|    org.apache.wicket.markup.html.basic.Label(now2)..............8%|..20|.....6|....14|
|#16|      java.lang.Integer(=1343434906).............................2%|...5|.....5|.....0|
|#18|      java.lang.String("now2")...................................2%|...5|.....5|.....0|
|#14|      de.wicketpraxis.usecase.dateformat.DateContainerModel......0%|...2|.....1|.....1|
|#15|        java.lang.Boolean........................................0%|...1|.....1|.....0|
|#17|      java.lang.Integer(=-1).....................................0%|...1|.....1|.....0|
| #0|      de.wicketpraxis.usecase.dateformat.DateFormatV3Page(4).....0%|...1|.....1|.....0|
|#19|  java.lang.Integer(=1342845082).................................2%|...5|.....5|.....0|
|#23|  org.apache.wicket.request.mapper.parameter.PageParameters......1%|...3|.....3|.....0|
| #2|  java.lang.Integer(=1)..........................................0%|...1|.....1|.....0|
|#20|  java.lang.Integer(=-1).........................................0%|...1|.....1|.....0|
|#21|  java.lang.Integer(=4)..........................................0%|...1|.....1|.....0|
|#24|  java.lang.Integer(=1)..........................................0%|...1|.....1|.....0|
============================================================================================

DEBUG - TreeSizeReport             - 
=============================================================================================
|   #|Type..............................................................%|.sum|.local|.child|
---------------------------------------------------------------------------------------------
|  #0|de.wicketpraxis.usecase.dateformat.DateFormatV3Page(4).........100%|.243|...137|...106|
|  #4|  [Ljava.lang.Object;...........................................38%|..94|.....2|....92|
|null|    org.apache.wicket.markup.html.basic.Label(now|now2).........37%|..92|....65|....27|
|null|      java.lang.Integer(=1343434906|=-1).........................4%|..12|....12|.....0|
|null|      java.lang.String("now"|"now2").............................3%|...9|.....9|.....0|
|null|      de.wicketpraxis.usecase.dateformat.DateContainerModel......1%|...4|.....2|.....2|
|null|        java.lang.Boolean........................................0%|...2|.....2|.....0|
|null|      de.wicketpraxis.usecase.dateformat.DateFormatV3Page(4).....0%|...2|.....2|.....0|
|null|  java.lang.Integer(=1|=1342845082|=-1|=4).......................3%|...9|.....9|.....0|
| #23|  org.apache.wicket.request.mapper.parameter.PageParameters......1%|...3|.....3|.....0|
=============================================================================================

Auch wenn wir wieder etwas Platz gespart haben, ist die wichtigste Botschaft hier eine etwas andere. Wir haben den Code für die Darstellung aus der Seite herausgelöst. Dieser Code wird nicht mehr mit serialisiert. Und überall, wo man den passenden Typ benutzt, ist sichergestellt, dass die Darstellung Anwendungsweit die gleiche sein wird (sofern man nicht wieder etwas dagegen unternimmt). Außerdem kann man nun andere Optimierungen vornehmen. z.B. kann man überlegen, ob man beim Erzeugen der Formater einen Instanzpool benutzt.

Zusammenfassung

Ich hoffe, ich konnte ein wenig zeigen, welche Dinge man mit dem Tool sichtbar machen kann. Außerdem wurde hoffentlich deutlich, dass der Teufel im Detail steckt und er sich auch gern sehr gut versteckt. Wer hätte gedacht, dass die Klasse DateFormatSymbols so speicherhungrig ist. In den noch folgenden Beiträgen versuche ich wieder, die eine oder andere Überraschung ins Licht zu ziehen. Ich würde mich auch sehr über eure Erfahrungen und Anregungen freuen. Vielleicht ist ja jemand dabei, der damit ganz andere verrückte Dinge macht. Ich bin gespannt.

Tags:

Veröffentlicht in Refactoring, Technologie, Wicket, .

Wicket Page Serializer und Performanceoptimierung - Teil 1

Wicket bildet den Zustand einer Anwendung, einer Webseite nicht wie andere Frameworks über Parameter in der Url ab. Wicket-Komponenten können viel mehr ihren eigenen Zustand über normale Java-Sprachmittel abbilden. Eine Komponente kann auf diese Weise die komplexesten Informationen halten, die möglicherweise sehr schwer über Url-Parameter abgebildet werden könnten. Das hat Vorteile, aber eben auch Nachteile. Der vermutlich größte Nachteil ist der Speicherbedarf, denn die Komponenten müssen für jeden Nutzer getrennt erzeugt und verwaltet werden. Anwendungen, die den Zustand einer Nutzerinteraktion immer über Url-Parameter abbilden, können nach dem ausliefern der Seite alle temporär erzeugten Daten verwerfen.

Wicket arbeitet anders. In Wicket ist eine Seite eine Sammlung verschiedenster Komponenten und Zustände in beliebigen Objekten. Wenn der Nutzer eine Aktion auf der Seite auslöst, wird die passende Komponente in diesem Komponentenbaum gesucht und die entsprechende Methode aufgerufen. Jede Interaktion erzeugt einen neuen Zustand und Wicket erzeugt eine neue Version der Seite für den Fall, dass der Nutzer z.B. mit dem Browser-Back-Buttom auf eine ältere Seite mit einem anderen Zustand zurück navigiert. Da der Arbeitsspeicher meist ein knappes Gut ist, werden die älteren Versionen einer Seite als Objektgraph mit all den Zuständen serialisiert und auf einem Datenträger abgelegt. Und hier wird es nun spannend.

Je mehr Zustand die Komponenten halten, je mehr Daten dafür notwendig sind, desto größer ist natürlich der Speicherbedarf zu Laufzeit aber auch der Platzbedarf auf dem Datenträger. Um nicht unnötig den Speicher und die Festplatte mit temporären Daten zu belasten, wird nach dem Ausliefern der Ergebnisseite für alle Komponenten die detach()-Methode aufgerufen, die sich darum kümmern muss, alles was für den Zustand der Komponente irrelevant ist, aufzuräumen.

Überraschung vorraus

Nun hat man seine Wicket-Komponenten geschrieben und vermutet, dass man alles richtig gemacht hat oder hat einfach akzeptiert, dass z.B. der Speicherbedarf so hoch sein muss wie er eben gerade ist. Wenn man genügend Speicher hat, muss man ja nichts verändern, aber das ist vermutlich selten der Fall. Zum Glück bietet die Vorgehensweise, die Wicket an den Tag legt sehr interessante Möglichkeiten, um verschiedeneste Optimierungen vorzunehmen.

Wenn die Menge der Daten, die auf Festplatte geschrieben werden sehr hoch ist und die die CPU's im Rechner eher langweilen, kann man recht günstig Plattenplatz und Rechnengeschwindigkeit eintauschen (typische Kompression 70%, Mehrbelastung der CPU vermutlich 1-5%). Dafür benutzt man statt dem Standard-Serializer einfach die DeflatedJavaSerializer-Implementierung. Vorsicht: die Daten, die mit dem Standard-Serializer auf die Festplatte geschrieben wurden, können nicht zurückgelesen werden.

Besser wäre es natürlich, gleich von Anfang an weniger Temporärdaten zu erzeugen. Doch was genau welchen Platz im Speicher und später auf der Festplatte belegt, ist meist nicht ganz so einfach zu ermitteln.

Der Anfang

Zu erst habe ich versucht, die Funktion, die bei Wicket für die aufschlußreiche Darstellung sorgt, welche Komponente nicht serialisierbare Objekte enthält entsprechend zu erweitern. Allerdings steigt man da in eine der eher nicht so gut gelüfteten Kammern von Java hinunter und kann sich sicher leicht verirren, so das eine andere Alternative gefunden werden musste. Zum Glück hatten dieses Gefühl wohl auch andere und haben das Projekt Kryo gestartet (http://code.google.com/p/kryo/). Kryo ist eine Alternative zur Serialisierung von Javaobjekten, die sehr konfigurierbar und trotz Reflection auch schneller und kompakter serialisieren soll.

Dann gab es recht schnell bei wicketstuff ein Projekt, dass Kryo in Wicket integriert hat. Allerdings bisher nur für die Version 1.x. Ich habe das dann für Kryo2 nachgeholt und entsprechend modularisiert, so dass man in den Serialisierungsprozess eingreifen kann und auf diese Weise verschiedene Informationen ermitteln kann. Doch dazu später mehr.

Setup

Zuerst muss man sein Wicket-Projekt um eine Dependency erweitern:

<dependency>
  <groupId>org.wicketstuff</groupId>
  <artifactId>wicketstuff-serializer-kryo2</artifactId>
  <version>${wicket.version}</version>
</dependency>

Dabei gibt es sowohl für Wicket 1.5 (ab Version 1.5.9) als auch für Wicket 6 (ab Version 6.2.1) eine Version in Maven Central.

Dann ergänzen wir unsere WebApplication-Klasse in der init()-Methode um folgende Codezeilen:

// output of report of type sizes, sorted tree report (by size), aggregated tree 
ISerializedObjectTreeProcessor typeAndSortedTreeAndCollapsedSortedTreeProcessors = TreeProcessors.listOf(
new TypeSizeReport(), new SortedTreeSizeReport(), new SimilarNodeTreeTransformator(
new SortedTreeSizeReport()));

// strips class object writes from tree
TreeTransformator treeProcessors = new TreeTransformator(
typeAndSortedTreeAndCollapsedSortedTreeProcessors,
TreeTransformator.strip(new TypeFilter(Class.class)));

// serialization listener notified on every written object
ISerializationListener serializationListener = SerializationListeners.listOf(
new DefaultJavaSerializationValidator(), new AnalyzingSerializationListener(
new NativeTypesAsLabel(new ComponentIdAsLabel()), treeProcessors));

// customized serializer
InspectingKryoSerializer serializer = new InspectingKryoSerializer(Bytes.megabytes(30L),
serializationListener);

// set custom serializer as default
getFrameworkSettings().setSerializer(serializer);

getStoreSettings().setAsynchronous(false);
getStoreSettings().setInmemoryCacheSize(0);
getPageSettings().setVersionPagesByDefault(true);

Ab sofort wird für jede Seite, die serialisiert wird, eine detailierte Übersicht über die verwendeten Komponenten und den Speicherbedarf ausgegeben.

Beispielausgabe

Wenn nun eine Seite von Wicket serialisiert wird, dann wird während des Schreibens des Objektgraphen die Anzahl der geschriebenen Bytes ermittelt und den passenden Objekten zugeteilt. Dabei gibt es gewisse Unschärfen. Wenn ein Objekt referenziert wird, wird nur das erste mal das Objekt selbst geschrieben. Jedes weitere mal wird nur eine Referenz auf das Objekt serialisiert, was natürlich viel weniger Platz in Anspruch nimmt. Insofern ist die Auswertung immer dann mit Vorsicht zu genießen, wenn ein Objekt an verschiedenen Stellen referenziert wird und nur einer den "Zuschlag" erhält.

Die Ausgabe und der ganze Serialisierungsprozess ist hochgradig anpassbar. In der Beispielkonfiguration entsteht für eine Beispielseite folgendes Ergebnis:

DEBUG - KryoSerializer             - Going to serialize: '[Page class = de.wicketpraxis.usecase.models.ModelsV1Page, id = 4, render count = 1]'
DEBUG - TypeSizeReport             - 
===================================================================
|Type........................................................bytes|
-------------------------------------------------------------------
|de.wicketpraxis.usecase.models.ModelsV1Page...................133|
|org.apache.wicket.markup.html.basic.Label.....................102|
|java.lang.Integer..............................................21|
|java.lang.String................................................9|
|de.wicketpraxis.usecase.models.ModelsV1Page$1...................3|
|de.wicketpraxis.usecase.models.ModelsV1Page$2...................3|
|org.apache.wicket.request.mapper.parameter.PageParameters.......3|
|[Ljava.lang.Object;.............................................2|
===================================================================

DEBUG - TreeSizeReport             - 
===========================================================================================
|  #|Type.............................................................%|.sum|.local|.child|
-------------------------------------------------------------------------------------------
| #0|de.wicketpraxis.usecase.models.ModelsV1Page(4)................100%|.276|...129|...147|
| #4|  [Ljava.lang.Object;..........................................48%|.135|.....2|...133|
|#12|    org.apache.wicket.markup.html.basic.Label(now2)............24%|..67|....51|....16|
|#15|      java.lang.Integer(=1343434906)............................1%|...5|.....5|.....0|
|#17|      java.lang.String("now2")..................................1%|...5|.....5|.....0|
|#14|      org.apache.wicket.model.LoadableDetachableModel...........1%|...4|.....3|.....1|
| #0|        de.wicketpraxis.usecase.models.ModelsV1Page(4)..........0%|...1|.....1|.....0|
|#16|      java.lang.Integer(=-1)....................................0%|...1|.....1|.....0|
| #0|      de.wicketpraxis.usecase.models.ModelsV1Page(4)............0%|...1|.....1|.....0|
| #5|    org.apache.wicket.markup.html.basic.Label(now).............23%|..66|....51|....15|
| #9|      java.lang.Integer(=1343434906)............................1%|...5|.....5|.....0|
| #8|      org.apache.wicket.model.LoadableDetachableModel...........1%|...4|.....3|.....1|
| #0|        de.wicketpraxis.usecase.models.ModelsV1Page(4)..........0%|...1|.....1|.....0|
|#11|      java.lang.String("now")...................................1%|...4|.....4|.....0|
|#10|      java.lang.Integer(=-1)....................................0%|...1|.....1|.....0|
| #0|      de.wicketpraxis.usecase.models.ModelsV1Page(4)............0%|...1|.....1|.....0|
|#18|  java.lang.Integer(=1342845082)................................1%|...5|.....5|.....0|
|#22|  org.apache.wicket.request.mapper.parameter.PageParameters.....1%|...3|.....3|.....0|
| #2|  java.lang.Integer(=1).........................................0%|...1|.....1|.....0|
|#19|  java.lang.Integer(=-1)........................................0%|...1|.....1|.....0|
|#20|  java.lang.Integer(=4).........................................0%|...1|.....1|.....0|
|#23|  java.lang.Integer(=1).........................................0%|...1|.....1|.....0|
===========================================================================================

DEBUG - TreeSizeReport             - 
============================================================================================
|   #|Type.............................................................%|.sum|.local|.child|
--------------------------------------------------------------------------------------------
|  #0|de.wicketpraxis.usecase.models.ModelsV1Page(4)................100%|.276|...129|...147|
|  #4|  [Ljava.lang.Object;..........................................48%|.135|.....2|...133|
| #12|    org.apache.wicket.markup.html.basic.Label(now2)............24%|..67|....51|....16|
|null|      java.lang.Integer(=1343434906|=-1)........................2%|...6|.....6|.....0|
| #17|      java.lang.String("now2")..................................1%|...5|.....5|.....0|
| #14|      org.apache.wicket.model.LoadableDetachableModel...........1%|...4|.....3|.....1|
|  #0|        de.wicketpraxis.usecase.models.ModelsV1Page(4)..........0%|...1|.....1|.....0|
|  #0|      de.wicketpraxis.usecase.models.ModelsV1Page(4)............0%|...1|.....1|.....0|
|  #5|    org.apache.wicket.markup.html.basic.Label(now).............23%|..66|....51|....15|
|null|      java.lang.Integer(=1343434906|=-1)........................2%|...6|.....6|.....0|
|  #8|      org.apache.wicket.model.LoadableDetachableModel...........1%|...4|.....3|.....1|
|  #0|        de.wicketpraxis.usecase.models.ModelsV1Page(4)..........0%|...1|.....1|.....0|
| #11|      java.lang.String("now")...................................1%|...4|.....4|.....0|
|  #0|      de.wicketpraxis.usecase.models.ModelsV1Page(4)............0%|...1|.....1|.....0|
|null|  java.lang.Integer(=1|=1342845082|=-1|=4)......................3%|...9|.....9|.....0|
| #22|  org.apache.wicket.request.mapper.parameter.PageParameters.....1%|...3|.....3|.....0|
============================================================================================

Die erste Tabelle enthält eine Aufschlüsselung nach Typen, die zweite Tabelle stellt den fast ungefilterten Komponentenbaum dar (die Klasse Component verwaltet Kindkomponenten und andere Dinge in einem Object-Array, weshalb im Baum [Ljava.lang.Object auftaucht). Die dritte Tabelle versucht gleiche Zweige zusammen zu fassen. Dabei werden die Größen aufadiert und die Labels (in Klammern dahinter) vereint. Die Angaben in den Klammern hinter den Klassennamen stellen ein Label für die Klasse dar. Bei Wicket-Komponenten ist das die Id, bei nativen Datentypen der Wert.

Und das nächste Mal sehen wir uns an, was für interessante Entdeckungen man damit machen kann. Doch zuvor ein Hinweis. Das sollten sie so nie in Produktivumgebungen einsetzen. Auch kann es bei bestehenden Anwendung zu sehr sehr umfangreichen Ausgaben kommen, so dass man in einer IDE die Logausgabe vielleicht besser in eine Datei umlenkt.

Viel Spass mit dem Experimentieren.

Den Code für die Beispielen findet man unter https://github.com/michaelmosmann/wicket-praxis/tree/master/quickstarts/de.wicketpraxis-quickstarts-15-kryo2 .

Tags:

Veröffentlicht in Maven, Refactoring, Wicket, .

Wicket Know-How für eigene Projekte

Dank der guten Erfahrungen die ich mit verschiedenen anderen Projekten und Github sammeln konnte, war es eigentlich ein erwartbarer Schritt, dass ich Code, den ich in meinen Beiträgen erläutert habe als in Projekten direkt nutzbare Bibliothek anbiete. Das hätte ich viel früher machen sollen. Aber hinterher ist man immer schlauer. Ich werde also nach und nach Wicket-Funktionalität im folgenden Github-Projekt einbringen:

https://github.com/flapdoodle-oss/de.flapdoodle.wicket

Wie man die Bibliothek (wovon es regelmäßig ein release in maven central geben wird) einbindet, steht auf der Readme-Seite. Viel Spass damit.

Veröffentlicht in Maven, Wicket, .

Wicket – Komponentenübersicht (6.0.0-beta3)

Eine erste Komponentenübersicht für Wicket 6 für die frisch erschienen Beta. Bisher sieht es nicht so aus, als ob sich da großartig was geändert hat, aber man könnte ja weitere Ansichten generieren. Wenn jemand dafür eine gute Idee hat...

Wicket Kompontenten -- Basis

Wicket Komponenten inklusive Extensions

Wicket Modelle

Wicket Behavior

Wicket Listener

Wicket RequestTarget

Veröffentlicht in Allgemein, .

Wicket – Komponentenübersicht (1.5.7)

Wicket ist schon eine Weile in der Version 1.5 unterwegs und steuert zügig auf ein neues Versionsschema zu (Wicket 6), da ist es sicher noch nicht zu spät, aktualisierte Kompontenen- und Klassenübersichten zu veröffentlichen.

Wicket Kompontenten -- Basis

Wicket Komponenten inklusive Extensions

Wicket Modelle

Wicket Behavior

Wicket Listener

Wicket RequestTarget

Veröffentlicht in Allgemein, Technologie, Wicket, .

Quelltexte zum Buch mit Wicket 1.5

Es ist etwas Aufwand, aber ich bin dabei, die Quelltexte zum Buch auf Wicket 1.5 (aktuell 1.5.7) zu migrieren. Natürlich macht Wicket 1.5 einige Dinge anders, so dass es nicht bei einer Funktionsnamensanpassung bleibt. Im Moment kann das Projekt gebaut werden und 90% des Codes funktioniert wie erwartet. Die restlichen Fehler werden in den nächsten Tagen beseitigt und dann kommt die eigentliche Fleißarbeit: die inhaltliche Migration auf Wicket 1.5. Das wird sicher zu dem einen oder anderen Artikel führen.

Wer sich beteiligen möchte, findet das Projekt unter https://github.com/michaelmosmann/wicket-praxis

Veröffentlicht in Maven, Refactoring, Wicket, .

Wicket best Practices - Leseempfehlung

Der Artikel ist irgendwie an mir vorbei gewandert, halte ihn aber für eine recht gute Zusammenfassung von Empfehlungen, die die Arbeit mit Wicket um einiges einfacher machen können.

Apache Wicket - best practices (german)

Veröffentlicht in Allgemein, .