你好,这里是codetrend专栏“Spring6全攻略”。
在 Spring 框架中,Bean 的作用域(Scope)定义了 Bean 实例在容器中如何创建、管理和销毁的策略。
Spring 提供了多种 Bean 作用域,每种作用域都有其特定的生命周期和适用场景。
下面通过一个简单的 Spring MVC Controller 示例来感受下 Bean 的作用域。
例子代码是这样的:
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.web.context.WebApplicationContext;
import java.util.UUID;
@Configuration
public class AppConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public SingletonBean singletonBean() {
return new SingletonBean();
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public PrototypeBean prototypeBean() {
return new PrototypeBean();
}
@Bean
@Scope(WebApplicationContext.SCOPE_SESSION)
public SessionBean sessionBean() {
return new SessionBean();
}
}
class SingletonBean {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
class PrototypeBean {
private String id;
public PrototypeBean() {
this.id = UUID.randomUUID().toString();
}
public String getId() {
return id;
}
}
class SessionBean {
private String id;
public SessionBean() {
this.id = UUID.randomUUID().toString();
}
public String getId() {
return id;
}
}
controller 代码:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ScopeController {
@Autowired
private SingletonBean singletonBean;
@Autowired
private ApplicationContext context;
@GetMapping("/singleton")
public String singletonCount() {
singletonBean.increment();
return "Singleton Count: " + singletonBean.getCount();
}
@GetMapping("/prototype")
public String prototypeGet() {
PrototypeBean prototypeBean = context.getBean(PrototypeBean.class);
return "Prototype ID: " + prototypeBean.getId();
}
@GetMapping("/session")
public String sessionGet() {
SessionBean prototypeBean = context.getBean(SessionBean.class);
return "Session ID: " + prototypeBean.getId();
}
}
/singleton
接口的返回是这样的:1
2
3
/prototype
接口的返回是这样的:Prototype ID: 3ea5af10-ddce-4a89-ad3c-3f07a764f179
Prototype ID: 7e6e9fe8-c0dc-423e-b282-96b7f8087dac
Prototype ID: 7aca1000-484d-46e8-80f7-d444a5a04f49
/session
接口的返回是这样的:Prototype ID: 7aca1000-484d-46e8-80f7-d444a5a04f49
Prototype ID: 7aca1000-484d-46e8-80f7-d444a5a04f49
# 开启新的窗口后
Session ID: bd22d310-29e5-4004-8555-9678c08275f0
Session ID: bd22d310-29e5-4004-8555-9678c08275f0
可以直接把样例代码复制到例子里面验证测试。这样我们就对BeanScope作用域有个直观的感受。
接下来通过实现一个自定义作用域来感受下Bean的作用域原理。
在 Spring 框架中,除了预定义的几种作用域(如 singleton、prototype 等)外,用户还可以自定义作用域以满足特定的业务需求。
自定义作用域允许控制 Bean 的创建、缓存和销毁逻辑,以适应特定的场景,如基于特定条件的实例化策略、自定义生命周期管理等。
自定义步骤:
org.springframework.beans.factory.config.Scope
接口,该接口定义了 Bean 作用域的基本行为。get
、remove
和registerDestructionCallback
方法,分别用于获取 Bean 实例、移除 Bean 实例以及注册销毁回调。@Scope
注解指定使用自定义的作用域名称。首先自定义作用域实现,也就是实现接口org.springframework.beans.factory.config.Scope
。
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
import java.util.UUID;
public class CustomScope implements Scope {
public final static String CUSTOM_SCOPE_NAME = "custom";
private final Map<String, Object> scopedObjects = new ConcurrentHashMap<>();
private final Map<String, Runnable> destructionCallbacks = new ConcurrentHashMap<>();
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Object scopedObject = scopedObjects.get(name);
if (scopedObject == null) {
scopedObject = objectFactory.getObject();
scopedObjects.put(name, scopedObject);
}
return scopedObject;
}
@Override
public Object remove(String name) {
scopedObjects.remove(name);
Runnable callback = destructionCallbacks.remove(name);
if (callback != null) {
callback.run();
}
return null;
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
destructionCallbacks.put(name, callback);
}
@Override
public Object resolveContextualObject(String key) {
// 可以根据需要实现上下文对象解析逻辑
return null;
}
@Override
public String getConversationId() {
// 返回一个唯一的标识,用于区分作用域上下文
return UUID.randomUUID().toString();
}
}
可以看到Scope
接口其实是对Bean的全生命周期进行管理,包括获取get、缓存和销毁remove和销毁回调等逻辑。这也是作用域的核心原理。
这里以org.springframework.web.context.request.RequestScope
为例子来理解Spring6怎么实现BeanScope的。
得益于Spring框架的抽象和封装,这个类的实现代码并没有多少。
RequestScope extends AbstractRequestAttributesScope
核心实现在这个类 AbstractRequestAttributesScope
。get
获取对象方法,其中对象的存储放在了ThreadLocal
中,也就是RequestContextHolder
这个类的核心。/**
* 根据名称获取对象,如果当前请求属性中没有该对象,则使用对象工厂创建一个对象,并将其设置到请求属性中
* 然后再次获取该对象,以便进行隐式会话属性更新。作为额外的好处,我们还允许在获取属性级别进行潜在的装饰。
* 如果再次获取到的对象不为空(预期情况),则只使用该对象。如果它同时消失了,我们则返回本地创建的实例。
*/
public Object get(String name, ObjectFactory<?> objectFactory) {
// 获取当前请求的属性
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
// 根据名称和作用域获取对象
Object scopedObject = attributes.getAttribute(name, getScope());
if (scopedObject == null) {
// 使用对象工厂创建对象
scopedObject = objectFactory.getObject();
// 将创建的对象设置到请求属性中
attributes.setAttribute(name, scopedObject, getScope());
// 再次获取对象,进行隐式会话属性更新
// 并允许进行潜在的装饰
Object retrievedObject = attributes.getAttribute(name, getScope());
if (retrievedObject!= null) {
// 只使用再次获取到的对象(如果仍然存在,这是预期情况)
// 如果它同时消失了,我们则返回本地创建的实例
scopedObject = retrievedObject;
}
}
// 返回获取到的对象
return scopedObject;
}
remove
方法也是差不多的。借助工具类RequestContextHolder
将缓存在ThreadLocal中的对象移除。/**
* 移除指定名称的对象,如果当前请求属性中存在该对象,则将其从请求属性中移除并返回该对象;否则返回 null
*/
public Object remove(String name) {
// 获取当前请求的属性
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
// 根据名称和作用域获取对象
Object scopedObject = attributes.getAttribute(name, getScope());
if (scopedObject!= null) {
// 将该对象从请求属性中移除
attributes.removeAttribute(name, getScope());
// 返回移除的对象
return scopedObject;
} else {
// 返回 null
return null;
}
}
注册作用域,需要通过BeanFactory的registerScope
方法进行注册。
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;
@Component
public class ScopeBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope(CustomScope.CUSTOM_SCOPE_NAME, new CustomScope());
}
}
将Bean注册到Spring容器中,并使用自定义作用域。
public class MyScopeBean {
private String id;
public MyScopeBean() {
this.id = UUID.randomUUID().toString();
}
public String getId() {
return id;
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
@Configuration
public class AppScopeConfig {
@Bean
@Scope(CustomScope.CUSTOM_SCOPE_NAME)
public MyScopeBean myBean() {
return new MyScopeBean();
}
}
新建一个Controller,访问/customScope
接口,返回自定义作用域的Bean实例。
@RestController
public class CustomScopeController {
@Autowired
private ApplicationContext context;
@GetMapping("/customScope")
public String customScope() {
MyScopeBean prototypeBean = context.getBean(MyScopeBean.class);
return "Prototype ID: " + prototypeBean.getId();
}
}
访问的结果输出如下:
Session ID: bd22d310-29e5-4004-8555-9678c08275f0
Session ID: bd22d310-29e5-4004-8555-9678c08275f0
Session ID: bd22d310-29e5-4004-8555-9678c08275f0
因为对象全局缓存到了一个MapscopedObjects
,所以可以看到这个自定义作用域效果和单例模式基本一致的。
Scope | 描述 |
---|---|
singleton | (Default) 将单个 bean 定义作用域限定为 Spring IoC 容器中的单个对象实例。 |
prototype | 将单个 bean 定义作用域限定为任意数量的对象实例。 |
request | 将单个 bean 定义作用域限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有自己的一个基于单个 bean 定义创建的 bean 实例。仅在 Web-aware Spring ApplicationContext 上下文中有效。 |
session | 将单个 bean 定义作用域限定为 HTTP Session 的生命周期。仅在 Web-aware Spring ApplicationContext 上下文中有效。 |
application | 将单个 bean 定义作用域限定为 ServletContext 的生命周期。仅在 Web-aware Spring ApplicationContext 上下文中有效。 |
websocket | 将单个 bean 定义作用域限定为 WebSocket 的生命周期。仅在 Web-aware Spring ApplicationContext 上下文中有效。 |
其中singleton
、prototype
是比较常用的数据。
可以通过在Spring的配置文件(如XML配置文件或Java注解)中指定@Scope
注解或<bean>
元素的scope
属性来定义Bean的Scope。
其中@Scope
注解可以是自定义的值或者如下常量:
其中ConfigurableBeanFactory.SCOPE_PROTOTYPE
是默认值。
例如:
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class MyPrototypeBean {
// Bean内容
}
或者使用XML配置:
<bean id="myBean" class="com.example.MyBean" scope="prototype">
<!-- Bean的其他配置 -->
</bean>
选择合适的Bean Scope取决于应用程序的需求。
Spring 框架设计 Bean 作用域(Scope)的原因主要是为了提供灵活性和资源管理能力,以适应不同应用场景的需求。
不同的 Bean 作用域会影响 Bean 的生命周期、创建方式和在容器中的共享程度,从而影响应用的性能、内存占用和并发处理能力。
以下是 Spring 提供 Bean 作用域设计背后的主要原因:
当单例 Bean 中注入原型(Prototype)Bean 时,会出现一个问题:
以下demo可以复现这种情况。
SpringBean的配置:
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
@Configuration
public class FaultAppConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public PrototypeInjectBean prototypeInjectBean() {
return new PrototypeInjectBean();
}
}
单例SpringBean:
import java.util.UUID;
public class PrototypeInjectBean {
private String id;
public PrototypeInjectBean() {
this.id = UUID.randomUUID().toString();
}
public String getId() {
return id;
}
}
测试代码如下:
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class PrototypeFaultController {
final private PrototypeInjectBean prototypeInjectBean;
protected PrototypeFaultController(PrototypeInjectBean prototypeInjectBean) {
this.prototypeInjectBean = prototypeInjectBean;
}
/**
* 原型作用域失效,每次返回同一个id
* @return
*/
@GetMapping("/prototypeDemo1")
public String prototypeDemo1() {
return "Prototype ID: " + prototypeInjectBean.getId();
}
}
在不重启应用或者垃圾回收的情况下,访问接口 /prototypeDemo1
原型 Bean 的id
值始终是相同的。
Prototype ID: 11893ed8-608c-452b-b2e6-82bc70b5cf97
Prototype ID: 11893ed8-608c-452b-b2e6-82bc70b5cf97
Prototype ID: 11893ed8-608c-452b-b2e6-82bc70b5cf97
那这种常用的使用场景遇到了该怎么解决呢?别急,Spring早已经给出了几种解决办法。
通过完善上面的测试代码给出3中解决方法。
修改完善后的代码如下:
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public abstract class PrototypeFaultController {
@Autowired
private ApplicationContext context;
@Autowired
private ObjectProvider<PrototypeInjectBean> prototypeBeanProvider;
final private PrototypeInjectBean prototypeInjectBean;
protected PrototypeFaultController(PrototypeInjectBean prototypeInjectBean) {
this.prototypeInjectBean = prototypeInjectBean;
}
/**
* 原型作用域失效,每次返回同一个id
* @return
*/
@GetMapping("/prototypeDemo1")
public String prototypeDemo1() {
return "Prototype ID: " + prototypeInjectBean.getId();
}
/**
* 使用实例工厂方法注入获取原型Bean,每次返回不同id
* @return
*/
@GetMapping("/prototypeDemo2")
public String prototypeDemo2() {
PrototypeInjectBean prototypeBean = context.getBean(PrototypeInjectBean.class);
return "Prototype ID: " + prototypeBean.getId();
}
/**
* Spring 提供了`ObjectProvider`接口(继承自`Provider`接口),它允许延迟查找和实例化 Bean,非常适合在单例 Bean 中按需获取原型 Bean 的新实例。
* @return
*/
@GetMapping("/prototypeDemo4")
public String prototypeDemo4() {
return "Prototype ID: " + prototypeBeanProvider.getObject().getId();
}
/**
* 使用`@Lookup`注解获取原型Bean,每次返回不同id
* @return
*/
@GetMapping("/prototypeDemo5")
public String prototypeDemo5() {
return "Prototype ID: " + getPrototypeBean().getId();
}
@Lookup
public abstract PrototypeInjectBean getPrototypeBean();
}
ObjectProvider
接口(继承自Provider
接口),它允许延迟查找和实例化 Bean,非常适合在单例 Bean 中按需获取原型 Bean 的新实例。通过访问接口/prototypeDemo4
可以发现每次返回的id
值是不同的。
通过访问接口/prototypeDemo2
可以发现每次返回的id
值是不同的。
@Lookup
注解,@Lookup
注解是Spring框架中的一个特殊注解,用于在Spring容器中查找另一个Bean,并将其注入到当前Bean中。注意使用@Lookup
注解的方法必须是抽象的(abstract)。通过访问接口/prototypeDemo5
可以发现每次返回的id
值是不同的。
来自一线全栈程序员nine的探索与实践,持续迭代中。
欢迎关注或者点个小红心~
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。