Bean 的作用域是理解 Spring 容器管理对象方式的关键。
什么是 Bean 的作用域 (Scope)?
Bean 的作用域定义了 Spring 容器如何管理 Bean 的实例。它决定了:
- 会创建多少个该 Bean 的实例?
- 这些实例在什么时候被创建?
- 这些实例在什么时候会被销毁?
- 每次从容器获取 Bean 时,是返回同一个实例,还是创建一个新的实例?
理解作用域,就是理解 Spring 如何控制 Bean 的生命周期和共享方式。
Spring 提供了几种标准的作用域:
1. Singleton (单例)
- 含义: 在整个 Spring 容器中,只创建该 Bean 的一个实例。
- 生命周期:
- 通常在 Spring 容器启动时创建(或者在第一次被请求时创建,取决于是否设置了懒加载)。
- 该实例会一直存在于容器中,直到容器关闭。
- 容器负责管理其完整的生命周期,包括创建、初始化和销毁。
- 特点: 所有的请求(无论是来自同一个用户还是不同用户,无论是同一个 HTTP 请求还是不同 HTTP 请求)都共享这同一个 Bean 实例。
- 用途: 这是 Spring 的默认作用域。适用于无状态的 Bean,比如 Service 层、DAO 层、Controller 层等。这些 Bean 通常只包含方法,不包含会随用户或请求变化的状态数据。
- 举例:
想象一下,你的应用程序启动后,Spring 容器就创建了一个@Service // 默认就是 singleton 作用域 public class UserService { // 这个 Bean 通常是无状态的,只提供方法 public User getUserById(Long id) { // ... 调用 DAO ... return null; } } // 在应用程序的不同地方获取 UserService Bean UserService service1 = applicationContext.getBean(UserService.class); UserService service2 = applicationContext.getBean(UserService.class); // 这里的 service1 和 service2 指向的是同一个对象实例 // service1 == service2 的结果是 true
UserService
对象。所有需要UserService
的地方,Spring 都把这个同一个对象“递”给你用。
2. Prototype (原型)
- 含义: 每一次从 Spring 容器中请求该 Bean 时,都会创建一个新的实例。
- 生命周期:
- 在每次请求时创建。
- Spring 容器只负责创建原型 Bean,不负责管理其后续的生命周期(比如销毁)。一旦 Bean 被创建并交给请求者,Spring 就不再跟踪它了。你需要自己负责管理它的销毁(如果需要的话)。
- 特点: 每个请求都获得一个全新、独立的 Bean 实例。
- 用途: 适用于有状态的 Bean,或者每次使用都需要一个全新实例的场景。比如一个表示工作任务的 Bean,一个临时的计算器 Bean,或者需要保存用户特定状态但又不适合放在 Session 里的 Bean。
- 举例:
也就是说:每次你向 Spring 容器要一个@Component @Scope("prototype") // 明确指定为 prototype 作用域 public class TaskProcessor { private String taskName; // 假设这个 Bean 需要保存一个任务名称的状态 public void setTaskName(String taskName) { this.taskName = taskName; } public void process() { System.out.println("Processing task: " + taskName); } } // 在应用程序的不同地方获取 TaskProcessor Bean TaskProcessor processor1 = applicationContext.getBean(TaskProcessor.class); processor1.setTaskName("Download File"); processor1.process(); // 输出: Processing task: Download File TaskProcessor processor2 = applicationContext.getBean(TaskProcessor.class); processor2.setTaskName("Upload Data"); processor2.process(); // 输出: Processing task: Upload Data // 这里的 processor1 和 processor2 指向的是不同的对象实例 // processor1 == processor2 的结果是 false
TaskProcessor
时,Spring 就像一个工厂一样,给你“生产”一个全新的TaskProcessor
对象。这两个对象是完全独立的,修改一个不会影响另一个。
可能存在的问题:“每一次对bean请求都会创建一个新的bean实例”是什么意思?与返回同一实例有什么区别?
- “每一次对 bean 请求”: 在 Spring 的语境下,这通常指:
- 通过
applicationContext.getBean("beanName")
方法显式地从容器获取 Bean。 - 通过
@Autowired
或@Resource
等注解进行依赖注入时,Spring 容器为你查找并提供 Bean 实例。
- 通过
- 原型 (Prototype) 的含义: 每当发生上述“请求”行为时,Spring 容器都会执行
new YourBeanClass()
的操作,创建一个全新的对象实例,并返回给你。 - 单例 (Singleton) 的含义: 无论发生多少次上述“请求”行为,Spring 容器都只会返回它在启动时(或第一次被请求时)创建的那唯一一个对象实例。
区别在于:
- 单例: 你拿到的是同一个对象。如果你修改了这个对象的状态,下次再获取它时,看到的就是修改后的状态。所有使用这个单例 Bean 的地方,都在操作同一个对象。
- 原型: 你拿到的是一个新的对象。这个对象的状态是独立的,不会受到之前获取的同类型 Bean 实例的影响。每个请求者都拥有自己的独立副本。
Web 相关的作用域 (仅在 Web 应用上下文有效):
这些作用域需要 Spring Web 模块的支持,并且通常在 WebApplicationContext
中使用。
3. Request (请求)
- 含义: 为每一个 HTTP 请求创建一个新的 Bean 实例。
- 生命周期:
- 在接收到 HTTP 请求时创建。
- 该实例仅在当前 HTTP 请求的生命周期内有效。
- 请求处理完毕后,该 Bean 实例会被销毁。
- 特点: 同一个请求中,多次获取该 Bean 会得到同一个实例。不同请求之间,该 Bean 的实例是独立的。
- 用途: 存储与当前请求相关的状态信息,比如请求 ID、用户 IP、本次请求特有的数据等。
- 举例:
当一个 HTTP 请求@Component @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) // 需要 proxyMode public class RequestInfo { private String requestId; // 存储当前请求的 ID // Getter/Setter public String getRequestId() { return requestId; } public void setRequestId(String requestId) { this.requestId = requestId; } } @Controller public class MyController { @Autowired private RequestInfo requestInfo; // 注入 RequestInfo Bean @GetMapping("/test") public String testRequestScope(HttpServletRequest request) { // 在同一个请求中,requestInfo 是同一个实例 String id = UUID.randomUUID().toString(); requestInfo.setRequestId(id); // 设置请求 ID // 在这个请求的处理过程中,任何地方注入 RequestInfo 都会得到这个设置了 ID 的实例 System.out.println("Request ID set: " + requestInfo.getRequestId()); return "success"; } }
/test
到达时,Spring 会创建一个RequestInfo
对象。在MyController
中注入的requestInfo
就是这个对象。如果在处理这个请求的过程中,其他地方也注入了RequestInfo
,它们拿到的也是同一个对象。当这个请求处理完,这个RequestInfo
对象就被销毁了。下一个 HTTP 请求到来时,会创建另一个全新的RequestInfo
对象。
4. Session (会话)
- 含义: 为每一个 HTTP Session 创建一个新的 Bean 实例。
- 生命周期:
- 在 HTTP Session 首次需要该 Bean 时创建。
- 该实例在整个 HTTP Session 的生命周期内有效。
- Session 失效或被销毁时,该 Bean 实例也会被销毁。
- 特点: 同一个 Session 中,多次获取该 Bean 会得到同一个实例。不同 Session 之间,该 Bean 的实例是独立的。
- 用途: 存储与当前用户会话相关的状态信息,比如登录用户信息、购物车内容、用户偏好设置等。
- 举例:
当用户首次访问需要@Component @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) // 需要 proxyMode public class ShoppingCart { private List<String> items = new ArrayList<>(); // 存储购物车商品 public void addItem(String item) { this.items.add(item); } public List<String> getItems() { return items; } } @Controller public class ProductController { @Autowired private ShoppingCart shoppingCart; // 注入 ShoppingCart Bean @GetMapping("/add/{item}") public String addItemToCart(@PathVariable String item) { shoppingCart.addItem(item); // 向当前用户的购物车添加商品 return "redirect:/cart"; } @GetMapping("/cart") public String viewCart(Model model) { model.addAttribute("items", shoppingCart.getItems()); // 获取当前用户的购物车内容 return "cartView"; } }
ShoppingCart
的页面时,Spring 会为这个用户的 Session 创建一个ShoppingCart
对象。用户在网站上浏览、添加商品,无论访问多少个页面,只要在同一个 Session 内,注入的shoppingCart
都是同一个对象。这个对象保存了该用户当前的购物车状态。另一个用户访问网站时,会拥有自己的独立ShoppingCart
对象。
这里可能对于Request和Session的区别划分的不是很清晰,那我们继续来学习它们之间的不同点:
我们先来明确一下 HTTP 请求 和 HTTP Session 是什么:
-
HTTP 请求 (HTTP Request):
- 这是客户端(通常是浏览器)向服务器发送的一个独立的请求,比如你点击一个链接、提交一个表单、或者浏览器加载一个图片。
- 这是一个非常短暂的过程:客户端发送请求 -> 服务器处理请求 -> 服务器发送响应 -> 过程结束。
- 每一次这样的交互都是一个新的 HTTP 请求。
-
HTTP Session (HTTP Session):
- 这是一个更长久的概念,代表了同一个客户端(通常是同一个浏览器)与服务器之间的一系列相关的交互。
- 服务器通过某种机制(最常见的是 Session ID,通常存储在浏览器 Cookie 中)来识别来自同一个客户端的请求属于同一个 Session。
- 一个 Session 可以持续一段时间(比如几分钟到几小时),即使客户端发起了多个 HTTP 请求。
- Session 的目的是在多个请求之间保持用户的状态。
我们来看 request
和 session
作用域 Bean 的区别:
1. Request 作用域 Bean:
- 生命周期: 绑定到单个 HTTP 请求的生命周期。
- 实例数量: 每收到一个 HTTP 请求,Spring 就会为这个请求创建一个新的
request
作用域的 Bean 实例。 - 共享范围: 这个 Bean 实例仅在当前这个 HTTP 请求的处理过程中有效。在同一个请求的处理链中(比如 Controller 调用 Service,Service 调用 DAO),多次获取这个 Bean 都会得到同一个实例。
- 销毁: 当这个 HTTP 请求处理完毕,响应发送回客户端后,这个 Bean 实例就会被销毁。
- 用途: 存储仅与当前请求相关的临时数据或状态。
举例:
假设你有一个 Bean 叫做 RequestLogger
,你想用它来记录每个请求的一些信息,比如请求开始时间、请求路径等。
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestLogger {
private long startTime;
private String requestPath;
public void start(String path) {
this.startTime = System.currentTimeMillis();
this.requestPath = path;
System.out.println("Request started for: " + requestPath);
}
public void logEndTime() {
long endTime = System.currentTimeMillis();
System.out.println("Request finished for: " + requestPath + ", duration: " + (endTime - startTime) + "ms");
}
}
@Controller
public class MyController {
@Autowired
private RequestLogger requestLogger; // 注入 RequestLogger
@GetMapping("/process")
public String processSomething(HttpServletRequest request) {
requestLogger.start(request.getRequestURI()); // 在请求开始时调用
// ... 执行一些业务逻辑 ...
requestLogger.logEndTime(); // 在请求结束前调用
return "view";
}
}
- 当用户 A 发起一个
/process
请求时,Spring 会创建一个RequestLogger
实例 A。在处理这个请求的过程中,MyController
注入的requestLogger
就是实例 A。 - 几乎同时,用户 B 也发起一个
/process
请求时,Spring 会为用户 B 的请求创建另一个全新的RequestLogger
实例 B。用户 B 的请求处理过程中使用的就是实例 B。 - 用户 A 的请求处理完,实例 A 被销毁。用户 B 的请求处理完,实例 B 被销毁。
- 如果用户 A 再次发起一个
/process
请求,Spring 会为这个新的请求创建第三个全新的RequestLogger
实例 C。
核心: request
作用域 Bean 的生命周期非常短,只活在一次完整的“请求-响应”交互中。
2. Session 作用域 Bean:
- 生命周期: 绑定到单个 HTTP Session 的生命周期。
- 实例数量: 每创建一个新的 HTTP Session,Spring 就会为这个 Session 创建一个新的
session
作用域的 Bean 实例(通常是在该 Session 首次需要这个 Bean 时)。 - 共享范围: 这个 Bean 实例在整个 Session 的生命周期内有效。来自同一个客户端(同一个 Session)的所有后续 HTTP 请求,获取这个 Bean 时都会得到同一个实例。
- 销毁: 当这个 HTTP Session 失效(比如用户长时间不活动,或者显式地使 Session 失效)时,这个 Bean 实例就会被销毁。
- 用途: 存储与用户会话相关的状态信息,需要在用户的多次请求之间保持。
举例:
假设你有一个 ShoppingCart
Bean,用来存储用户的购物车内容。购物车内容需要在用户浏览网站、添加商品、查看购物车等多个页面(多个请求)之间保持。
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart {
private List<String> items = new ArrayList<>(); // 存储购物车商品
public void addItem(String item) {
this.items.add(item);
}
public List<String> getItems() {
return items;
}
}
@Controller
public class ProductController {
@Autowired
private ShoppingCart shoppingCart; // 注入 ShoppingCart Bean
@GetMapping("/add/{item}")
public String addItemToCart(@PathVariable String item) {
shoppingCart.addItem(item); // 向当前用户的购物车添加商品
return "redirect:/cart";
}
@GetMapping("/cart")
public String viewCart(Model model) {
model.addAttribute("items", shoppingCart.getItems()); // 获取当前用户的购物车内容
return "cartView";
}
}
- 用户 A 第一次访问网站,Spring 创建一个 Session A。当用户 A 访问
/add/apple
时,Spring 会为 Session A 创建一个ShoppingCart
实例 A。apple
被添加到实例 A 的items
列表中。 - 用户 A 接着访问
/cart
。由于是同一个 Session A,Spring 注入的shoppingCart
仍然是实例 A。页面显示实例 A 中的商品列表(包含 apple)。 - 用户 A 又访问
/add/banana
。仍然是同一个 Session A,注入的shoppingCart
还是实例 A。banana
被添加到实例 A 的items
列表中。 - 用户 B 访问网站,Spring 创建一个 Session B。当用户 B 访问
/add/orange
时,Spring 会为 Session B 创建另一个全新的ShoppingCart
实例 B。orange
被添加到实例 B 的items
列表中。 - 用户 B 访问
/cart
。由于是同一个 Session B,注入的shoppingCart
是实例 B。页面显示实例 B 中的商品列表(包含 orange)。 - 用户 A 的 Session 失效后,实例 A 被销毁。用户 B 的 Session 失效后,实例 B 被销毁。
核心: session
作用域 Bean 的生命周期较长,贯穿用户与网站交互的整个会话过程,用于在多个请求之间保持用户状态。
总结区别:
特性 | Request 作用域 Bean | Session 作用域 Bean |
---|---|---|
生命周期 | 单个 HTTP 请求 | 单个 HTTP Session (跨多个请求) |
实例数量 | 每个 HTTP 请求一个新实例 | 每个 HTTP Session 一个新实例 |
共享范围 | 仅在当前请求内共享 | 在同一个 Session 的所有请求中共享 |
用途 | 存储请求相关的临时数据 | 存储用户会话相关的状态数据 (购物车, 登录) |
持续时间 | 非常短 (请求处理时间) | 较长 (Session 有效期) |
所以,虽然它们都与 Web 环境有关,但 request
关注的是一次独立的交互,而 session
关注的是同一个用户的一系列相关交互。
言归正传:
5. Global Session (全局会话)
- 含义: 仅在基于 Portlet 的 Web 应用中有效。它为每一个全局 Portlet Session 创建一个 Bean 实例。
- 用途: 在 Portlet 环境中共享信息。
- 注意: 在标准的 Servlet Web 应用(如 Spring MVC 或 Spring Boot 应用)中,这个作用域不常用,或者说没有实际意义。如果不是在开发 Portlet 应用,基本可以忽略这个作用域。
6. WebSocket
- 含义: 为每一个 WebSocket 连接创建一个新的 Bean 实例。
- 生命周期:
- 在 WebSocket 连接建立并需要该 Bean 时创建。
- 该实例在整个 WebSocket 连接的生命周期内有效。
- 连接关闭时,该 Bean 实例会被销毁。
- 用途: 存储与特定 WebSocket 连接相关的状态信息。
- 举例: 在一个多人聊天应用中,可能需要一个 Bean 来管理某个用户在特定连接上的状态或数据。
关于 Web 作用域的 proxyMode
:
对于 request
, session
, global session
, websocket
这些 Web 作用域的 Bean,如果它们被注入到单例 Bean 中,会有一个问题:单例 Bean 在容器启动时就创建了,而 Web 作用域的 Bean 是在请求/会话开始时才创建。如果直接注入,单例 Bean 拿到的将是一个 null
或者一个过期的实例。
为了解决这个问题,Spring 使用了代理 (Proxy)。当你设置 proxyMode = ScopedProxyMode.TARGET_CLASS
(或 INTERFACES
) 时,Spring 注入的不是实际的 Web 作用域 Bean 实例,而是一个代理对象。当单例 Bean 调用这个代理对象的方法时,代理对象会去查找当前请求/会话中真正的 Web 作用域 Bean 实例,并将方法调用委托给它。这样就保证了单例 Bean 总是能访问到当前上下文(请求或会话)中正确的 Bean 实例。
总结一下:
作用域 | 实例数量 | 生命周期 | 用途 | 默认? | Web 环境? |
---|---|---|---|---|---|
Singleton | 整个容器中一个 | 容器启动到关闭 | 无状态 Bean (Service, DAO, Controller) | 是 | 否 (通用) |
Prototype | 每次请求 (getBean/注入) 时创建一个新实例 | 创建后 Spring 不管理销毁 | 有状态 Bean, 每次需要独立实例的场景 | 否 | 否 (通用) |
Request | 每个 HTTP 请求一个 | 请求开始到结束 | 存储请求相关状态 | 否 | 是 |
Session | 每个 HTTP Session 一个 | Session 开始到结束 | 存储用户会话相关状态 (购物车, 登录信息) | 否 | 是 |
Global Session | 每个全局 Portlet Session 一个 | 全局 Session 开始到结束 | Portlet 环境共享状态 | 否 | 是 (Portlet) |
WebSocket | 每个 WebSocket 连接一个 | 连接建立到关闭 | 存储 WebSocket 连接相关状态 | 否 | 是 |