Home > Software Development > Practical Spring MVC Part 5: Sessions

Practical Spring MVC Part 5: Sessions

In this fast-paced, demo-driven series, I will take you on an exciting tour of Spring MVC. Unlike “pet clinic” style demonstrations, I will make use of practical solutions to real-world problems in order to demonstrate the breadth of functionality offered by Spring MVC. This article is a perfect fit for anyone looking for a quick overview of Spring MVC and its capabilities. Furthermore, complete code is included for those who wish to a follow along closely and explore the concepts presented.

Introduction

In the previous articles we explored how to bind entities to types and vice versa.  Nonetheless, occasionally types are huge or complex adequate that it is desirable to split them more than several pages.  Given that HTTP is stateless, this raises the question of how to retailer the data in between pages.

One could envision that as every type is submitted the command object is loaded, updated, and saved back to the database.  Sadly, in all but the most simple business circumstances a partially completed command object would violate validation constraints and possibly database constraints.  Worse, if the user abandons the procedure the middle of the flow, an incomplete command object would stay in the database.  Clearly, it would be greatest to accumulate the kind submission somewhere else.

The solution to this difficulty is provided by sessions a nearby, persistent shop connected with every user of your web site.  Luckily, Spring MVC makes functioning with session information entirely transparent.

Author Registration

To demonstrate working with session data, we will build a registration form for authors who wish to contribute to our blog.  Here is the flow of events:

  1. The user visits “/authors/register.html”.
  2. The user is asked for his first and last name.
  3. The user is then asked for his contact information.
  4. The user is given a chance to review his information.
  5. Finally, the user submits the registration.

At any time throughout the process, the user can go back and make changes to previous steps.

The Basics

We annotate AuthorRegistrationController with an @SessionAttributes annotation.  In this annotation we list one or more model attributes that are to be persisted in the session automatically.

@Controller
@RequestMapping("authors/register")
@SessionAttributes("registration")
public class AuthorRegistrationController {

In this example, anything placed in the model with the key/name of “registration” will be transparently stored in the session and be made available through the model in all subsequent requests.

The startFlow method handles requests for the entry page “/authors/register.html”.

@RequestMapping("")
public String startFlow(ModelMap model) {
	model.put("registration", new AuthorRegistrationForm());
	return "redirect:/authors/register/name.html";
}

The method creates a new AuthorRegistrationForm and places in the model as “registration”.  At any time in the future we can access this registration form through model.get(“registration”).  The method then redirects the user to the first step in the process, collection of his name.

Accessing Session Variables

First, we must present the user with the main form:

@RequestMapping("name")
public String collectName(@ModelAttribute("registration") AuthorRegistrationForm registration) {
	return "authors/register/name";
}

The registration parameter is marked with @ModelAttribute.  This tells Spring to associate this parameter with the model attribute “registration”.  Effectively, this parameter will be set to the registration form we created in the previous step.

We use the same technique in processing the form:

@RequestMapping(value="name", method=RequestMethod.PUT)
public String saveName(@ModelAttribute("registration") @Valid AuthorRegistrationForm registration, BindingResult result) {
	result.recordSuppressedField("email");
	result.recordSuppressedField("phoneNumber");
	if (result.hasErrors())
		return "authors/register/name";
	
	return "redirect:/authors/register/contact.html";
}

The registration parameter will be set to the registration form stored in the session (the model attribute “registration”), and then updated with the first and last name submitted by the form.

Validation

If we just validated the registration kind now, the e-mail and telephone number fields, which cannot be blank, would fail to validate.  Internally, we know that is okay because we have not completed the registration method.  This is a general problem for multistage types requiring validation and there are two common options.

The straightforward remedy offered right here is to explicitly ignore validation failures associated with certain fields.  In this instance we inform the BindingResult object to ignore errors connected to the email and phoneNumber fields at this time.

Although this remedy functions for easy instances, it does not scale well with large numbers of fields and numerous actions.  In that case is suggested that you create a separate form object dedicated to every web page in the procedure that defines its personal information and validation rules proper to that step in the procedure.  All of these can be stored in one parent object, which is subsequently stored in the session.  In the really last step, submitting the registration in this instance, all of the person types would be merged into the underlying registration entity and validated 1 final time.  Whilst this may appear a small redundant (think of DTOs), it assures you of maximum flexibility.

Moving On

The next step, the collection of contact information, works identically:

@RequestMapping("contact")
public String collectContact(@ModelAttribute("registration") AuthorRegistrationForm registration) {
	return "authors/register/contact";
}

@RequestMapping(value="contact", method=RequestMethod.PUT)
public String saveContact(@ModelAttribute("registration") @Valid AuthorRegistrationForm registration, BindingResult result) {
	if (result.hasErrors())
		return "authors/register/contact";
	
	return "redirect:/authors/register/review.html";
}

The only difference is in the validation, which does not suppress any fields because the form should be completely filled out by this time.

Moving Back

If you have been running the code locally as you step through this article, then you will noticed a back link on a number of the pages that take you previous steps.  As you navigate back and forth between the two forms (name and contact info), you will see the data you submitted and any changes you make.  It simply works “the right way”.

Completing

When the user has entered his data and reviewed it, he will ultimately submit the registration.

@RequestMapping("submit")
public String submit (@ModelAttribute("registration") AuthorRegistrationForm registration, SessionStatus session) {
	session.setComplete();
	return "";
}

The SessionStatus parameter provides some basic control over the session attribute preceding it, in this case the registration.  Once the registration has been validated and saved to the database (not shown here) session.setComplete() should be called.  This will remove the registration form from the session so the next time the user begins this flow, he will be presented with a new, fresh registration form.

Exceptions

What if the user leaves his computer for a while and then comes back?  This session will have expired and he will no longer have access to the registration form.  Or, what happens if the user bypasses our startFlow method and heads directly to one of the forms?  Again, the registration form will not be available in the session.  In any case where a session attribute is accessed but not available Spring will throw an exception.

Exception handling in Spring controllers is dirt simple.  Simply create a method and annotate it with @ExceptionHandler and identify the exceptions this method should handle.  Anytime Spring catches an exception of the specified type it will redirect to the annotated method.

@ExceptionHandler(HttpSessionRequiredException.class)
public String restartFlow() {
	return "redirect:/authors/register.html";
}

Here, when a session is not available this method will be called and the user will be redirected back to the beginning of the flow (startFlow).

Session Scoped Beans

The strategy outlined above works fantastic for “conversational” information data that is specific to an instance of a workflow.  Nevertheless, oftentimes there is a need to associate information with a user all through his whole interaction (session) with the net application.  In addition, this information is often required by a lot more than one controller.  Although you can access the HTTPSession object directly, it is much better to use session scoped beans (eliminating the dependency on the servlet container).

Session scoped beans work like normal spring beans in that they can be injected and autowired.  In truth, they feel like any other singleton Spring-managed bean.  However, behind the scenes their data is associated with the session so every user will see a distinct instance with their personal data.

Consider an example where we want to keep track of a set of user preferences for the current user.  This information can be used throughout the entire web application and possibly the presentation layer.  In this example, I have used the UserPreferences class to keep track of the user’s preferred name, which I presumably could use to display a nice welcome message on each page.

@Component
@Scope(value="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
public class UserPreferences {

The class is marked with @Component, which makes it Spring-managed.  The @Scope annotation is used to make all instances session scoped.  It is also used to define the proxy mode, which I am not going to describe here.  Suffice it to say, you must specify proxy mode whenever you use session scope (TARGET_CLASS will provide the most expected behavior if you do not understand the differences between proxy modes).

Now, an instance of UserPreferences can be injected into any Spring-managed bean, including controllers, and accessed.

@Autowired
private UserPreferences preferences;

You are guaranteed that the instance will always be not null and always be tied/related to the user’s session.  You can use a @ModelAttribute method in your controller to automatically expose the user preferences to your model:

@ModelAttribute("preferences")
public UserPreferences getPreferences() {
	return preferences;
}

Summary

This is the end of my 5-component series on Spring MVC.  We have focused on the breadth of features rather than covering specific functions in great depth.  The Spring documentation is outstanding and covers every little thing in excellent detail.  As such, my aim was to offer you with a fast idea of just what this framework can help you achieve.

  1. No comments yet.