红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

Spring 源码学习 5:Scope

2025年6月22日 7点热度 0人点赞 0条评论

Scope

Spring 的 bean 是有作用域(Scope)的:

  • singleton:单例

  • prototype:原型,每次都获取到新的实例。

  • request:请求,在一次 HTTP 请求内有效。

  • session:会话,在一次 HTTP 会话内有效。

  • application:应用,在一个 Servlet 容器中有效。

默认的作用域是单例。

最后三种作用域都与 Web 应用相关,可以用一个简单的基于 Spring Boot 的 Web 应用观察其创建和销毁。

定义三种不同作用域的 bean:

@Scope(WebApplicationContext.SCOPE_APPLICATION)
@Component
@Slf4j
public class ApplicationBean {
    @PreDestroy
    public void destroy() {
        log.info("destroy");
    }
}
@Scope(WebApplicationContext.SCOPE_REQUEST)
@Component
@Slf4j
public class RequestBean {
    @PreDestroy
    public void destroy() {
        log.info("destroy");
    }
}
@Scope(WebApplicationContext.SCOPE_SESSION)
@Component
@Slf4j
public class SessionBean {
    @PreDestroy
    public void destroy() {
        log.info("destroy");
    }
}

创建一个 Controller 以观察这些 bean 的创建和销毁:

@Controller
@RequestMapping("/scope")
public class ScopeController {
    @Lazy
    @Autowired
    private RequestBean requestBean;
    @Lazy
    @Autowired
    private SessionBean sessionBean;
    @Lazy
    @Autowired
    private ApplicationBean applicationBean;
​
    @GetMapping
    @ResponseBody
    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注解,原因之后会说明。

访问页面 localhost:8080/scope 会看到每次请求都会创建一个新的 requestBean 对象。而 sessionBean 和 applicationBean 对象则不是。

作用域失效

在某些情况下,作用域会失效。比如一个单例 bean 注入了一个原型 bean 的依赖:

@Component
static class SingletonBean{
    @Autowired
    @Getter
    private PrototypeBean prototypeBean;
}
@Component
@Scope(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注解:

@Component
static class SingletonBean{
    @Autowired
    @Getter
    @Lazy
    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):

@Component
@Scope(value = AbstractBeanDefinition.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
static class PrototypeBean{}

这里将作用域的代理模式指定为ScopedProxyMode.TARGET_CLASS,这样就会为这个 Bean 启用 Spring CGLIB 动态代理。因此注入的部分不需要使用@Lazy,只需要像注入普通单例 bean 一样:

@Component
static class SingletonBean{
    @Getter
    @Autowired
    private PrototypeBean prototypeBean;
}

ObjectFactory

注入ObjectFactory而非原始类型:

@Component
static class SingletonBean{
    @Autowired
    private ObjectFactory<PrototypeBean> prototypeBeanFactory;
​
    @SneakyThrows
    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 实例,而不是使用对象工厂:

@Component
static class SingletonBean{
    @Autowired
    private ApplicationContext ctx;
​
    public PrototypeBean getPrototypeBean(){
        return ctx.getBean(PrototypeBean.class);
    }
}

测试用例与方式三相同,不再赘述。

不过这样做的坏处是让代码与 Spring 容器强绑定,不符合 Spring 框架的解耦思想,让代码无法迁移到其它类型的容器,不过一般的代码也不会考虑这种问题就是了。

本文的完整示例代码可以i从这里获取。

The End.

参考资料

  • 黑马程序员Spring视频教程,深度讲解spring5底层原理

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: scope spring
最后更新:2025年6月22日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号