I’m unclear on some subtleties of using @SessionAttributes
in Spring MVC via Spring Boot 2.3.3.RELEASE.
- I have two controllers,
Step1Controller
andStep2Controller
. - Both controllers use
@SessionAttributes("foobar")
at the class level. Step1Controller
during its request handling for@PostMapping
adds a specialFooBar
instance to the model usingmodel.addAttribute("foobar", new FooBar("foo", "bar"))
.Step2Controller
, invoked under a completely independent HTTPPOST
, picks up theFooBar
instance in its@PostMapping
service method usingdoSomething(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.
- If
@SessionAttributes
only stores model attributes in the HTTP session temporarily and removes them at the end of the conversation, why doesfoobar
still show up in the request toStep2Controller
? 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 appearfoobar
is stored in the session normally. - It would appear that simply by having
@SessionAttributes("foobar")
onStep1Controller
, Spring will automatically copyfoobar
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. - It would appear that by placing
@SessionAttributes("foobar")
onStep2Controller
, Spring copiesfoobar
from the session to the model before the request. This was not clear to me at all from the documentation. - And finally, note that in
Step2Controller.doSomething(FooBar fooBar)
I don’t have any annotation at all on theFooBar
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 asStep2Controller.doSomething(@ModelAttribute("foobar") FooBar fooBar)
or at leastStep2Controller.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.
Advertisement
Answer
this answer has two parts
- Giving some general information about
SessionAttributes
- 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 aSessionStatus
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
- If
@SessionAttributes
only stores model attributes in the HTTP session temporarily and removes them at the end of the conversation, why doesfoobar
still show up in the request toStep2Controller
?
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); } }
- It would appear that simply by having
@SessionAttributes("foobar")
onStep1Controller
, Spring will automatically copyfoobar
from the model to the session after handling the request.
- It would appear that by placing
@SessionAttributes("foobar")
onStep2Controller
, Spring copiesfoobar
from the session to the model before the request.
- And finally, note that in
Step2Controller.doSomething(FooBar fooBar)
I don’t have any annotation at all on theFooBar
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 asStep2Controller.doSomething(@ModelAttribute("foobar") FooBar fooBar)
or at leastStep2Controller.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:
- Attempts to use class
- 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