Placing the profile in a session

The next thing we want is the profile to be stored in a session so that it does not get reset every time we go on the profile page. This can apparently prove tiresome to some users and we have to address it.

Tip

HTTP sessions are a way to store information between requests. HTTP is a stateless protocol, which means that there is no way to relate two requests coming from the same user. What most Servlet containers do is they associate a cookie called JSESSIONID to each user. This cookie will be transmitted in the request header and will allow you to store arbitrary objects in a map, an abstraction called HttpSession. Such a session will typically end when the user closes or switches web browsers or after a predefined period of inactivity.

We just saw a method to put objects in a session using the @SessionAttributes annotation. This works well within a controller but makes the data difficult to share when spread across multiple controllers. We have to rely on a string to resolve the attribute from its name, which is hard to refactor. For the same reason, we don't want to manipulate the HttpSession directly. Another argument that will discourage the direct usage of the session is how difficult it is to unit test the controller that depends on it.

There is another popular approach when it comes to saving things in a session with Spring: annotate a bean with @Scope("session").

You will then be able to inject your session bean in your controllers and other Spring components to either set or retrieve values from it.

Let's create a UserProfileSession class in the profile package:

package masterSpringMvc.profile;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserProfileSession implements Serializable {
    private String twitterHandle;
    private String email;
    private LocalDate birthDate;
    private List<String> tastes = new ArrayList<>();

    public void saveForm(ProfileForm profileForm) {
        this.twitterHandle = profileForm.getTwitterHandle();
        this.email = profileForm.getEmail();
        this.birthDate = profileForm.getBirthDate();
        this.tastes = profileForm.getTastes();
    }

    public ProfileForm toForm() {
        ProfileForm profileForm = new ProfileForm();
        profileForm.setTwitterHandle(twitterHandle);
        profileForm.setEmail(email);
        profileForm.setBirthDate(birthDate);
        profileForm.setTastes(tastes);
        return profileForm;
    }
}

We have conveniently provided a way to convert from and to a ProfileForm object. This will help us store and retrieve the form data from our ProfileController constructor. We need to inject our UserProfileSession variable in the controller's constructor and store it as a field. We also need to expose the ProfileForm as a model attribute, which will remove the need to inject it in the displayProfile method. Finally, we can save the profile once it has been validated:

@Controller
public class ProfileController {

    private UserProfileSession userProfileSession;
    @Autowired
    public ProfileController(UserProfileSession userProfileSession) {
        this.userProfileSession = userProfileSession;
    }

    @ModelAttribute
    public ProfileForm getProfileForm() {
        return userProfileSession.toForm();
    }

    

    @RequestMapping(value = "/profile", params = {"save"}, method = RequestMethod.POST)
    public String saveProfile(@Valid ProfileForm profileForm, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "profile/profilePage";
        }
        userProfileSession.saveForm(profileForm);
        return "redirect:/profile";
    }

    // the rest of the code is unchanged
}

That's all it takes to save data in a session with Spring MVC.

Now, if you complete the profile form and refresh the page, the data will be persisted between requests.

Just before moving on to the next chapter, I want to detail a couple of concepts we just used.

The first is the injection by the constructor. The ProfileController constructor is annotated with @Autowired, which means Spring will resolve the constructor arguments from the application context before instantiating the bean. The alternative, which is a little less verbose, would have been to use field injection:

@Controller
public class ProfileController {

    @Autowired
    private UserProfileSession userProfileSession;
}

Constructor injection is arguably better because it makes the unit testing of our controller easier if we were to move away from the spring-test framework and it makes the dependencies of our bean somewhat more explicit.

For a detailed discussion on field injection and constructor injection, refer to the excellent blog post by Oliver Gierke at http://olivergierke.de/2013/11/why-field-injection-is-evil/.

Another thing that might need clarification is the proxyMode parameter on the Scope annotation:

@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)

There are three proxyMode parameters available with Spring, if we don't count the default one:

  • TARGET_CLASS: This uses a CGI proxy
  • INTERFACES: This creates a JDK proxy
  • NO: This does not create any proxy

The advantage of a proxy typically comes into play when you inject something into a long-lived component such as a singleton. Because injection only happens once, when the bean is created, subsequent calls to the injected bean might not reflect its actual state.

In our case, a session bean's actual state is stored in the session and not directly on the bean. This explains why Spring has to create a proxy: it needs to intercept calls to our bean methods and listen for its mutations. This way, the state of the bean can be transparently stored and retrieved from the underlying HTTP session.

For a session bean, we are forced to use a proxy mode. The CGI proxy will instrument your bytecode and work on any class, whereas the JDK approach might be a bit more lightweight but requires you to implement an interface.

Lastly, we made the UserProfileSession bean implement the Serializable interface. This is not strictly required because the HTTP sessions can store arbitrary objects in memory, but making objects that end up in the session serializable really is a good practice.

Indeed, we might change the way the session is persisted. In fact, we will store the session in a Redis database in Chapter 8, Optimizing Your Requests, where Redis has to work with Serializable objects. It's always best to think of the session of a generic data store. We have to provide a way to write and read objects from this storage system.

For serialization to work properly on our bean, we also need every one of its field to be serializable. In our case, strings and dates are serializable so we are good to go.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.93.175