使用Spring Web作用域的指南(第一部分)

114 阅读13分钟

Spring以不同的方式管理Bean的生命周期,这取决于你在Spring上下文中如何声明Bean。Spring有自定义的方法,通过使用HTTP请求作为参考点来管理Web应用的实例。Spring是非常酷的,不是吗?

在任何Spring应用中,你可以选择将Bean声明为

  • Singleton- Spring中默认的Bean范围,对于它来说,框架在上下文中用一个名字唯一地识别每个实例。
  • 原型--Spring中的Bean范围,对于它,框架只管理类型,并在每次有人请求它时(直接从上下文或通过布线或自动布线)创建该类的新实例。

在这篇文章中,你将了解到在Web应用程序中,你可以使用其他bean作用域。这些Bean作用域只与Web应用程序有关。我们称它们为Web作用域,它们的内容如下:

  • 请求作用域 - Spring为每个HTTP请求创建一个bean类的实例。这个实例只为那个特定的HTTP请求而存在。
  • 会话作用域--Spring为整个HTTP会话创建一个实例并将该实例保存在服务器的内存中。Spring将上下文中的实例与客户端的会话联系起来。
  • 应用范围--该实例在应用的上下文中是唯一的,并且在应用运行时是可用的。

为了教你这些Web作用域如何在Spring应用程序中工作,我们将在整个章节中研究一个例子。在这个例子中,我们实现了一个登录功能。今天的大多数Web应用都为用户提供了登录和访问账户的可能性,从现实世界的角度来看,这个例子也很有意义。

首先,我们将使用一个请求域Bean来获取用户的登录凭证,并确保应用程序只在登录请求中使用这些凭证。然后,我们将使用一个会话作用的Bean来存储所有我们需要为登录用户保留的相关细节,只要该用户保持登录状态。最后,我们将使用应用范围的Bean来为我们的应用程序添加一个计算登录次数的功能。图1向你展示了我们在整个章节中为实现这个应用程序所采取的步骤。


图1.我们将分三步实现登录功能。对于我们实现的每一步,我们都需要使用不同的bean范围。首先,我们将使用一个请求作用域的Bean来实现登录逻辑,而不需要冒险将凭证的存储时间超过登录请求的时间。然后,我们决定在一个会话范围的Bean中为认证的用户存储哪些细节。最后,我们实现了一个统计所有登录请求的功能,并使用一个应用域的Bean来保存登录请求的数量。


在Spring Web应用中使用请求域

在本节中,你将学习如何在Spring Web应用程序中使用请求作用域Bean。Web应用程序专注于HTTP请求和响应。由于这个原因,而且通常在Web应用程序中,如果Spring为你提供了一种管理与HTTP请求有关的Bean生命周期的方法,那么某些功能就更容易管理。

请求范围的Bean是一个由Spring管理的对象,框架为每个HTTP请求创建一个新的实例。应用程序只能在创建该实例的请求中使用该实例。任何新的HTTP请求(来自同一个或其他客户端)都会创建并使用同一个类的不同实例(图2)。


图2.对于每个HTTP请求,Spring都会为请求作用域的Bean提供一个新的实例。当使用请求作用域的Bean时,你可以确定你在Bean上添加的数据只在创建Bean的HTTP请求中可用。Spring管理Bean类型(工厂),并使用它来为每个新请求获取实例(咖啡豆)。


请求范围的Bean的关键方面

在深入实现一个使用请求作用域Bean的Spring应用之前,我想在这里简短地列举一下使用这种Bean作用域的关键方面。这些方面可以帮助你分析在现实世界的情况下,请求作用域的Bean是否是正确的方法。请记住请求作用域Bean的这些相关方面。

事实

后果

要考虑

要避免

Spring为来自任何客户端的每个HTTP请求创建一个新实例

在执行过程中,Spring会在应用程序的内存中创建大量的Bean实例。

实例的数量通常不是一个大问题。这是因为这些实例是短命的。应用程序需要它们的时间不会超过HTTP请求需要完成的时间。一旦HTTP请求完成,应用程序就会释放这些实例,它们就会被垃圾收集起来。

确保你没有实现Spring需要执行的耗时逻辑来创建实例(比如从数据库获取数据或实现网络调用)。避免在构造函数或@PostConstruct方法中为请求范围的Bean编写逻辑。

只有一个请求可以使用请求域Bean的一个实例。

请求域Bean的实例不容易出现多线程相关的问题,因为只有一个线程(请求的那个)可以访问它们。

你可以使用实例的属性来存储请求使用的数据。

不要对这些Bean的属性使用同步技术。这些技术是多余的,而且它们只会影响你的应用程序的性能。

现在让我们在一个例子中演示一下请求域Bean的使用。我们将实现一个Web应用程序的登录功能,我们将使用一个请求域Bean来管理用户的登录逻辑的凭证。

注意像这样的登录例子,我们在本文中用来演示,对于教学来说是非常好的,但在一个可以生产的应用中,最好不要自己实现认证和授权机制。在真实世界的Spring应用中,我们使用Spring Security来实现任何与认证和授权有关的东西。使用Spring Security(它也是Spring生态系统的一部分)可以简化你的实现,并确保你在编写应用级安全逻辑时不会(错误地)引入漏洞。我建议你也读一下《Spring Security in Action》,这是我写的一本书,里面详细描述了如何使用Spring Security来保护你的Spring应用。

为了使事情简单明了,我们考虑将一组凭证植入我们的应用程序。在现实世界的应用程序中,应用程序将用户存储在一个数据库中。它还会对密码进行加密以保护它们。现在,我们只关注本文的目的--讨论Spring Web Bean的作用域。

让我们创建一个Spring Boot项目并添加所需的依赖项。你可以在提供的项目sq-ch9-ex1中找到这个例子。你可以在创建项目时直接添加依赖项(例如,使用start.spring.io),或者之后在你的pom.xml中添加。在这个例子中,我们使用Web依赖关系和Thymeleaf作为模板引擎。接下来的代码片段显示了你需要在pom.xml文件中设置的依赖关系。

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
 </dependency>
 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
 </dependency>

我们将创建一个包含登录表单的页面,要求提供用户的姓名和密码。应用程序将用户名和密码与它知道的一组凭证进行比较(在我的例子中,用户 "natalie "和密码 "password")。如果我们提供了正确的凭证(它们与应用程序所知道的凭证相匹配),那么页面就会在登录表格下显示 "您现在已登录 "的信息。如果我们提供的凭证不正确,那么应用程序就会显示一条信息。"登录失败"。

我们需要实现一个页面(代表我们的视图)和一个控制器类。控制器根据登录的结果向视图发送它需要显示的信息(图3)。


图3.我们需要实现控制器和视图。在控制器中,我们实现了一个动作,找出登录请求中发送的凭证是否有效。控制器向视图发送一条信息,而视图则显示这条信息。


清单1显示了HTML登录页面,它定义了我们应用程序中的视图。你需要将该页面存储在Spring Boot项目的resources/templates文件夹中。在这个例子中,让我们把这个页面命名为login.html。为了显示逻辑结果的消息,我们需要从控制器向视图发送一个参数。我把这个参数命名为 "message",你可以在清单1中看到,我使用了${message}的语法,在登录表单下的一段显示这个参数。

清单1.登录页面的定义 login.html

 <!DOCTYPE html>
 <html lang="en" xmlns:th="http://www.thymeleaf.org">    

#A 我们定义了 "th "Thymeleaf前缀来使用模板引擎的功能。

#B 我们定义了一个HTML表单来发送证书到服务器上。

#C 输入字段是用来写证书、用户名和密码的。

#D 当用户点击提交按钮时,客户端就会用证书发出一个HTTP POST请求。

#E 我们在HTML表单下显示一个登录请求结果的信息。

一个控制器动作需要获得HTTP请求(来自dispatcher servlet)。让我们为清单1中创建的页面定义控制器和接收HTTP请求的动作。 在列表2中,你可以看到控制器类的定义。我们把控制器的动作映射到Web应用程序的根路径("/")。我把这个控制器命名为LoginController。

清单2.控制器的动作被映射到了根路径上

 @Controller   

#A 我们使用@Controller定型注解,将该类定义为Spring MVC控制器。

#B 我们将控制器的动作映射到应用程序的根("/")路径上

#C 我们返回我们希望由应用程序渲染的视图名称。

现在我们有了一个登录页面,我们要实现登录逻辑。当用户点击提交按钮时,我们希望页面在登录表格下显示一个适当的信息。如果用户提交了正确的证书集,信息就是 "你现在已经登录了",否则,显示的信息就是 "登录失败"(图4)。


图4.我们在本节中实现的功能。该页面为用户显示一个登录表格。然后用户提供有效的凭证,应用程序就会显示一条消息,告诉他们已经成功登录。如果用户提供了不正确的凭证,应用程序就会告诉用户,登录失败。


为了处理HTML表单在用户点击提交按钮时产生的HTTP POST请求,我们需要在LoginController中再添加一个动作。这个动作接收客户端的请求参数(用户名和密码),并根据登录结果向视图发送一个消息。清单3显示了控制器动作的定义,我们将其映射到HTTP POST登录请求。

请注意,我们还没有实现登录的逻辑。在清单3中,我们接受请求,并根据代表请求结果的变量发送消息,但这个变量(在清单3中名为loggedIn)始终是 "false"。在本节的下一个列表中,我们通过添加对登录逻辑的调用来完成这个动作。这个登录逻辑根据客户在请求中发送的凭证返回登录结果。

清单3.控制器的登录动作

 @Controller
 public class LoginController {
  
   @GetMapping("/")
   public String loginGet() {
     return "login.html";
   }
  
   @PostMapping("/")    

#A 我们将控制器的动作映射到登录页面的HTTP POST请求中。

#B 我们从HTTP请求参数中获取证书。

#C 我们声明一个Model参数来发送消息值到视图中。

#D 当我们以后实现登录逻辑时,这个变量会存储登录请求的结果。

#E 根据登录的结果,我们向视图发送一个特定的消息。

#F 我们返回视图的名称,仍然是login.html,并且我们仍然在同一个页面上。

图5直观地描述了控制器类和我们实现的视图之间的联系。


图5.当有人提交HTML登录表单时,调度器servlet会调用控制器的动作。控制器的动作从HTTP请求参数中获取凭证。根据登录结果,控制器向视图发送一条信息,视图在HTML表单下显示这条信息。


好了!我们有一个控制器和一个视图,但是这一切中的请求范围在哪里?我们写的唯一一个类是LoginController,而且我们让它成为一个单例,这是默认的Spring范围。只要LoginController不在其属性中存储任何细节,我们就不需要改变它的作用域,但请记住,我们需要实现登录逻辑。登录逻辑取决于用户的凭证,我们必须考虑到关于这些凭证的两件事。

  1. 凭证是敏感的细节,你不希望在应用程序的内存中存储它们的时间超过登录请求的时间。
  2. 更多拥有不同凭证的用户可能试图同时登录。

考虑到这两点,我们需要确保如果我们使用Bean来实现登录逻辑,每个实例对每个HTTP请求都需要是唯一的。我们需要使用一个请求域的Bean。我们将扩展如图5所示的应用程序。我们添加一个请求域的Bean LoginProcessor。这个Bean接收请求中的凭证并进行验证(图6)。


图6.LoginProcessor Bean是请求域的。Spring确保为每个HTTP请求创建一个新的实例。这个Bean实现了登录逻辑。控制器调用它实现的一个方法。如果凭证有效,该方法返回true,否则返回false。根据LoginProcessor返回的值,LoginController会向视图发送正确的信息。


清单4显示了LoginProcessor类的实现。为了改变Bean的范围,我们使用@RequestScoped注解。我们仍然需要通过在配置类中使用@Bean注解或使用定型注解,使该类成为Spring上下文中的一个Bean。在这个例子中,我选择了用@Component定型注解来注解该类。

清单4/ 请求域的LoginProcessorBean实现了登录逻辑

 @Component    

#A 我们用定型注解来告诉Spring这是一个Bean类。

#B 我们使用@RequestScope注解将Bean的范围改为请求范围。这样一来,Spring就会为每个HTTP请求创建一个新的类实例。

#C 该Bean将凭证存储为属性。

#D 该Bean定义了一个方法来实现登录逻辑。

你可以运行该应用程序,并使用浏览器地址栏中的localhost:8080地址访问登录页面。图7向你展示了应用程序在访问该页面后的行为,以及使用有效和不正确凭证的行为。


图7.当在浏览器中访问该页面时,该应用程序显示了一个登录表单。你可以使用有效的凭证,应用程序会显示一个成功的登录信息。如果你使用不正确的凭证,应用程序会显示一个 "登录失败!"的消息。