寿命和在Spring MVC中注入@SessionAttributes对象

6

我对在Spring Boot 2.3.3.RELEASE中使用Spring MVC的@SessionAttributes存在一些细微之处不清楚。

  • 我有两个控制器,Step1ControllerStep2Controller
  • 这两个控制器在类级别上都使用了@SessionAttributes("foobar")
  • Step1Controller在处理@PostMapping请求时,使用model.addAttribute("foobar", new FooBar("foo", "bar"))向模型添加一个特殊的FooBar实例。
  • Step2Controller在完全独立的HTTP POST下调用,在其@PostMapping服务方法中使用doSomething(FooBar fooBar)获取FooBar实例。
  • 一切都很顺利!

但是我对为什么它有效的一些细节不太清楚。

@SessionAttributes API文档部分内容如下:

那些属性将在处理程序指示其对话会话完成后被删除。因此,在特定处理程序对话过程中,使用此功能来存储临时存储的会话属性。对于永久性的会话属性,例如用户认证对象,请改用传统的session.setAttribute方法。
  1. 如果 @SessionAttributes 只是临时将模型属性存储在HTTP会话中,并在对话结束时删除它们,为什么 foobar 仍然会显示在发送到 Step2Controller 的请求中?在我的看法中,它似乎仍然存在于会话中。我不明白文档中所指的"临时"和"处理程序对话"是什么意思。看起来 foobar 在会话中被正常存储。
  2. 似乎只要在 Step1Controller 上使用 @SessionAttributes("foobar"),Spring 就会在处理请求后自动将 foobar 从模型复制到会话中。文档中稍微提到了这一点,但通过实验才清楚明白。
  3. 似乎只要在 Step2Controller 上使用 @SessionAttributes("foobar"),Spring 就会在请求之前将 foobar 从会话中复制到模型中。从文档中我完全没有看出这一点。
  4. 最后,请注意在 Step2Controller.doSomething(FooBar fooBar) 中,除了控制器类上的 @SessionAttributes("foobar")(但这并没有在参数上有任何注解),我对 FooBar 参数没有任何注解。文档似乎表明我需要在方法参数上添加 @ModelAttribute 注解,例如 Step2Controller.doSomething(@ModelAttribute("foobar") FooBar fooBar) 或至少 Step2Controller.doSomething(@ModelAttribute FooBar fooBar)。但是,即使在参数上没有任何注释,Spring 似乎仍然会找到会话变量。为什么?我怎么知道这一点?
这是我对Spring一直感到困惑的事情之一:有太多的事情发生得“神奇”,没有清晰的文档说明预期会发生什么。使用Spring多年的人可能只是凭感觉知道什么有效,什么无效;但是一个新开发者看着代码只能相信它会神奇地做它应该做的事情。
为什么我描述的东西能够工作,特别是第一个问题的参考?也许通过这种方式,我也能够培养出这种“Spring感”,本能地知道该使用哪些咒语。

我认为这个问题本质上很好,但我会改变两件事情:不要用符号点列出您的代码外观,而是直接展示给我们。 "展示不要告诉" 也适用于 StackOverflow。您倒数第二段的文字也让我感到有些牢骚,我不知道它真正添加了什么内容。 - Michael
1个回答

4

这个答案有两部分

  1. 提供关于SessionAttributes的一些通用信息
  2. 解答问题本身

Spring中的@SessionAttributes

@SessionAttributes的javadoc指出,它应该被用来暂时存储属性:

使用这个功能来存储那些在特定处理程序对话期间暂时存储在会话中的对话属性。

Temporal boundaries of such a "conversation" are defined by the programmer explicitly, or more precisely, the programmer defines the completion of the conversation. They can do this through SessionStatus. Here is the relevant part of the documentation and an 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 shown in the following example.
@Controller
@SessionAttributes("pet") 
public class EditPetForm {
    @PostMapping("/pets/{id}")
    public String handle(Pet pet, BindingResult errors, SessionStatus status) {
        if (errors.hasErrors) {
            // ...
        }
        status.setComplete(); 
        // ...
    }
}

如果你想深入了解,可以研究以下源代码:

浏览问题

  1. 如果@SessionAttributes仅在HTTP会话中临时存储模型属性,并在对话结束时将其删除,为什么foobar仍然显示在对Step2Controller的请求中?

因为很可能你没有定义对话完成。

在我看来,它仍然存在于会话中。

完全正确。

我不明白文档中提到的“临时”和“处理程序的对话”是什么意思。

我猜这与Spring WebFlow有关。(请参见this介绍性文章)

看起来foobar通常存储在会话中。

是的,请参见DefaultSessionAttributeStore 你可能会问:使一些会话属性具有临时性,而另一些则没有,是什么原因?它们如何区分?答案可以在源代码中找到:

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. 看起来只要在Step1Controller上有@SessionAttributes("foobar"),Spring就会在处理请求后自动将foobar从模型复制到会话中。

是的,它会这样做

  1. 看起来只要在Step2Controller上放置@SessionAttributes("foobar"),Spring就会在请求之前将foobar从会话中复制到模型中。

同样正确

最后,注意在Step2Controller.doSomething(FooBar fooBar)中,除了控制器类上的@SessionAttributes("foobar")之外,我在FooBar参数上没有任何注释。文档似乎表明我需要在方法参数上添加一个@ModelAttribute注释,例如Step2Controller.doSomething(@ModelAttribute("foobar") FooBar fooBar)或至少Step2Controller.doSomething(@ModelAttribute FooBar fooBar)。但是,即使在参数上没有任何注释,Spring仍然会找到会话变量。为什么?我怎么知道这个呢?请参见方法参数部分:

如果方法参数未与此表中较早的任何值匹配,并且它是一个简单类型(由BeanUtils#isSimpleProperty确定),则将其解析为@RequestParam。否则,它将被解析为@ModelAttribute。

这是我一直对Spring感到困扰的事情之一:太多的事情发生在“神奇”的情况下,没有清晰的文档说明预期会发生什么。使用Spring多年的人可能只是对什么有效、什么无效有一个“感觉”,但是新开发人员看着代码只能相信它会神奇地完成它应该做的事情。在这里,我建议阅读参考文档,它可以给出如何描述Spring的某些具体行为的线索。

2020年10月11日更新:

Denis,这个自动将模型中的参数作为方法参数应用的功能只适用于接口吗?我发现如果FooBar是一个接口,Step2Controller.doSomething(FooBar fooBar)按照上面讨论的方式工作。但是如果FooBar是一个类,即使我在模型中有FooBar的实例,Step2Controller.doSomething(FooBar fooBar)会导致“找不到类FooBar的主要或默认构造函数”异常。即使使用@ModelAttribute也不起作用。我必须使用@ModelAttribute(“foobar”)。为什么类和接口在参数替换方面有不同的工作方式?

这听起来像是命名/ @SessionAttributes#names 有些问题。

我创建了一个 示例项目 来演示问题可能隐藏的位置。

该项目分为两部分:

  1. 尝试使用类
  2. 尝试使用接口

该项目的入口点是两个测试(请参见ClassFooBarControllerTestInterfaceFooBarControllerTest

我已经留下了注释来解释这里发生的事情。

2
caco3,这是一个真正让我豁然开朗并指引我前往正确方向的神奇答案。我将在问题上添加一份悬赏以表彰您的工作。非常感谢您。 - Garret Wilson
1
我很高兴它有帮助。 - Denis Zavedeev
Denis,这种自动将模型参数应用为方法参数的能力只适用于接口吗?我发现如果FooBar是一个接口,Step2Controller.doSomething(FooBar fooBar)会像上面讨论的那样工作。但是如果FooBar是一个类,即使我在模型中有FooBar的实例,Step2Controller.doSomething(FooBar fooBar)也会导致“找不到类FooBar的主要或默认构造函数”异常。即使使用@ModelAttribute也不起作用,我必须使用@ModelAttribute("foobar")。为什么类在参数替换方面与接口不同呢? - Garret Wilson
谢谢,Denis。但是名字中不可能有错别字,因为我一直在使用字符串常量。你的示例并没有完全重现出这种情况。最重要的是你的 FooBar 类有一个无参构造函数,那么它怎么会产生我展示的错误呢?试试这个:1)删除 FooBar 的无参构造函数,2)在 Controller1 中创建一个 FooBar 实例,并将其放入模型中,键为 "foobar",3)在 Controller2 中,在 GETPUT 映射方法中引用 FooBar 类型,但没有任何注释。(两个控制器都有一个 @SessionAttributes("foobar") 当然。) - Garret Wilson
同时,我将使用 foobar @ModelAttribute("foobar") 注释参数;这似乎很好用。我并不是想麻烦您调试我的代码。我只是好奇为什么 Spring 似乎可以自动从模型中注入基于接口的参数而不需要命名的 @ModelAttribute 注释,但不能注入基于具体类的参数;我想知道您是否了解这方面的任何信息。再次感谢。 - Garret Wilson
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接