Spring Start Here 读书笔记:第9章 Using the Spring web scopes
在第五章中,我们学习了Spring bean的范围(scope)包括Singleton(默认,非并发)和Prototype,以及即时实例化和延迟实例化。
本章将学习另外几个和Web应用相关的范围(web scope):
- 请求范围——Spring 为每个 HTTP 请求创建一个 Bean 类的实例。该实例仅存在于该特定的 HTTP 请求中。
- 会话范围——Spring 创建一个实例,并将其保存在服务器内存中,以完成整个 HTTP 会话。Spring 将上下文中的实例与客户端会话关联起来。
- 应用程序范围——该实例在应用上下文中是唯一的,并且在应用运行时可用。
这些都是Spring框架赋予你的能力。
9.1 在 Spring Web 应用中使用请求作用域
请求范围的 Bean 是由 Spring 管理的对象,框架会为每个 HTTP 请求创建一个新的实例。应用只能将该实例用于创建它的请求。任何新的 HTTP 请求(来自同一客户端或其他客户端)都会创建并使用同一类的不同实例。
请求作用域 bean 的关键方面:
事实 | 后果 | 考虑 | 避免 |
---|---|---|---|
Spring 会为来自任何客户端的每个 HTTP 请求创建一个新的实例。 | Spring 在应用执行期间会在内存中创建大量此 Bean 的实例。 | 实例数量通常不是什么大问题,因为这些实例的生命周期很短。HTTP 请求完成后,应用会释放这些实例,然后它们会被垃圾回收。 | 但是,请确保不要实现 Spring 创建实例时需要执行的耗时逻辑(例如从数据库获取数据或执行网络调用)。避免在构造函数或 @PostConstruct 方法中为请求范围的 bean 编写逻辑。 |
只有一个请求可以使用一个请求作用域 bean 的实例。 | 请求作用域 bean 的实例不易出现多线程相关问题,因为只有一个线程(请求所在的线程)可以访问它们。 | 您可以使用实例的属性来存储请求使用的数据。 | 不要对这些 Bean 的属性使用同步技术。这些技术是多余的,而且只会影响应用的性能。 |
登录应用的场景非常适合演示请求范围Bean,因为用户的用户名口令只用于登录,后续不会保存。当然,在生产环境中,登录建议用Spring Security实现,作者也有一本书讲这个:Spring Security in Action (Manning, 2020)。
参见示例sq-ch9-ex1
。这是一个登录验证程序。
登录页面如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>Login</title>
</head>
<body><form action="/" method="post">Username: <input type="text" name="username" /><br />Password: <input type="password" name="password" /><br /><button type="submit">Log in</button></form><p th:text="${message}"></p>
</body>
</html>
然后,LoginController是控制类,其中并没有存放用户名和口令敏感信息。LoginProcessor是model类,其中有属性用户名和口令,但他的范围是基于请求的(见@RequestScope注解),因此数据也不会长期保留在内存中。
界面如下:
9.2 在 Spring Web 应用中使用会话作用域
会话是指HTTP会话。会话范围的 Bean 允许我们存储同一客户端的多个请求共享的数据。
会话作用域 Bean适用的场景包括:
- 登录 - 在经过身份验证的用户访问应用的不同部分并发送多个请求时,保留其详细信息。
- 在线购物车 - 用户访问应用中的多个位置,搜索并添加到购物车中的商品。购物车会记住客户端添加的所有商品。
会话范围 bean 的关键方面
事实 | 后果 | 考虑 | 避免 |
---|---|---|---|
会话范围的 bean 实例会在整个 HTTP 会话期间保留。 | 与 requestscoped bean 相比,它们的寿命更长,而且垃圾收集频率更低。 | 应用会将您存储在会话范围的 bean 中的数据保留更长时间。 | 避免在会话中保存过多数据。这可能会造成性能问题。此外,切勿在会话 Bean 属性中存储敏感信息(例如密码、私钥或任何其他机密信息)。 |
多个请求可以共享会话范围的 Bean 实例。 | 如果同一客户端发出多个并发请求来更改实例上的数据,则可能会遇到与多线程相关的问题,例如竞争条件。 | 当你知道这种情况可能发生时,你可能需要使用同步技术来避免并发。但是,我通常建议你先看看是否可以避免这种情况,并且只有在无法避免的情况下才将同步作为最后的手段。 | |
会话范围的 Bean 是一种通过将数据保存在服务器端来实现请求间数据共享的方法。 | 您实现的逻辑可能意味着请求彼此依赖。 | 当将状态详细信息保存在一个应用的内存中时,客户端会依赖于该特定的应用实例。在决定使用会话范围的 Bean 实现某些功能之前,请考虑其他替代方案,例如将要共享的数据存储在数据库中,而不是会话中。这样,您就可以使 HTTP 请求彼此独立。 |
参见示例sq-ch9-ex2
,他实现了以下4方面:
- 创建一个会话范围的 Bean 来保存已登录用户的详细信息。(model类LoginProcessor,仍带@RequestScope注解;服务类LoggedUserManagementService,带@SessionScope注解)
- 创建用户只有登录后才能访问的页面。(验证登录使用控制类LoginController)
- 确保用户在未登录的情况下无法访问步骤 1 中创建的页面。(控制类MainController中的逻辑实现)
- 身份验证成功后,将用户从登录页面重定向到主页面。
在本例中,会话范围Bean存储的只有用户名,登录成功后会设置此用户名(最初为空),登出时会置空此用户名。根据用户名是否为空,就可以实现以上所有的逻辑。
这是登录成功后的页面:
点击Log out后,又回到登录页面。
9.3 在 Spring Web 应用中使用应用程序作用域
生产系统应避免使用,而是用数据库持久层来实现。
应用程序作用域与单例的工作方式类似。区别在于,应用程序作用域bean在整个应用中只有一个实例,并且在讨论 Web 作用域(包括应用程序作用域)的生命周期时,我们始终使用 HTTP 请求作为参考点。
应用程序作用域的 Bean与单例 Bean 一样,都存在并发问题。最好为单例 Bean 和应用程序作用域Bean设置不可变的属性。但如果将属性设置为不可变,则可以直接使用单例 Bean。
示例sq-ch9-ex3
实现了登录计数(也包括失败的登录)。
和上一个例子相比,增加了一个应用范围的服务类LoginCountService :
@Service
@ApplicationScope
public class LoginCountService {private int count;public void increment() {count++;}public int getCount() {return count;}
}
另外,model类也做了微小的改动,即在登录成功后计数器加1:
public boolean login() {loginCountService.increment();
...
}
最后,main.html稍微修改以显示登录次数。
界面如下:
总结
- 除了单例和原型 bean 作用域之外,在 Spring Web 应用中还有三个 bean 作用域,他们只在 Web 应用中才有意义,因此我们称之为 Web 作用域:
- 请求作用域——Spring 为每个 HTTP 请求创建一个 bean 实例。
- 会话作用域——Spring 为每个客户端的 HTTP 会话创建一个 bean 实例。来自同一客户端的多个请求可以共享同一个实例。
- 应用程序作用域——对于该特定 bean,整个应用只有一个实例。来自任何客户端的每个请求都可以访问此实例。
- Spring 保证请求范围的 Bean 实例只能被一个 HTTP 请求访问。因此,您可以放心使用实例的属性,而不必担心并发相关的问题。此外,您也不必担心它们会填满应用的内存。由于它们的生命周期很短,因此在 HTTP 请求结束后,这些实例就会被垃圾回收。
- Spring 会为每个 HTTP 请求创建请求范围的 Bean 实例。这种情况很常见。最好不要通过在构造函数或 @PostConstruct 方法中实现逻辑来增加实例创建的难度。
- Spring 将会话范围的 Bean 实例链接到客户端的 HTTP 会话。这样,会话范围的 Bean 实例可用于在来自同一客户端的多个 HTTP 请求之间共享数据。
- 即使来自同一个客户端,客户端也可以并发发送 HTTP 请求(例如在同一页面中同时发送多个请求)。如果这些请求更改了会话范围实例中的数据,则可能会陷入竞争条件。您需要避免这种情况,或者让您的代码以支持并发。
- 避免使用应用程序范围的 Bean 实例。由于所有 Web 应用请求共享应用程序范围的 Bean 实例,任何写入操作通常都需要同步,从而造成瓶颈并严重影响应用的性能。此外,这些 Bean 会在应用内存中驻留的时间与应用本身一样长,因此无法被垃圾回收。更好的方法是将数据直接存储在数据库中。
- 会话范围的 Bean 和应用程序范围的 Bean 都意味着请求的独立性会降低。我们称应用程序管理请求所需的状态(或者说应用程序是有状态的)。有状态的应用程序意味着不同的架构问题,这些问题最好避免。