Wicket Session Pitfalls
Environment: Wicket, Spring
Example Environment: Wicket, Spring, Hibernate, OpenSessionInView
When Wicket processes a request, the component hierarchy including the models is constructed, then events are processed, the response is rendered, and finally the components and models are put into the
PageMap
. This is Wicket's way of storing state. The PageMap
in turn is put into the Wicket session, which is put into a SessionStore
, which is put into the HttpSession
(by default). And as the HttpSession
is a critical resource, one has to take care of the size of the components. Especially, the size of the models, that are attached to the components, can be of critical size - imagine huge amounts of table data or large object graphs. For this reason, Wicket provides models, which get detached at the end of the request, so that their model objects will not be put into the session (this generally means nulling out transient fields). But there are still some cases, where model objects are accidently put into the session. This article sheds some light on these cases.The Example
As an example to demonstrate these cases, let's take an entity
Person
...
@Entity
public class Person {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String lastName;
private String firstName;
...
}
... and a very simple page that displays the details of a person:
public class PersonPage extends WebPage {
public PersonPage(final Person person) {
super(new Model(person));
add(new Label("firstName",
new PropertyModel(person, "firstName")));
add(new Label("lastName",
new PropertyModel(person, "lastName")));
add(new Label("username",
new PropertyModel(person, "username")));
Link link = new Link("editLink") {
public void onClick() {
setResponsePage(
new PersonEditPage(person));
}
};
add(link);
}
}
The person entity is provided as an argument to the constructor. We call
super()
with a new Model(person)
to associate the person model with the page, so we can get a handle to the person later on. Then we add some labels to display the details of a person. We provide PropertyModel
s to the labels, which are used to access a property of the person using a property expression. Finally, we add a Link
, which directs the user to the PersonEditPage
, where the user details can be edited. We do so by setting the response page to a new PersonEditPage(person)
. We pass the person instance, that was provided to the constructor of PersonPage
, to the edit page.So, this is a really, really simple page. And in fact, it has some flaws, so that the person entity is put into the session. This is something we wouldn't want, because the person may be connected to a larger object graph (e.g. person references some orders, which reference some line items, which references items, ...), which would all be put into the session as well.
The person entity in this example is put into the session for different reasons. The following sections describe these reasons and give some rules to prevent model objects from getting put into the session.
Rule #1: Use detachable models for entities
The first one is a very basic one. In the example above, the person is passed as an argument to the constructor and then it's put into a newly created
Model
instance. Now, Model
is the basic implementation of an IModel
(the base model interface in Wicket) and it does no more than keeping a reference to the entity, which is returned by getObject()
- no detaching at all. In fact, the constructor of the Model
class gives a hint that the model object is put into to the session: it just accepts model objects as an argument that implement Serializable
.Model
is not the only implementation of IModel
that does not detach entities. For example, PropertyModel
s and CompoundPropertyModel
s don't detach entites either. These models do some other kind of detaching, though, which is shown in the next section. But first, let's see, what we can do about the problem here. We are looking for an IModel
implementation that implements detach()
(inherited from IDetachable
) in a suitable way, i.e. detaching the entity from the model after the request. And in this case, we could use a LoadableDetachableModel
. LoadableDetachableModel
requires you to implement the abstract method load()
, which is meant to return the entity (e.g. fetch it from the database). When getObject()
of LoadableDetachableModel
is called, it calls load()
to fetch the entity and assigns it to a transient field. At the end of a request, detach()
is called, which nulls out this transient field, so the entity is not put into the session.Rule #2: Rely on models, not on entities
The second reason, for which the person entity gets into the session in the example above, is the way the
PropertyModels
for the Labels
are used. When new PropertyModel(person, "firstName")
is called, the PropertyModel
assigns the person instance to an internal, non-transient, never nulled-out instance variable. In this sense, PropertyModel
is no better than an ordinary Model
. But we can still leverage the benefits of a
PropertyModel
as it implements IChainingModel
in a proper way. That means, we can pass another person model to it as an argument instead of the entity itself, and it will chain through the calls to getObject()
, detach()
and so on. So, if we pass a LoadableDetachableModel
for the person to the PropertyModel
, we are fine: new PropertyModel(loadableDetachablePersonModel, "firstName")
. So, whenever it is possible, we should build models that rely on other models instead of the entities directly.Rule #3: Do not assign entities to instance variables
The example above does not violate this rule, so here is a variation of the
PersonPage
that does:
public class PersonPage extends WebPage {
private Person person;
public PersonPage(final Person person) {
this.person = person;
...
}
}
In this case, the person instance gets put into the session, simply because the page is put into the session, as all components are. It's easy to follow this rule. Just follow rule #1 and rule #2.
Rule #4: Do not circumvent @SpringBean proxies
This rule is kind of the same as rule #3 but related to
@SpringBean
annotated fields (when using Wicket in Spring container). When a field of a component is injected with the use of @SpringBean
, what really is assigned to the field is a proxy of that spring bean.
public class PersonPage extends WebPage {
@SpringBean
private PersonDao personDao;
public PersonPage(final Person person) {
...
}
}
As this is a lightweight proxy of the
PersonDao
, rule #3 is not violated. In fact, the @SpringBean
-proxy-mechanism is a good example of following rule #3. Anyway, while it's okay to keep a reference to this proxy, it's not okay, to pull other spring beans out of the proxy and assign them to instance variables. Example:
public class PersonPage extends WebPage {
@SpringBean
private PersonDao personDao;
public PersonPage(final Person person) {
super(new PersonModel(
personDao.getSessionFactory(),
person.getId());
...
}
}
public class PersonModel
extends LoadableDetachableModel {
private SessionFactory sessionFactory;
private Long personId;
public PersonModel(SessionFactory sessionFactory,
personId) {
this.sessionFactory = sessionFactory;
this.personId = personId;
}
public Object load() {
return sessionFactory.get(personId);
}
}
When
personDao.getSessionFactory()
is called, the real sessionFactory is returned, of coures. No proxy. Then it's passed to the PersonModel
which holds a reference to it, and the sessionFactory
will be put into the session along with this model. We can prevent this by injecting the session factory by using @SpringBean
as well.Rule #5: Implement anonymous inner classes carefully
The
PersonPage
in the example uses a Wicket Link
to redirect the user to the person edit page. The Link
is implemented as an anonymous inner class in order to implement its abstract method onClick()
. Many Wicket components are implemented this way, and as a result, anonymous inner classes are very common in Wicket.
public PersonPage(final Person person) {
super(new PersonModel(person));
...
Link link = new Link("editLink") {
public void onClick() {
setResponsePage(new PersonEditPage(person));
}
};
add(link);
}
The
onClick()
method is implemented in the most straightforward way. The person instance, which was passed as an argument to the constructor of PersonPage
, is passed to the constructor of PersonEditPage
, which is set as the response page. It's not easy to see immediately, how the person is getting into the session now, but in fact it's put there. And it can happen as easily in many other cases using anonymous inner classes.This is because in Java, when an inner class, declared in a method (or constructor), accesses a variable of its declaring method, such as an argument or a local variable, this variable effectively becomes a member of this inner class. This is because these variables have to be declared
final
to be able to access them from inner classes. In the example above, the person
parameter becomes an instance variable of the anonymous Link
class, and as rule #3 has shown, instance variables are put into the session along with the components they belong to.Rule #6: Always attach models to components
This rule also relates to rule #3. This time it's about models as instance variables. Let's add an instance variable of type
IModel
to the person edit page.
public class PersonPage extends WebPage {
private IModel accountModel;
public PersonPage(final Person person) {
super(new PersonModel(person);
this.accountModel = new AccountModel(
person.getAccount());
}
}
Let
PersonModel
and AccountModel
be properly implemented LoadableDetachableModel
s. So what's wrong with assigning a LoadableDetachableModel
to an instance variable? After all, it's a lightweight model and the PersonModel
is assigned to an instance variable somewhere in the page hierarchy, too. Actually, the
PersonModel
is assigned to a component, which takes care of detaching the model. A LoadableDetachableModel
has to be detached by some component that knows about this model. And this is not the case with the accountModel
. No component ever calls its detach()
method, as it's not attached to a component as a model. We could implement some detachment logic for this model in the person page, though.Conclusion
The conclusion of this article is simple: If you use objects of some serious weight in components, don't use them directly. Instead use an
IModel
implementation, that implements a proper detachment logic, or use some kind of other leightweight representation of that object like an ID or a @SpringBean
proxy.
7 comments:
That is a very concise and helpful summary of the models vs. entities topic in Wicket. I struggled with this in my first Wicket project and your article would have helped me getting a grasp of this quicker.
Can you please tell me how I can avoid the problems of rule #5? How should the anonymous implementation of the Link class access the person in your example?
Thanks a lot in advance.
@Marc Sorry, I didn't mention a solution to rule #5. You can get access to the person this way:
Person person = (Person) PersonPage.this.getModelObject();
This way it works fine.
Great summary Nick. I think that many Wicket users learn this the hard way, so it is excellent to have something like this put together.
One point though. In rule #5 you should follow rule #1 and use detachable model in super constructor invocation, as you did in #4 and #6 (assuming PersonModel is detachable model). In that case the only offending part left is the one in focus of rule #5, that is referring Person instance from inner class.
Thanks kirlich. Just fixed it.
Great article. I have a question on rule #5. If we did the below instead would this be ok and will PersonEditPage hold a reference to PersonPage? Basiclly is it ok to pass annoymous inner models from one component to the next?
Link link = new Link("editLink") {
public void onClick() {
setResponsePage(new PersonEditPage(personModel));
}
};
@tim: Yes that would be okay.
Post a Comment