I'm unclear on some subtleties of using @SessionAttributes
in Spring MVC via Spring Boot 2.3.3.RELEASE.
Step1Controller
and Step2Controller
.@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)
.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.
@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.@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.@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.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.
@SessionAttribute annotation retrieve the existing attribute from the session. This annotation allows you to tell Spring which of your model attributes will also be copied to HttpSession before rendering the view.
There is a Front Controller pattern and the Front Controller in Spring MVC is DispatcherServlet. Upon every incoming request from the user, Spring manages the entire life cycle as described in here. In the overall view, DispatcherServlet dispatches the request to a controller for a service at the back-end.
SessionAttribute annotation is the simplest and straight forward instead of getting session from request object and setting attribute. Any object can be added to the model in controller and it will stored in session if its name matches with the argument in @SessionAttributes annotation.
In this setup, we don't define TodoList as a Spring-managed @Bean. Instead, we declare it as a @ModelAttribute and specify the @SessionAttributes annotation to scope it to the session for the controller. The first time our controller is accessed, Spring will instantiate an instance and place it in the Model.
this answer has two parts
SessionAttributes
@SessionAttributes
in SpringThe @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:
SessionAttributesHandler
- 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.
Yes, it will
- It would appear that by placing
@SessionAttributes("foobar")
onStep2Controller
, Spring copiesfoobar
from the session to the model before the request.
Also true
- 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:
The entry point to the project are the two tests (see ClassFooBarControllerTest
and InterfaceFooBarControllerTest
)
I've left comments to explain what is happening here
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With