Scope
Spring 的 bean 是有作用域(Scope)的:
-
singleton:单例
-
prototype:原型,每次都获取到新的实例。
-
request:请求,在一次 HTTP 请求内有效。
-
session:会话,在一次 HTTP 会话内有效。
-
application:应用,在一个 Servlet 容器中有效。
默认的作用域是单例。
最后三种作用域都与 Web 应用相关,可以用一个简单的基于 Spring Boot 的 Web 应用观察其创建和销毁。
定义三种不同作用域的 bean:
WebApplicationContext.SCOPE_APPLICATION)
(
public class ApplicationBean {
public void destroy() {
log.info("destroy");
}
}
WebApplicationContext.SCOPE_REQUEST)
(
public class RequestBean {
public void destroy() {
log.info("destroy");
}
}
WebApplicationContext.SCOPE_SESSION)
(
public class SessionBean {
public void destroy() {
log.info("destroy");
}
}
创建一个 Controller 以观察这些 bean 的创建和销毁:
"/scope")
(public class ScopeController {
private RequestBean requestBean;
private SessionBean sessionBean;
private ApplicationBean applicationBean;
public String index() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("<ul>");
stringBuilder.append("<li>%s</li>".formatted(requestBean));
stringBuilder.append("<li>%s</li>".formatted(sessionBean));
stringBuilder.append("<li>%s</li>".formatted(applicationBean));
stringBuilder.append("</ul>");
return stringBuilder.toString();
}
}
注意,这里注入时使用
@Lazy
注解,原因之后会说明。
访问页面 会看到每次请求都会创建一个新的 requestBean 对象。而 sessionBean 和 applicationBean 对象则不是。
作用域失效
在某些情况下,作用域会失效。比如一个单例 bean 注入了一个原型 bean 的依赖:
static class SingletonBean{
private PrototypeBean prototypeBean;
}
AbstractBeanDefinition.SCOPE_PROTOTYPE)
(static class PrototypeBean{}
我们可能希望每次调用单例 bean 的 getXXX 方法都获取一个新的原型 bean 实例,但实际上并不会。
SingletonBean bean = applicationContext.getBean(SingletonBean.class);
PrototypeBean prototypeBean1 = bean.getPrototypeBean();
PrototypeBean prototypeBean2 = bean.getPrototypeBean();
PrototypeBean prototypeBean3 = bean.getPrototypeBean();
log.info(prototypeBean1.toString());
log.info(prototypeBean2.toString());
log.info(prototypeBean3.toString());
Assertions.assertSame(prototypeBean1, prototypeBean2);
Assertions.assertSame(prototypeBean2, prototypeBean3);
执行上面的测试用例就会发现,打印的三个PrototypeBean
对象是同一个对象。
这是因为依赖注入只会发生一次,在依赖注入时的确会获取一个新的PrototypeBean
实例并注入,但之后通过 Getter 方法获取到的都是同一个实例的引用,而非期望的每次获取到新实例。
有几种方式可以解决这个问题。
@Lazy
方式一是注入目标类型的代理对象而非原始对象,这样对所有注入后的对象的调用都会经过代理对象,而代理对象就会每次都产生一个新的实例并调用相应的方法。
我们都知道,使用@Lazy
注解可以延迟绑定,也就是在依赖注入时注入代理对象,以实现对象的延迟绑定。因此,注入代理对象的最简单方式是使用@Lazy
注解:
static class SingletonBean{
private PrototypeBean prototypeBean;
}
其它代码无需修改。
值得注意的是,测试用例:
log.info(prototypeBean1.toString());
log.info(prototypeBean2.toString());
log.info(prototypeBean3.toString());
Assertions.assertSame(prototypeBean1, prototypeBean2);
Assertions.assertSame(prototypeBean2, prototypeBean3);
打印的对象显示是三个不同的对象:
cn.icexmoon.demo.ScopeTests2$PrototypeBean@43034809 cn.icexmoon.demo.ScopeTests2$PrototypeBean@23202c31 cn.icexmoon.demo.ScopeTests2$PrototypeBean@4f824872
但断言显示三个对象是同一个对象。
事实上这恰恰说明了前面所说的,这里获取到的prototypeBean1
、prototypeBean2
、prototypeBean3
是同一个代理对象,只不过每次调用其toString
方法时会创建不同的PrototypeBean
对象实例并调用其toString
方法。
可以打印并观察其真实类型:
log.info(prototypeBean1.getClass().toString());
输出:
class cn.icexmoon.demo.ScopeTests2$PrototypeBean$$SpringCGLIB$$0
可以看到,这是 SpringCGLIB (动态代理)创建的一个代理对象。
proxyMode
第二种方式与第一种方式本质是相同的,只是写法不同。
修改PrototypeBean
的作用域定义(@Scope
):
value = AbstractBeanDefinition.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
(static class PrototypeBean{}
这里将作用域的代理模式指定为ScopedProxyMode.TARGET_CLASS
,这样就会为这个 Bean 启用 Spring CGLIB 动态代理。因此注入的部分不需要使用@Lazy
,只需要像注入普通单例 bean 一样:
static class SingletonBean{
private PrototypeBean prototypeBean;
}
ObjectFactory
注入ObjectFactory
而非原始类型:
static class SingletonBean{
private ObjectFactory<PrototypeBean> prototypeBeanFactory;
public PrototypeBean getPrototypeBean(){
return prototypeBeanFactory.getObject();
}
}
prototypeBeanFactory.getObject
方法可以帮我们创建具体的实例。因此这里每次调用getPrototypeBean
方法都会获取到一个新的PrototypeBean
实例。
测试用例:
SingletonBean bean = applicationContext.getBean(SingletonBean.class);
PrototypeBean prototypeBean1 = bean.getPrototypeBean();
PrototypeBean prototypeBean2 = bean.getPrototypeBean();
PrototypeBean prototypeBean3 = bean.getPrototypeBean();
log.info(prototypeBean1.toString());
log.info(prototypeBean2.toString());
log.info(prototypeBean3.toString());
Assertions.assertNotSame(prototypeBean1, prototypeBean2);
Assertions.assertNotSame(prototypeBean2, prototypeBean3);
log.info(prototypeBean1.getClass().toString());
注意,这里使用了assertNotSame
,因为获取到的对象不再是代理对象而是目标类型的新实例。
ApplicationContext
方式四和方式三本质上是一样的,不同的是这里直接获取容器,并通过容器获取 Bean 实例,而不是使用对象工厂:
static class SingletonBean{
private ApplicationContext ctx;
public PrototypeBean getPrototypeBean(){
return ctx.getBean(PrototypeBean.class);
}
}
测试用例与方式三相同,不再赘述。
不过这样做的坏处是让代码与 Spring 容器强绑定,不符合 Spring 框架的解耦思想,让代码无法迁移到其它类型的容器,不过一般的代码也不会考虑这种问题就是了。
本文的完整示例代码可以i从获取。
The End.
参考资料
文章评论