Spring IOC官方文档学习笔记(五)之bean的作用域
1.Bean的作用域
(1) Bean的作用域即Bean实例的作用范围,Spring支持6种bean的作用域,其中4种只能在web环境中使用,具体如下
作用域 | 描述 |
---|---|
singleton | 默认作用域,采用单例模式,Spring只会创建一个该bean实例,每次请求时Spring返回的都是同一个bean实例 |
prototype | 采用原型模式,Spring会创建多个该bean实例,每次请求时Spring返回的都是一个新的bean实例 |
request | 仅用于web环境,Spring会为每次Http请求创建一个新的bean实例 |
session | 仅用于web环境,Spring会为每个Session创建一个新的bean实例 |
application | 仅用于web环境,Spring会为每个ServletContext创建一个新的bean实例 |
websocket | 仅用于web环境,Spring会为每个websocket创建一个新的bean实例 |
(2) Singleton作用域:如果一个bean的作用域为singleton,那么Spring只会创建出一个该bean实例存储于IOC容器中,之后每次对这个bean的请求都只会返回容器中的那个特定的bean实例,换句话说,对该bean请求返回的结果都是相同的,如下图
基于xml的配置如下
<beans ...>
<!-- scope属性用于声明bean的作用域,默认值即为singleton -->
<bean id="exampleA" class="cn.example.spring.boke.ExampleA" scope="singleton"></bean>
</beans>
(3) Prototype作用域:如果一个bean的作用域为prototype,那么我们每次对这个bean的请求都会导致Spring会为我们创建出一个全新的bean实例并返回,换句话说,对该bean请求返回的结果都是不同(全新)的,如下图
基于xml的配置如下
<beans ...>
<!-- scope属性用于声明bean的作用域-->
<bean id="exampleA" class="cn.example.spring.boke.ExampleA" scope="prototype"></bean>
</beans>
注意:最好对无状态的bean采用singleton模式,而对有状态的bean采用prototype模式,此外,与其他作用域的bean相比,Spring不会管理prototype bean的完整生命周期,即Spring只管prototype bean的创建,不管它的销毁,prototype bean的初始化回调会被Spring调用,但它的销毁回调却不会,因此,我们在使用prototype bean时,必须清理其所拥有的资源,防止内存泄漏(清理方式:通过自定义bean后置处理器)
(4) request,session,application与websocket这4种作用域只能在web环境中使用,否则会抛出异常,此外,如果当前的servlet环境是Spring MVC环境且请求均通过DispatcherServlet进行处理,那么无需任何其他配置,直接就可以使用这4种作用域,否则,需要进行一些特殊的配置,具体可参考官方文档,此处略
(5) 当我们想要将一个短作用域(例如:request)的bean注入到一个长作用域(例如:singleton)的bean中时,可选择注入这个短作用域bean的AOP代理对象,这是因为通常容器只初始化一次,因此singleton bean的依赖项也只会被注入一次,从而我们所获得的依赖项至始至终都是相同的,在这种情况下,我们就需要一个代理对象,在每次请求时都让这个代理去获取实际对象并进行方法委托,从而执行正确的方法调用,如下
<beans ...>
<bean id="exampleA" class="cn.example.spring.boke.ExampleA" scope="request">
<!--添加如下标签后,Spring会创建出该对象的代理对象-->
<aop:scoped-proxy />
</bean>
<bean id="exampleB" class="cn.example.spring.boke.ExampleB" scope="singleton">
<!-- 此时,这里注入的不再是exampleA,而是exampleA的代理对象,每次调用方法,其实都是调用代理对象上的方法,然后代理对象再去获取真正的bean,执行方法调用,这样就避免了依赖项exampleA至始至终都是同一个bean -->
<property name="exampleA" ref="exampleA"></property>
</bean>
</beans>
注意:在默认情况下,Spring会为含有aop:scoped-proxy标签的bean采用CGLIB来创建代理对象,如果不想通过这种方式创建代理对象,可通过指定标签aop:scoped-proxy中的属性proxy-target-class为false,来创建基于JDK的代理对象,如下
<beans ...>
<bean id="exampleA" class="cn.example.spring.boke.ExampleA" scope="request">
<!--创建基于JDK的动态代理对象-->
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
</beans>
2.自定义bean的作用域
(1) bean的作用域是支持扩展的,我们可以自定义作用域,或覆盖spring内置的作用域(其中singleton和prototype不能覆盖),如果要自定义bean的作用域,需要实现Scope接口并将其注入进容器内,便可直接进行使用了,如下所示
//自定义bean的作用域:线程域,即每个线程所获得bean是不同的对象
//首先需实现Scope接口,它主要提供了5大方法,用于获取或设置bean,如下
public class ThreadScope implements Scope {
//使用ThreadLocal来保存线程私有变量
private final ThreadLocal<Map<String, Object>> threadBeanMap = new ThreadLocal() {
@Override
protected Object initialValue() {
return new HashMap<>();
}
};
/**
* 用于从当前作用域(此例中是线程)中检索bean
* @param s 需检索的bean的名称
* @param objectFactory 如果检索失败,就用它来创建一个bean
* @return
*/
@Override
public Object get(String s, ObjectFactory<?> objectFactory) {
Object bean = threadBeanMap.get().get(s);
if(null == bean) {
bean = objectFactory.getObject();
threadBeanMap.get().put(s, bean);
}
return bean;
}
/**
* 从当前作用域中移除bean
* @param s 需移除的bean的名称
* @return
*/
@Override
public Object remove(String s) {
return threadBeanMap.get().remove(s);
}
/**
* 用于注册销毁回调,当bean作用域销毁时的清理工作
* @param s
* @param runnable
*/
@Override
public void registerDestructionCallback(String s, Runnable runnable) {
}
/**
* 用于解析该作用域的上下文,返回该作用域的一些属性
* @param s
* @return
*/
@Override
public Object resolveContextualObject(String s) {
return null;
}
/**
* 用于获取该作用域的标识符
* @return
*/
@Override
public String getConversationId() {
return null;
}
}
<!-- xml文件配置 -->
<beans ...>
<!-- 注意这里的scope属性采用了我们自定义的作用域,其名称来源于我们的注册 -->
<bean id="exampleA" class="cn.example.spring.boke.ExampleA" scope="thread"></bean>
</beans>
//测试
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("boke/from.xml");
//创建出我们自定义的作用域对象
Scope threadScope = new ThreadScope();
//使用registerScope方法将自定义的作用域对象注入进容器中,同时指定作用域的名称,在上面的xml配置文件中,就是使用的该名称
ctx.getBeanFactory().registerScope("thread", threadScope);
for (int i = 0; i < 2; i++) {
new Thread(() -> {
//开启2个线程,可以发现每个线程内的bean都是不同的对象
System.out.println(Thread.currentThread().getName() + " " + ctx.getBean("exampleA"));
}).start();
}
}
除了上面编程式的配置之外,我们还可以使用基于xml的配置来向容器中注册我们自定义作用域,如下
<beans ...>
<!-- 注册我们自定义作用域 -->
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<!-- 使用entry标签,其中的key代表自定义作用域的名称,值为自定义作用域对象 -->
<entry key="thread">
<bean class="cn.example.spring.boke.scope.ThreadScope"/>
</entry>
</map>
</property>
</bean>
<bean id="exampleA" class="cn.example.spring.boke.ExampleA" scope="thread"></bean>
</beans>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具