玩转Spring各种作用域Bean Scope及源码分析

2024年 1月 5日 79.0k 0

环境:Spring5.3.23

一. 简介

Spring Scope Bean是Spring用于管理Bean的作用域的一种机制。它定义了容器中Bean的生命周期和实例化策略,即如何创建Bean实例。

在Spring中,Bean的作用域包括单例(singleton)、原型(prototype)、请求(request)、会话(session)等。每个作用域都有其特定的使用场景和行为:

  • 单例(singleton):这是Spring默认的作用域,表示在整个Spring容器中,只有一个Bean实例存在。无论你从哪个地方获取这个Bean,都将返回同一个实例。
  • 原型(prototype):每次从容器中请求Bean时,都会创建一个新的Bean实例。
  • 请求(request):在一个HTTP请求的范围内,Bean是单例的。这种作用域适用于与单个请求关联的Bean。
  • 会话(session):在一个HTTP会话的范围内,Bean是单例的。这种作用域适用于与单个用户会话关联的Bean。
  • 此外,Spring还提供了其他一些作用域应用(Application)、WebSocket,以满足不同场景的需求。

    通过合理地选择Bean的作用域,可以优化应用的性能和资源利用率。例如,对于需要频繁创建和销毁实例的Bean,使用原型作用域会更高效;而对于需要在多个请求或会话之间共享状态的Bean,则可以选择单例或会话作用域。附官方图:

    图片图片

    接下来将分别介绍每一种作用域bean。

    二. 作用域应用

    基础类

    static class Person {
      @Override
      public String toString() {
        return super.toString() + " - " + this.hashCode() + "" ;
      }
    }

    2.1 单例(singleton)

    默认使用@Bean,@Service,@Controller注解标注的注解都是单例的。也可以同@Scope注解指定作用域为单例

    @Bean
    // 不指定@Scope默认就是单例
    @Scope(value = "singleton")
    public Person person() {
      return new Person() ;
    }

    测试

    try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
      context.registerBean(Config.class) ;
      context.refresh() ;
      
      System.out.println(context.getBean(Person.class)) ;
      System.out.println(context.getBean(Person.class)) ;
    }

    控制台输出

    com.pack.main.scope.ScopeMain5$Person@5e0e82ae - 1578009262
    com.pack.main.scope.ScopeMain5$Person@5e0e82ae - 1578009262

    每次获取的都是同一个实例。

    原理

    public abstract class AbstractBeanFactory {
      protected  T doGetBean(...) {
        // ...
        // 判断是否是单例
        if (mbd.isSingleton()) {
          // 先从单例池中查找是否已经存在,不存在则调用createBean创建,
          // 然后存入单例池中
          sharedInstance = getSingleton(beanName, () -> {
            try {
              return createBean(beanName, mbd, args);
            }
          });
        }
        // ...
      }
    }

    2.2 原型(prototype)

    每次从容器中请求Bean时,都会创建一个新的Bean实例。

    @Bean
    @Scope(value = "prototype")
    public Person person() {
      return new Person() ;
    }

    控制台输出

    com.pack.main.scope.ScopeMain5$Person@fa4c865 - 262457445
    com.pack.main.scope.ScopeMain5$Person@3bd82cf5 - 1004023029

    每次获取都是不同的对象。

    原理

    public abstract class AbstractBeanFactory {
      protected  T doGetBean(...) {
        // ...
        // 判断是否是单例
        if (mbd.isSingleton()) {
          // ...
        }
        // 判断是否是原型
        else if (mbd.isPrototype()) {
          Object prototypeInstance = null;
          try {
            // 不存在什么缓存池,直接创建bean实例返回
            prototypeInstance = createBean(beanName, mbd, args);
          }
        }
        // ...
      }
    }

    这里考虑一个问题,如何在单例bean中正确的注入原型bean?

    2.3 请求(request)

    接下来都是与web环境相关了,所以这里演示的示例会以SpringBoot3.0.5环境演示。

    基础类

    @Component
    @Scope(value = "request")
    public class Person {
    }

    测试类

    @RestController
    @RequestMapping("/scopes")
    public class ScopeController {
      @Resource
      private Person person ;
      @Resource
      private PersonService ps ;
      @GetMapping("/request")
      public Person request() {
        System.out.println("ScopeController: " + person) ;
        ps.query() ;
        return person ;
      }
    }

    Service

    @Service
    public class PersonService {
      @Resource
      private Person person ;
      public void query() {
        System.out.println("PersonService: " + person) ;
      }
    }

    如果上面这样配置,启动服务将会报错:

    Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
      at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131) ~[spring-web-6.0.7.jar:6.0.7]

    该错误的原因就是你在一个单例bean中注入一个request作用域的bean,而request作用域bean的生命周期是在一个web请求开始创建的,所以这里你当然是没法注入的。

    解决办法:

    • @Scope设置代理模式
    @Component
    @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public class Person {}

    测试结果

    ScopeController: com.pack.scopes.Person@106a9684 - 275420804
    PersonService: com.pack.scopes.Person@106a9684 - 275420804
    ScopeController: com.pack.scopes.Person@64396678 - 1681483384
    PersonService: com.pack.scopes.Person@64396678 - 1681483384

    每次请求接口都获取的不是同一个实例。并且在一个完整的请求中获取的Person都是同一个。

    • 使用@RequestScope

    该注解原理与上面其实一致的

    @Scope(WebApplicationContext.SCOPE_REQUEST)
    public @interface RequestScope {
      @AliasFor(annotation = Scope.class)
      // 设置好了使用代理
      ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
    }

    2.4 会话(session)

    @Component
    @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
    // 与request一样,必须设置代理模式或者使用下面这个注解
    // @SessionScope
    public class Person {}

    测试

    ScopeController: com.pack.scopes.Person@2b56038d - 727057293
    PersonService: com.pack.scopes.Person@2b56038d - 727057293
    ScopeController: com.pack.scopes.Person@2b56038d - 727057293
    PersonService: com.pack.scopes.Person@2b56038d - 727057293

    多次访问都是同一个session;你再换个浏览器访问

    ScopeController: com.pack.scopes.Person@1aa201fd - 446824957
    PersonService: com.pack.scopes.Person@1aa201fd - 446824957
    ScopeController: com.pack.scopes.Person@1aa201fd - 446824957
    PersonService: com.pack.scopes.Person@1aa201fd - 446824957

    此时对象就是一个新的了,不同的浏览器访问当然不是同一个session了。

    2.5 应用(application)

    @Scope(value = "application", proxyMode = ScopedProxyMode.TARGET_CLASS)
    // @ApplicationScope
    // 都是web环境,所以情况都一样
    public class Person {}

    测试

    360浏览器

    ScopeController: com.pack.scopes.Person@6371b4b6 - 1668396214
    PersonService: com.pack.scopes.Person@6371b4b6 - 1668396214

    Chrome浏览器

    ScopeController: com.pack.scopes.Person@6371b4b6 - 1668396214
    PersonService: com.pack.scopes.Person@6371b4b6 - 1668396214

    他们是同一个对象,application作用域生命周期与整个应用一样,只有你关闭了服务器,在启动后才会是再重新创建的bean对象。

    3. web作用域原理

    3.1 注册作用域

    public abstract class AbstractApplicationContext {
      public void refresh() {
        postProcessBeanFactory(beanFactory);
      }
    }
    public class AnnotationConfigServletWebServerApplicationContext {
      protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        super.postProcessBeanFactory(beanFactory);
      }
    }
    public class ServletWebServerApplicationContext {
      protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        // ...
        registerWebApplicationScopes();
      }
      private void registerWebApplicationScopes() {
        WebApplicationContextUtils.registerWebApplicationScopes(getBeanFactory());
      }
    }
    public abstract class WebApplicationContextUtils {
      public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory) {
        registerWebApplicationScopes(beanFactory, null);
      }
      public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory,
          @Nullable ServletContext sc) {
        // 注册作用域
        beanFactory.registerScope(WebApplicationContext.SCOPE_REQUEST, new RequestScope());
        beanFactory.registerScope(WebApplicationContext.SCOPE_SESSION, new SessionScope());
        if (sc != null) {
          ServletContextScope appScope = new ServletContextScope(sc);
          beanFactory.registerScope(WebApplicationContext.SCOPE_APPLICATION, appScope);
        }
      }
    }

    这里每一种web作用域都有一个对应的Scope实现RequestScope,SessionScope,ServletContextScope。

    3.2 查找web作用域bean

    public abstract class AbstractBeanFactory {
      protected  T doGetBean(...) {
        // ...
        // 判断是否是单例
        if (mbd.isSingleton()) {
          // ...
        }
        // 判断是否是原型
        else if (mbd.isPrototype()) {
          Object prototypeInstance = null;
          try {
            // 不存在什么缓存池,直接创建bean实例返回
            prototypeInstance = createBean(beanName, mbd, args);
          }
        }
        // 其它作用域bean,如上面的web作用域
        else {
          String scopeName = mbd.getScope();
          Scope scope = this.scopes.get(scopeName);
          if (scope == null) {
            throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
          }
          try {
              // 通过具体Scope的实现类获取bean对象
            Object scopedInstance = scope.get(beanName, () -> {
              beforePrototypeCreation(beanName);
              try {
                // 首次都还是会创建
                return createBean(beanName, mbd, args);
                }
              });
            }
          }
        }
        // ...
      }
    }

    总结:Spring Scope Bean是Spring框架中用于管理Bean的作用域的机制,它定义了Bean的生命周期和实例化策略。通过合理地选择Bean的作用域,可以优化应用的性能和资源利用率。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论