Most web applications will need to have some data that is shared across multiple pages. Perhaps you are creating a multi-page wizard, or you have an object that tracks the user's identify once logged in, or maybe you need to manage a shopping cart.
Ordinary page-persistent fields won't work for this, since persistent fields are available only to a specific page, not shared across multiple pages.
Tapestry provides two mechanisms for storing such data: Session State Objects and Session Attributes. When deciding between the two, it's best to use Session State Objects for complex objects, and Session Attributes for simple types.
With a Session State Object (SSO), the value is automatically stored outside the page; with the default storage strategy, it is stored in the session. Such a value is global to all pages for the same user, but is stored separately for different users.
A field holding an SSO is marked with the @SessionState annotation.
Any other component or page that declares a field of the same type, regardless of name, and marks it with the SessionState annotation will share the same value. It's that simple. However, using @SessionState safely requires care:
|DO NOT USE @SessionState FOR SIMPLE TYPES! Only use it on variables that are of a custom-built class designed expressly for this purpose! See the Pitfalls section below.|
The first time you access an SSO, it is created automatically. Typically, the SSO will have a public no-args constructor ... but you may inject dependencies into the SSO via its constructor, as you can with a Tapestry IoC service implementation.
For Tapestry 4 Users: a big change here is that you don't need to provide any configuration for the SSO before using it, nor do you provide a logical name. Tapestry 5 uses the class name to identify the SSO, so there's no need for a logical name.
Assigning a value to an SSO field will store that value. Assigning null to an SSO field will remove the SSO (reading the field subsequently will force a new SSO instance to be created).
With @SessionState, you are creating a session-wide data storage area that is tied to the type (class) of the variable you annotate. It is not specifically tied to the variable itself, or even to the class in which that variable was annotated. As with all session data, there is the serious possibility of collisions, not just within your application but with other modules/libraries:
The simple rule is, NEVER use @SessionState for simple-type variables. It is ALWAYS worth taking the time to build a special class to hold your session state information. Doing so will force you to consolidate that information into a single, logical unit that can't be accidentally accessed by other classes. (Alternatively, see the Session Attribute section below.)
Scalable web applications do not create the server-side session needlessly. If you can avoid creating the session, especially on first access to your web application, you will be able to handle an order of magnitude more users. So, if you can avoid creating the SSO, you should do so.
But how to avoid creating it? Simply checking ("shoppingCart!= null") will force the creation of the SSO and the session to store it in.
Instead, create a second field with a matching name but with "Exists" appended:
It is not annotated; it is located by naming convention ("Exists" appended). It must be type boolean and must be a private instance variable. Tapestry will automatically set this variable to true when the SSO is created, so you can check it to see if the SSO already exists.
Alternately, you may allow for the state being null:
In this case, the shoppingCart field will be null if the ShoppingCart SSO does not exist, but will be non-null if it has been created (either by assigning a value to the field, or by a different SSO field where create is true).
Main Article: Persistent Page Data
Each SSO is managed according to a persistence strategy. The default persistence strategy, "session", stores the SSOs inside the session. The session is created as needed.
Generally, you will need to configure your Session State Object if you want to change the persistence strategy to other than the default. (Right now there's only one built in strategy, but more will be coming in the future.)
Alternately, you can configure a Session State Object in order to control how it is instantiated. You may need to inject some values into the SSO when it is first created, or otherwise initialize it. In this case, you may provide an ApplicationStateCreator object, which will be called upon to create the SSO as necessary. This is also the technique to use when you want your SSO to be represented by an interface rather than a class: you need to provide a creator that knows about the class that implements the interface.
A Session State Object is configured using contributions to the ApplicationStateManager service. From your application's module:
Here, we have an SSO type of ShoppingCart, and we're providing a creator for it. We've dolled the creator up with some generic types, but that isn't essential.
Our creator creates a new MyState instance using an alternate constructor that takes the current date and time. Again, just an example.
Finally, we create an ApplicationStateContribution identifying the strategy name and the creator, and give that to the configuration.
Note: You might be confused by the name "_Application_StateManager" and "_Application_StateCreator"; these reflect a difference in naming between 5.0 and 5.1; SSOs were originally called "Application State Objects", but that naming implied they were stored in the ServletContext, as application global to all users. The new SessionState annotation was introduced, but the existing services need to keep thier names as-is.
|Added in 5.2|
As an alternative to SSOs, Tapestry provides a Session Attribute mechanism, which lets you store data in the session by name (rather than type). It is particularly useful when integrating Tapestry with legacy applications that directly manipulate the HttpSession.
Starting with Tapestry 5.2, this can be accomplished just by annotating a page or component property with @SessionAttribute. This annotation is used to map a property of a page or component to value stored in session. Unlike Session State Objects, the name (not the type) of the annotated property is used as the name of the session attribute to look for.
You can also provide a name using the annotation's value parameter:
As with SSOs, when using Session Attributes you are creating a session-wide data storage area that has the serious possibility of data collisions, not just within your application but with other modules/libraries. To avoid problems, you should qualify the session attribute name with a package-like naming convention. For example, use something like "com.mycompany.myapp.username" instead of just "username".
It's best to define the session attribute name as constant, and use that in the annotation's value parameter, rather then defaulting to the instance variable name. This will help prevent subtle runtime errors due to misspellings. For example:
The Servlet API was designed with the intention that there would be only a modest amount of server-side state, and that the stored values would be individual numbers and strings, and thus, immutable.
However, many web applications do not use the HttpSession this way, instead storing large, mutable objects in the session. This is not a problem for single servers, but in a cluster, anything stored in the session must be serialized to a bytestream and distributed to other servers within the cluster, and restored there.
Most application servers perform that serialization and distribution whenever HttpSession.setAttribute() is called. This creates a data consistency problem for mutable objects, because if you read a mutable session object, change its state, but don't invoke setAttribute(), the changes will be isolated to just a single server in the cluster.
Tapestry attempts to solve this: any session-persisted object that is read during a request will be re-stored back into the HttpSession at the end of the request. This ensures that changed internal state of those mutable objects is properly replicated around the cluster.
But while this solution solves the data consistency problem, it does so at the expense of performance, since all of those calls to setAttribute() result in extra session data being replicated needlessly if the internal state of the mutable object hasn't changed.
Tapestry has solutions to this, too:
Tapestry knows that Java's String, Number and Boolean classes are immutable. Immutable objects do not require a re-store into the session.
You can mark your own session objects as immutable (and thus not requiring session replication) using the ImmutableSessionPersistedObject annotation.
The OptimizedSessionPersistedObject interface allows an object to control this behavior. An object with this interface can track when its mutable state changes. Typically, you should extend from the BaseOptimizedSessionPersistedObject base class.
The SessionPersistedObjectAnalyzer service is ultimately responsible for determining whether a session persisted object is dirty or not (dirty meaning in need of a restore into the session). This is an extensible service where new strategies, for new classes, can be introduced.
Starting with version 5.4, by default Tapestry will apply locking semantics around access to the HttpSession. Reading attribute names occurs with a shared read lock, and getting or setting an attribute upgrades the lock to an exclusive write lock. This can tend to serialize threads when a number of simultaneous (Ajax) requests from the client arrive. However, many implementations of HttpSession are not thread safe, and often mutable objects
are stored in the session and shared between threads.
The tapestry.session-locking-enabled symbol can control this behavior. Setting this to true (the default) will yield a more robust application; setting it to false may speed up processing for more Ajax intensive applications (but care should then be given to ensuring that objects shared inside the session are themeselves immutable or thread-safe).