Life span and injection of @SessionAttributes object in Spring MVC

Tags: , , ,



I’m unclear on some subtleties of using @SessionAttributes in Spring MVC via Spring Boot 2.3.3.RELEASE.

  • I have two controllers, Step1Controller and Step2Controller.
  • Both controllers use @SessionAttributes("foobar") at the class level.
  • Step1Controller during its request handling for @PostMapping adds a special FooBar instance to the model using model.addAttribute("foobar", new FooBar("foo", "bar")).
  • Step2Controller, invoked under a completely independent HTTP POST, picks up the FooBar instance in its @PostMapping service method using doSomething(FooBar fooBar).
  • This all words great!

But I’m unclear on some details of why it works.

The @SessionAttributes API documentation says in part:

Those attributes will be removed once the handler indicates completion of its conversational session. Therefore, use this facility for such conversational attributes which are supposed to be stored in the session temporarily during the course of a specific handler’s conversation. For permanent session attributes, e.g. a user authentication object, use the traditional session.setAttribute method instead.

  1. If @SessionAttributes only stores model attributes in the HTTP session temporarily and removes them at the end of the conversation, why does foobar still show up in the request to Step2Controller? It appears to me to still be in the session. I don’t understand what the docs mean when they refer to “temporarily” and “handler’s conversation”. It would appear foobar is stored in the session normally.
  2. It would appear that simply by having @SessionAttributes("foobar") on Step1Controller, Spring will automatically copy foobar from the model to the session after handling the request. That was sort of hinted at in the documentation, but it only became clear to me through experimentation.
  3. It would appear that by placing @SessionAttributes("foobar") on Step2Controller, Spring copies foobar from the session to the model before the request. This was not clear to me at all from the documentation.
  4. And finally, note that in Step2Controller.doSomething(FooBar fooBar) I don’t have any annotation at all on the FooBar parameter, other than the @SessionAttributes("foobar") (but that is on the controller class). The documentation seemed to indicate I need to add a @ModelAttribute annotation to the method parameter, such as Step2Controller.doSomething(@ModelAttribute("foobar") FooBar fooBar) or at least Step2Controller.doSomething(@ModelAttribute FooBar fooBar). But Spring still seems to find the session variable, even with no annotation at all on the parameter. Why? How would I have known this?

This is on of the things that has always bugged me about Spring: too many things happen “magically”, with no clear documentation of what is expected to happen. People who use Spring for years I suppose just get a “feel” for what works and doesn’t; but a new developer looking at the code just has to trust that it magically does what it’s supposed to.

Could someone clarify why what I have described works, especially enlightening me on the first question? Maybe that way I too can develop this “Spring sense” to instinctively know which incantations to evoke. Thank you.

Answer

this answer has two parts

  1. Giving some general information about SessionAttributes
  2. Going through the question itself

@SessionAttributes in Spring

The @SessionAttributes‘s javadoc states that it should be used to store attributes temporarily:

use this facility for such conversational attributes which are supposed to be stored in the session temporarily during the course of a specific handler’s conversation.

Temporal boundaries of such a “conversation” are defined by programmer explicitly, or to be more exact: programmer defines completion of conversation, they can do it via SessionStatus. Here is relevant part of documentation and example:

On the first request, when a model attribute with the name, pet, is added to the model, it is automatically promoted to and saved in the HTTP Servlet session. It remains there until another controller method uses a SessionStatus method argument to clear the storage, as the following example shows:

@Controller
@SessionAttributes("pet") 
public class EditPetForm {
    @PostMapping("/pets/{id}")
    public String handle(Pet pet, BindingResult errors, SessionStatus status) {
        if (errors.hasErrors) {
            // ...
        }
        status.setComplete(); 
        // ...
    }
}

If you want to dig deep you can study the source code of:

Going through the question

  1. If @SessionAttributes only stores model attributes in the HTTP session temporarily and removes them at the end of the conversation, why does foobar still show up in the request to Step2Controller?

Because, most probably you have not defined conversation completion.

It appears to me to still be in the session.

Exactly

I don’t understand what the docs mean when they refer to “temporarily” and “handler’s conversation”.

I guess it’s somehow related to the Spring WebFlow. (See this introductory article)

It would appear foobar is stored in the session normally.

Yes, see DefaultSessionAttributeStore

You may ask here: What does make some session attributes temporal and some not? How are they distinguished?. The answer may be found in the source code:

SessionAttributesHandler.java#L146:

/**
 * Remove "known" attributes from the session, i.e. attributes listed
 * by name in {@code @SessionAttributes} or attributes previously stored
 * in the model that matched by type.
 * @param request the current request
 */
public void cleanupAttributes(WebRequest request) {
    for (String attributeName : this.knownAttributeNames) {
        this.sessionAttributeStore.cleanupAttribute(request, attributeName);
    }
}
  1. It would appear that simply by having @SessionAttributes("foobar") on Step1Controller, Spring will automatically copy foobar from the model to the session after handling the request.

Yes, it will

  1. It would appear that by placing @SessionAttributes("foobar") on Step2Controller, Spring copies foobar from the session to the model before the request.

Also true

  1. And finally, note that in Step2Controller.doSomething(FooBar fooBar) I don’t have any annotation at all on the FooBar parameter, other than the @SessionAttributes("foobar") (but that is on the controller class). The documentation seemed to indicate I need to add a @ModelAttribute annotation to the method parameter, such as Step2Controller.doSomething(@ModelAttribute("foobar") FooBar fooBar) or at least Step2Controller.doSomething(@ModelAttribute FooBar fooBar). But Spring still seems to find the session variable, even with no annotation at all on the parameter. Why? How would I have known this?

See Method Arguments section:

If a method argument is not matched to any of the earlier values in this table and it is a simple type (as determined by BeanUtils#isSimpleProperty, it is a resolved as a @RequestParam. Otherwise, it is resolved as a @ModelAttribute.

This is on of the things that has always bugged me about Spring: too many things happen “magically”, with no clear documentation of what is expected to happen. People who use Spring for years I suppose just get a “feel” for what works and doesn’t; but a new developer looking at the code just has to trust that it magically does what it’s supposed to.

Here I would suggest going through the reference documentation, it can give a clue how can you describe some specific behavior of Spring


10/11/2020 update:

Denis, does this ability to automatically apply an argument from the model as a method argument only work with interfaces? I’ve found that if FooBar is an interface, Step2Controller.doSomething(FooBar fooBar) works as discussed above . But if FooBar is a class, even if I have an instance of FooBar in the model, Step2Controller.doSomething(FooBar fooBar) results in a “No primary or default constructor found for class FooBar” exception. Even @ModelAttribute won’t wor k. I have to use @ModelAttribute(“foobar”). Why do classes work differently from interfaces in parameter substitution?

This sounds to me, that there is some issue with naming/@SessionAttributes#names.

I’ve created a sample project to demonstrate where the problem may be hidden.

The project has two parts:

  1. Attempts to use class
  2. Attempts to use an interface

The entry point to the project are the two tests (see ClassFooBarControllerTest and InterfaceFooBarControllerTest)

I’ve left comments to explain what is happening here



Source: stackoverflow