从自定义一个作用域开始来了解SpringBean的作用域

你好,这里是codetrend专栏"Spring6全攻略"。

在 Spring 框架中,Bean 的作用域(Scope)定义了 Bean 实例在容器中如何创建、管理和销毁的策略。

Spring 提供了多种 Bean 作用域,每种作用域都有其特定的生命周期和适用场景。

先试试不同的 Bean Scope

下面通过一个简单的 Spring MVC Controller 示例来感受下 Bean 的作用域。

例子代码是这样的:

java 复制代码
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 代码:

java 复制代码
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(单例)的属性持续增加,也就是说访问到的SingletonBean每次都是同一个对象。访问/singleton接口的返回是这样的:
shell 复制代码
1
2
3
  • Prototype(原型)的属性每次都是不一样的,也就是说明id每次都是调用构造器新创建的。访问/prototype接口的返回是这样的:
shell 复制代码
Prototype ID: 3ea5af10-ddce-4a89-ad3c-3f07a764f179
Prototype ID: 7e6e9fe8-c0dc-423e-b282-96b7f8087dac
Prototype ID: 7aca1000-484d-46e8-80f7-d444a5a04f49
  • Session(会话)的属性同一窗口是一样的,开启无痕窗口或者其他浏览器就不一样。访问/session接口的返回是这样的:
shell 复制代码
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 Scope

接下来通过实现一个自定义作用域来感受下Bean的作用域原理。

在 Spring 框架中,除了预定义的几种作用域(如 singleton、prototype 等)外,用户还可以自定义作用域以满足特定的业务需求。

自定义作用域允许控制 Bean 的创建、缓存和销毁逻辑,以适应特定的场景,如基于特定条件的实例化策略、自定义生命周期管理等。

自定义步骤:

  • 定义作用域接口 :首先,需要实现org.springframework.beans.factory.config.Scope接口,该接口定义了 Bean 作用域的基本行为。
  • 实现逻辑 :在自定义的 Scope 接口实现中,需要覆盖getremoveregisterDestructionCallback方法,分别用于获取 Bean 实例、移除 Bean 实例以及注册销毁回调。
  • 注册作用域:在 Spring 配置中注册的自定义作用域,使其可被容器识别和使用。
  • 使用自定义作用域 :在 Bean 定义中通过@Scope注解指定使用自定义的作用域名称。

自定义作用域实现

首先自定义作用域实现 ,也就是实现接口org.springframework.beans.factory.config.Scope

java 复制代码
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和销毁回调等逻辑。这也是作用域的核心原理。

Spring6怎么实现的Scope

这里以org.springframework.web.context.request.RequestScope 为例子来理解Spring6怎么实现BeanScope的。

得益于Spring框架的抽象和封装,这个类的实现代码并没有多少。

  • RequestScope extends AbstractRequestAttributesScope 核心实现在这个类 AbstractRequestAttributesScope

  • get获取对象方法,其中对象的存储放在了ThreadLocal中,也就是RequestContextHolder这个类的核心。

java 复制代码
/**
 * 根据名称获取对象,如果当前请求属性中没有该对象,则使用对象工厂创建一个对象,并将其设置到请求属性中
 * 然后再次获取该对象,以便进行隐式会话属性更新。作为额外的好处,我们还允许在获取属性级别进行潜在的装饰。
 * 如果再次获取到的对象不为空(预期情况),则只使用该对象。如果它同时消失了,我们则返回本地创建的实例。
 */
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中的对象移除。
java 复制代码
/**
 * 移除指定名称的对象,如果当前请求属性中存在该对象,则将其从请求属性中移除并返回该对象;否则返回 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方法进行注册。

java 复制代码
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容器中,并使用自定义作用域。

java 复制代码
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实例。

java 复制代码
@RestController
public class CustomScopeController {
    @Autowired
    private ApplicationContext context;
    @GetMapping("/customScope")
    public String customScope() {
        MyScopeBean prototypeBean = context.getBean(MyScopeBean.class);
        return "Prototype ID: " + prototypeBean.getId();
    }
}

访问的结果输出如下:

shell 复制代码
Session ID: bd22d310-29e5-4004-8555-9678c08275f0
Session ID: bd22d310-29e5-4004-8555-9678c08275f0
Session ID: bd22d310-29e5-4004-8555-9678c08275f0

因为对象全局缓存到了一个MapscopedObjects,所以可以看到这个自定义作用域效果和单例模式基本一致的。

Bean Scope 的分类

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 上下文中有效。

其中singletonprototype是比较常用的数据。

Bean Scope 的使用

可以通过在Spring的配置文件(如XML配置文件或Java注解)中指定@Scope注解或<bean>元素的scope属性来定义Bean的Scope。

其中@Scope注解可以是自定义的值或者如下常量:

  • ConfigurableBeanFactory.SCOPE_PROTOTYPE
  • ConfigurableBeanFactory.SCOPE_SINGLETON
  • org.springframework.web.context.WebApplicationContext.SCOPE_REQUEST
  • org.springframework.web.context.WebApplicationContext.SCOPE_SESSION

其中ConfigurableBeanFactory.SCOPE_PROTOTYPE是默认值。

例如:

java 复制代码
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配置:

xml 复制代码
<bean id="myBean" class="com.example.MyBean" scope="prototype">
    <!-- Bean的其他配置 -->
</bean>

选择合适的Bean Scope取决于应用程序的需求。

为什么设计 Bean Scope

Spring 框架设计 Bean 作用域(Scope)的原因主要是为了提供灵活性和资源管理能力,以适应不同应用场景的需求。

不同的 Bean 作用域会影响 Bean 的生命周期、创建方式和在容器中的共享程度,从而影响应用的性能、内存占用和并发处理能力。

以下是 Spring 提供 Bean 作用域设计背后的主要原因:

  • 资源优化:通过作用域设计,Spring 能够根据业务场景高效管理 Bean 的创建与销毁。例如,单例(Singleton)模式可以减少频繁创建实例的开销,原型(Prototype)模式则确保每次请求都得到新的实例,避免了共享状态问题。
  • 并发处理:对于 Web 应用,特定作用域如请求(Request)和会话(Session)使得每个用户请求或会话都有独立的 Bean 实例,解决了并发用户数据隔离的问题,提高了应用的线程安全。
  • 生命周期管理:不同的作用域允许开发者控制 Bean 的生命周期,比如通过自定义作用域实现复杂的生命周期管理逻辑。Spring 容器在 Bean 的创建、初始化、销毁等关键时刻调用生命周期回调方法,增加了灵活性。
  • 可测试性:通过作用域的设计,特别是原型模式,可以更容易地创建独立的测试环境,因为每次测试都能得到全新的实例,减少了测试间状态干扰。
  • 扩展性:Spring 允许开发者自定义作用域,为特定的业务需求或架构设计提供定制化的 Bean 管理方式,增强了框架的扩展性和适应性。
  • 内存管理:合理使用作用域可以减少内存消耗,例如,原型模式避免了单例 Bean 累积大量状态导致的内存泄漏风险,而请求作用域则确保请求结束后自动清理资源。

单例 bean 里面注入了原型 bean

当单例 Bean 中注入原型(Prototype)Bean 时,会出现一个问题:

  • 单例 Bean 在整个应用生命周期中只创建一次。
  • 而原型 Bean 本应每次请求时创建新实例。
  • 但直接注入到单例 Bean 中时,实际上只会注入一次原型 Bean 的实例。
  • 后续对该原型 Bean 的使用都将复用首次注入的同一个实例,这可能并不符合预期。

以下demo可以复现这种情况。

SpringBean的配置:

java 复制代码
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:

java 复制代码
import java.util.UUID;

public class PrototypeInjectBean {
    private String id;

    public PrototypeInjectBean() {
        this.id = UUID.randomUUID().toString();
    }

    public String getId() {
        return id;
    }
}

测试代码如下:

java 复制代码
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值始终是相同的。

shell 复制代码
Prototype ID: 11893ed8-608c-452b-b2e6-82bc70b5cf97
Prototype ID: 11893ed8-608c-452b-b2e6-82bc70b5cf97
Prototype ID: 11893ed8-608c-452b-b2e6-82bc70b5cf97

那这种常用的使用场景遇到了该怎么解决呢?别急,Spring早已经给出了几种解决办法。

通过完善上面的测试代码给出3中解决方法。

修改完善后的代码如下:

java 复制代码
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();

}
  • 解决办法1: Spring 提供了ObjectProvider接口(继承自Provider接口),它允许延迟查找和实例化 Bean,非常适合在单例 Bean 中按需获取原型 Bean 的新实例。

通过访问接口/prototypeDemo4可以发现每次返回的id值是不同的。

  • 解决办法2: 可以通过定义一个工厂方法来创建原型 Bean 的实例,然后在单例 Bean 中注入这个工厂方法,每次需要时调用工厂方法获取新实例。

通过访问接口/prototypeDemo2可以发现每次返回的id值是不同的。

  • 解决办法3: 通过@Lookup注解,@Lookup注解是Spring框架中的一个特殊注解,用于在Spring容器中查找另一个Bean,并将其注入到当前Bean中。注意使用@Lookup注解的方法必须是抽象的(abstract)。

通过访问接口/prototypeDemo5可以发现每次返回的id值是不同的。

关于作者

来自一线全栈程序员nine的探索与实践,持续迭代中。

欢迎关注或者点个小红心~

相关推荐
山海上的风2 小时前
Spring Batch终极指南:原理、实战与性能优化
spring·性能优化·batch·springbatch
找不到、了3 小时前
Spring的Bean原型模式下的使用
java·spring·原型模式
超级小忍4 小时前
Spring AI ETL Pipeline使用指南
人工智能·spring
Boilermaker19927 小时前
【Java EE】SpringIoC
前端·数据库·spring
写不出来就跑路7 小时前
Spring Security架构与实战全解析
java·spring·架构
sleepcattt8 小时前
Spring中Bean的实例化(xml)
xml·java·spring
小七mod8 小时前
【Spring】Java SPI机制及Spring Boot使用实例
java·spring boot·spring·spi·双亲委派
ruan1145149 小时前
Java Lambda 类型推断详解:filter() 方法与 Predicate<? super T>
java·开发语言·spring·stream
paopaokaka_luck10 小时前
基于SpringBoot+Vue的非遗文化传承管理系统(websocket即时通讯、协同过滤算法、支付宝沙盒支付、可分享链接、功能量非常大)
java·数据库·vue.js·spring boot·后端·spring·小程序
邓不利东14 小时前
Spring中过滤器和拦截器的区别及具体实现
java·后端·spring