【Bean的作用域 简单易懂版】

Bean 的作用域是理解 Spring 容器管理对象方式的关键。

什么是 Bean 的作用域 (Scope)?

Bean 的作用域定义了 Spring 容器如何管理 Bean 的实例。它决定了:

  1. 会创建多少个该 Bean 的实例?
  2. 这些实例在什么时候被创建?
  3. 这些实例在什么时候会被销毁?
  4. 每次从容器获取 Bean 时,是返回同一个实例,还是创建一个新的实例?

理解作用域,就是理解 Spring 如何控制 Bean 的生命周期和共享方式。

Spring 提供了几种标准的作用域:

1. Singleton (单例)

  • 含义: 在整个 Spring 容器中,只创建该 Bean 的一个实例
  • 生命周期:
    • 通常在 Spring 容器启动时创建(或者在第一次被请求时创建,取决于是否设置了懒加载)。
    • 该实例会一直存在于容器中,直到容器关闭。
    • 容器负责管理其完整的生命周期,包括创建、初始化和销毁。
  • 特点: 所有的请求(无论是来自同一个用户还是不同用户,无论是同一个 HTTP 请求还是不同 HTTP 请求)都共享这同一个 Bean 实例。
  • 用途: 这是 Spring 的默认作用域。适用于无状态的 Bean,比如 Service 层、DAO 层、Controller 层等。这些 Bean 通常只包含方法,不包含会随用户或请求变化的状态数据。
  • 举例:
    @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
    
    想象一下,你的应用程序启动后,Spring 容器就创建了一个 UserService 对象。所有需要 UserService 的地方,Spring 都把这个同一个对象“递”给你用。

2. Prototype (原型)

  • 含义: 每一次从 Spring 容器中请求该 Bean 时,都会创建一个新的实例
  • 生命周期:
    • 在每次请求时创建。
    • Spring 容器只负责创建原型 Bean,不负责管理其后续的生命周期(比如销毁)。一旦 Bean 被创建并交给请求者,Spring 就不再跟踪它了。你需要自己负责管理它的销毁(如果需要的话)。
  • 特点: 每个请求都获得一个全新、独立的 Bean 实例。
  • 用途: 适用于有状态的 Bean,或者每次使用都需要一个全新实例的场景。比如一个表示工作任务的 Bean,一个临时的计算器 Bean,或者需要保存用户特定状态但又不适合放在 Session 里的 Bean。
  • 举例:
    @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
    
    也就是说:每次你向 Spring 容器要一个 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、本次请求特有的数据等。
  • 举例:
    @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";
        }
    }
    
    当一个 HTTP 请求 /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 是什么:

  1. HTTP 请求 (HTTP Request):

    • 这是客户端(通常是浏览器)向服务器发送的一个独立的请求,比如你点击一个链接、提交一个表单、或者浏览器加载一个图片。
    • 这是一个非常短暂的过程:客户端发送请求 -> 服务器处理请求 -> 服务器发送响应 -> 过程结束。
    • 每一次这样的交互都是一个新的 HTTP 请求。
  2. 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 作用域 BeanSession 作用域 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

对于 requestsessionglobal sessionwebsocket 这些 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 连接相关状态
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值