Spring5中文文档【3】- IOC容器之Bean作用域

1. Bean作用域

本系列基于最新5.3.10版本,大部分内容copy于官方文档…
官方文档地址

创建 bean definition 时,您创建了一个配方,用于创建由该 bean definition 定义的类的实际实例。bean definition是一个配方的想法很重要,因为这意味着,与类一样,您可以从单个配方创建许多对象实例。

您不仅可以控制要插入到从特定 bean definition创建的对象中的各种依赖项和配置值,还可以控制从特定 bean definition 创建的对象的范围。这种方法功能强大且灵活,因为您可以通过配置选择您创建的对象的范围,而不必在 Java 类级别设置对象的范围。可以将 Bean 定义为部署在多个范围之一中。

Spring Framework 支持六个范围,其中四个仅在您使用web程序时的ApplicationContext时才可用。您还可以创建自定义范围。

从 Spring 3.0 开始,线程作用域可用,但默认情况下未注册。

下表描述了支持的范围:

范围 描述
singleton (默认)将单个 bean 定义范围限定为每个 Spring IoC 容器的单个对象实例。
prototype 将单个 bean 定义范围限定为任意数量的对象实例。
request 将单个 bean 定义范围限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有自己的 bean 实例,该 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网络套接字。

1.1 Singleton (单例)

只有一个单例 bean 的共享实例被管理,对当前bean 的请求都会导致 Spring 容器返回一个特定的相同的 bean 实例。

换句话说,当您定义一个 bean definition 并且它的作用域是一个单例时,Spring IoC 容器会创建该 bean definition 定义的对象的一个​​实例。该单个实例存储在此类单例 bean 的缓存中,并且对该命名 bean 的所有后续请求和引用都返回缓存对象。下图显示了单例范围的工作原理:

在这里插入图片描述

Spring 的单例 bean 概念不同于 (GoF) 模式中定义的单例模式。GoF 单例对对象的范围进行了硬编码,以便每个 ClassLoader 只创建一个特定类的一个实例。Spring 单例的范围最好描述为每个容器和每个 bean。这意味着,如果您在单个 Spring 容器中为特定类定义一个 bean,则 Spring 容器会创建该 bean 定义定义的类的一个且仅一个实例。单例作用域是 Spring 中的默认作用域。

要将 bean 定义为 XML 中的单例,您可以定义一个 bean,如下例所示:

<bean id="accountService" class="com.something.DefaultAccountService"/>

<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

1.2 Prototype (多例)

Prototype 范围导致每次对特定 bean 发出请求时都会创建一个新 bean 实例。也就是说,bean 被注入到另一个 bean 中,或者您通过getBean()容器上的方法调用来请求它。

通常,您应该对所有有状态 bean 使用Prototype 作用域,对无状态 bean 使用单例作用域。

下图说明了 Spring 多例范围:
在这里插入图片描述
数据访问对象 (DAO) 通常不配置为Prototype ,因为典型的 DAO 不保存任何对话状态。我们更容易重用单例的核心。

以下示例将 bean 定义为 XML 中的Prototype 范围:

<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>

与其他作用域相比,Spring 不管理Prototype bean 的完整生命周期。容器实例化、配置和以其他方式组装原型对象并将其交给客户端,没有该实例的进一步记录。

因此,尽管在所有对象上调用初始化生命周期回调方法,而不管范围如何,但在Prototype 的情况下,不会调用配置的销毁生命周期回调。客户端代码必须清理原型范围内的对象并释放原型 bean 持有的昂贵资源。要让 Spring 容器释放Prototype 作用域 bean 持有的资源,请尝试使用自定义bean post-processor方法,它保存对需要清理的 bean 的引用。

在某些方面,Spring 容器在Prototype 作用域 bean 方面的角色是 Java new运算符的替代品。超过该点的所有生命周期管理都必须由客户端处理。(有关 Spring 容器中 bean 生命周期的详细信息,请参阅生命周期回调。)

具有 Prototype bean 依赖关系的 Singleton Bean

当您使用具有对Prototype bean 依赖单例作用域 的bean 时,请注意在实例化时解析依赖关系。因此,如果您将Prototype 范围的 bean 依赖注入到单例范围的 bean 中,则会实例化一个新的Prototype bean,然后将依赖项注入到单例 bean 中。Prototype 实例是唯一提供给单例作用域 bean 的实例。

但是,假设您希望单例范围的 bean 在运行时重复获取原型范围的 bean 的新实例。您不能将Prototype 范围的 bean 依赖注入到您的单例 bean 中,因为该注入仅发生一次,当 Spring 容器实例化单例 bean 并解析并注入其依赖项时。如果您在运行时多次需要Prototype bean 的新实例,请参阅方法注入。

1.3. Request、Session、Application和 WebSocket 范围

在request,session,application,和websocket范围只有当你使用一个基于web的Spring容器ApplicationContext实现(例如 XmlWebApplicationContext)。如果将这些作用域与常规 Spring IoC 容器(例如 ClassPathXmlApplicationContext)一起使用,则会抛出抛出未知 bean 作用域IllegalStateException异常 。

2. 自定义作用域

Bean 作用域机制是可扩展的。您可以定义自己的范围,甚至重新定义现有范围,尽管后者被认为是不好的做法,并且您不能覆盖内置范围singleton和prototype范围。

2.1 创建自定义范围

要将自定义范围集成到 Spring 容器中,需要实现org.springframework.beans.factory.config.Scope接口。

该Scope接口有四种方法,可以从作用域中获取对象、从作用域中移除它们、以及让它们被销毁。

例如,会话作用域实现返回会话作用域 bean(如果它不存在,则该方法在将它绑定到会话以供将来参考后返回该 bean 的一个新实例)。以下方法从范围返回对象:

Object get(String name, ObjectFactory<?> objectFactory)

例如,会话范围实现从底层会话中删除会话范围的 bean。应该返回该对象,但如果没有找到具有指定名称的对象,您可以返回null。以下方法从基础范围中删除对象:

Object remove(String name)

以下方法注册了一个回调,当它被销毁或范围中的指定对象被销毁时,该范围应该调用该回调:

void registerDestructionCallback(String name, Runnable destructionCallback)

以下方法获取基础范围的对话标识符:

String getConversationId()

这个标识符对于每个范围都是不同的。对于会话范围的实现,此标识符可以是会话标识符。

2.2 使用自定义范围

在编写并测试一个或多个自定义Scope实现之后,您需要让 Spring 容器知道您的新范围。以下方法是Scope向Spring 容器注册的核心方法:

void registerScope(String scopeName, Scope scope);

此方法在ConfigurableBeanFactory接口上声明,可通过 Spring 附带的BeanFactory大多数具体实现的ApplicationContext属性获得。

该registerScope(…)方法的第一个参数是与作用域关联的唯一名称。Spring 容器本身中此类名称的示例是singleton和 prototype。该registerScope(…)方法的第二个参数是Scope您希望注册和使用的自定义实现的实际实例。

3. 案例演示

因为有些作用域需要Web应用支持,所以这里采用spring boot演示。

3.1 搭建Spring boot工程

使用Spring Initializr初始化一个web工程。
在这里插入图片描述
创建一个测试User类,构造方法使用UUID赋值给userId成员属性,用来判断是否是同一个对象。

@Data
@Component
public class User {
    
    
    String username;
    String password;
    Integer age;
    String name;
    String userId;
    String permissionCode;

    public void test() {
    
    
        System.out.println("test");
    }
    public User() {
    
    
        userId = UUID.randomUUID().toString();
    }
}

创建一个访问接口,获取注入的User类的userId属性。

@RestController
@RequestMapping("/v1")
public class UserController {
    
    

    @Autowired
    User user;
    
    @GetMapping("/test")
    @ResponseBody
    public Object test() {
    
    
        System.out.println(user.getUserId());
        return user.getUserId();
    }
}

3.2 测试

3.2.1 测试Singleton

User类使用注解@Component注入,没有声明作用域,说以默认就是Singleton。

访问接口,发现都返回了同一个ID,说明在IOC容器中,获取到的User类的实例都是同一个。
在这里插入图片描述

3.2.1 测试Prototype

在User类上添加@Scope,并指定其作用域范围为prototype,代理模式proxyMode 为TARGET_CLASS。非单例作用域实例需要指定代理模式。

@Scope(value= ConfigurableBeanFactory.SCOPE_SINGLETON,proxyMode = ScopedProxyMode.TARGET_CLASS)

引入hutool工具包。

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.13</version>
        </dependency>

在访问接口中,使用hutool工具类获取Bean对象。

    @GetMapping("/test")
    @ResponseBody
    public Object test() {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            User user1 = SpringUtil.getBean("user",User.class);
            System.out.println(user1.getUserId());
        }
        return this.user.getUserId();
    }

访问接口,发现每次获取的对象都不同。。。多例模式生效。
在这里插入图片描述

3.2.3 测试request

在User类上添加@Scope,并指定其作用域范围为request。

@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)

访问接口,首先获取 @Autowired注入的Bean,然后通过SpringUtil工具类获取Bean。

    @Autowired
    User user;

    @GetMapping("/test")
    @ResponseBody
    public Object test() {
    
    
        System.out.println("=============" + this.user.getUserId());
        for (int i = 0; i < 10; i++) {
    
    
            User user1 = SpringUtil.getBean("user", User.class);
            System.out.println(user1.getUserId());
        }
        return this.user.getUserId();
    }

访问接口测试,发现当次请求,无论是注入的还是工具类获取的都是同一个对象。
在这里插入图片描述

再次请求时,就不是同一个实例对象了,说明每次请求都创建了不同的实例。
在这里插入图片描述

3.2.4 自定义作用域

原理

启动时BeanFactory会注册各个作用域范围。Spring注入或获取Bean时,会根据作用域判断是否产生新的实例。

如果是单例模式,则会放入缓存中,下次用时,直接从缓存中获取。Prototype作用域初始化时也会创建实例,但是不会放入缓存中,下次获取时,会再创建。

其他范围的作用域,则不会初始化就创建,而是在获取的时候进行创建,根据Bean元数据配置的作用域范围,获取对应的Scope实现类,然后通过实现类获取Bean并返回。

比如Request作用域,初始化并不会常见,使用时,会进入到RequestScope的get方法,如果当前请求已存在了该Bean,则会直接返回,不存在则直接创建,下一次请求时,该Request又会重新获取Bean实例。

需求:

定义一个线程级别的作用域,不同线程时,获取该Bean的不同实例。

  1. 实现Scope接口,从ThreadLocal中获取Bean
public class ThreadScope implements Scope {
    
    


    ThreadLocal<Object> threadLocal = new ThreadLocal<>();

    /**
     * 从作用域返回具有给定名称的对象
     *
     * @param name          要检索的对象的名称
     * @param objectFactory 用于创建作用域的对象工厂
     * @return 返回所需对象
     */
    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
    
    
        // 1. 从当前线程获取Bean对象
        Object obj = threadLocal.get();
        if (obj == null) {
    
    
            // 2. 当前线程不存在该Bean,则创建并设置到线程中
            Object object = objectFactory.getObject();
            threadLocal.set(object);
            return object;
        } else {
    
    
            // 3. 当前线程已存在,直接返回
            return obj;
        }
    }

    /**
     * 从作用域中删除具有给定{@code name}的对象
     */
    @Override
    public Object remove(String name) {
    
    
        return null;
    }

    /**
     * 注册一个回调,在销毁指定的对象
     *
     * @param name     要为其执行回调销毁的对象名称
     * @param callback 要执行的销毁回调
     */
    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
    
    

    }

    /**
     * 解析给定键的上下文对象(如果有)
     */
    @Override
    public Object resolveContextualObject(String key) {
    
    
        return null;
    }

    /**
     * 返回当前作用域的会话ID(如果有)
     */
    @Override
    public String getConversationId() {
    
    
        return null;
    }
}

  1. 注册作用域到BeanFactory中。
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    
    
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    
    
        beanFactory.registerScope("thread",new ThreadScope());
    }
}

  1. 编写一个访问接口
    @GetMapping("/test")
    @ResponseBody
    public Object test() {
    
    
        System.out.println("=============" + this.user.getUserId());
        //  开启新线程
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                User user1 = SpringUtil.getBean("user", User.class);
                System.out.println(user1.getUserId());
            }
        }).start();
        return this.user.getUserId();
    }
  1. 测试,发现不同的线程,获取到了不同的Bean实例对象。
    在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_43437874/article/details/120529966