写在前面
Spring 容器中的组件默认是单例的, 在 Spring 启动时就会实例化并初始化这些对象, 将其放到 Spring 容器中, 之后, 每次获取对象时, 直接从 Spring 容器中获取, 而不再创建对象. 如果每次从 Spring 容器中获取对象时, 都要创建一个新的实例对象, 该如何处理呢? 此时就需要使用 @Scope 注解设置组件的作用域.
项目工程源码已经提交到 GitHub: https://github.com/sunshinelyz/spring-annotation
本文内容概览
@Scope 注解概述
单实例 bean 作用域
多实例 bean 作用域
单实例 bean 作用域如何创建对象?
多实例 bean 作用域如何创建对象?
单实例 bean 注意的事项
多实例 bean 注意的事项
自定义 Scope 的实现
@Scope 注解概述
@Scope 注解能够设置组件的作用域, 我们先来看 @Scope 注解类的源码, 如下所示.
- package org.springframework.context.annotation;
- import java.lang.annotation.Documented;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
- import org.springframework.beans.factory.config.ConfigurableBeanFactory;
- import org.springframework.core.annotation.AliasFor;
- @Target({ElementType.TYPE, ElementType.METHOD})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface Scope {
- @AliasFor("scopeName")
- String value() default "";
- /**
- * Specifies the name of the scope to use for the annotated component/bean.
- * <p>Defaults to an empty string ({@code ""}) which implies
- * {@link ConfigurableBeanFactory#SCOPE_SINGLETON SCOPE_SINGLETON}.
- * @since 4.2
- * @see ConfigurableBeanFactory#SCOPE_PROTOTYPE
- * @see ConfigurableBeanFactory#SCOPE_SINGLETON
- * @see org.springframework.Web.context.WebApplicationContext#SCOPE_REQUEST
- * @see org.springframework.Web.context.WebApplicationContext#SCOPE_SESSION
- * @see #value
- */
- @AliasFor("value")
- String scopeName() default "";
- ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
- }
从源码中可以看出, 在 @Scope 注解中可以设置如下值.
- ConfigurableBeanFactory#SCOPE_PROTOTYPE
- ConfigurableBeanFactory#SCOPE_SINGLETON
- org.springframework.Web.context.WebApplicationContext#SCOPE_REQUEST
- org.springframework.Web.context.WebApplicationContext#SCOPE_SESSION
很明显, 在 @Scope 注解中可以设置的值包括 ConfigurableBeanFactory 接口中的 SCOPE_PROTOTYPE 和 SCOPE_SINGLETON, 以及 WebApplicationContext 类中 SCOPE_REQUEST 和 SCOPE_SESSION. 这些都是什么鬼? 别急, 我们来一个个查看.
首先, 我们进入到 ConfigurableBeanFactory 接口中, 发现在 ConfigurableBeanFactory 类中存在两个常量的定义, 如下所示.
- public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
- String SCOPE_SINGLETON = "singleton";
- String SCOPE_PROTOTYPE = "prototype";
- /***************** 此处省略 N 多行代码 *******************/
- }
没错, SCOPE_SINGLETON 就是 singleton,SCOPE_PROTOTYPE 就是 prototype.
那么, WebApplicationContext 类中 SCOPE_REQUEST 和 SCOPE_SESSION 又是什么鬼呢? 就是说, 当我们使用了 Web 容器来运行 Spring 应用时, 在 @Scope 注解中可以设置 WebApplicationContext 类中 SCOPE_REQUEST 和 SCOPE_SESSION 的值, 而 SCOPE_REQUEST 的值就是 request,SCOPE_SESSION 的值就是 session.
综上, 在 @Scope 注解中的取值如下所示.
singleton: 表示组件在 Spring 容器中是单实例的, 这个是 Spring 的默认值, Spring 在启动的时候会将组件进行实例化并加载到 Spring 容器中, 之后, 每次从 Spring 容器中获取组件时, 直接将实例对象返回, 而不必再次创建实例对象. 从 Spring 容器中获取对象, 小伙伴们可以理解为从 Map 对象中获取对象.
prototype: 表示组件在 Spring 容器中是多实例的, Spring 在启动的时候并不会对组件进行实例化操作, 而是每次从 Spring 容器中获取组件对象时, 都会创建一个新的实例对象并返回.
request: 每次请求都会创建一个新的实例对象, request 作用域用在 spring 容器的 Web 环境中.
session: 在同一个 session 范围内, 创建一个新的实例对象, 也是用在 Web 环境中.
application: 全局 Web 应用级别的作用于, 也是在 Web 环境中使用的, 一个 Web 应用程序对应一个 bean 实例, 通常情况下和 singleton 效果类似的, 不过也有不一样的地方, singleton 是每个 spring 容器中只有一个 bean 实例, 一般我们的程序只有一个 spring 容器, 但是, 一个应用程序中可以创建多个 spring 容器, 不同的容器中可以存在同名的 bean, 但是 sope=aplication 的时候, 不管应用中有多少个 spring 容器, 这个应用中同名的 bean 只有一个.
其中, request 和 session 作用域是需要 Web 环境支持的, 这两个值基本上使用不到, 如果我们使用 Web 容器来运行 Spring 应用时, 如果需要将组件的实例对象的作用域设置为 request 和 session, 我们通常会使用 request.setAttribute("key",object) 和 session.setAttribute("key", object) 的形式来将对象实例设置到 request 和 session 中, 通常不会使用 @Scope 注解来进行设置.
单实例 bean 作用域
首先, 我们在 io.mykit.spring.plugins.register.config 包下创建 PersonConfig2 配置类, 在 PersonConfig2 配置类中实例化一个 Person 对象, 并将其放置在 Spring 容器中, 如下所示.
- package io.mykit.spring.plugins.register.config;
- import io.mykit.spring.bean.Person;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- /**
- * @author binghe
- * @version 1.0.0
- * @description 测试 @Scope 注解设置的作用域
- */
- @Configuration
- public class PersonConfig2 {
- @Bean("person")
- public Person person(){
- return new Person("binghe002", 18);
- }
- }
接下来, 在 SpringBeanTest 类中创建 testAnnotationConfig2() 测试方法, 在 testAnnotationConfig2() 方法中, 创建 ApplicationContext 对象, 创建完毕后, 从 Spring 容器中按照 id 获取两个 Person 对象, 并打印两个对象是否是同一个对象, 代码如下所示.
- @Test
- public void testAnnotationConfig2(){
- ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
- // 从 Spring 容器中获取到的对象默认是单实例的
- Object person1 = context.getBean("person");
- Object person2 = context.getBean("person");
- System.out.println(person1 == person2);
- }
由于对象在 Spring 容器中默认是单实例的, 所以, Spring 容器在启动时就会将实例对象加载到 Spring 容器中, 之后, 每次从 Spring 容器中获取实例对象, 直接将对象返回, 而不必在创建新对象实例, 所以, 此时 testAnnotationConfig2() 方法会输出 true. 如下所示.
这也验证了我们的结论: 对象在 Spring 容器中默认是单实例的, Spring 容器在启动时就会将实例对象加载到 Spring 容器中, 之后, 每次从 Spring 容器中获取实例对象, 直接将对象返回, 而不必在创建新对象实例.
多实例 bean 作用域
修改 Spring 容器中组件的作用域, 我们需要借助于 @Scope 注解, 此时, 我们将 PersonConfig2 类中 Person 对象的作用域修改成 prototype, 如下所示.
- @Configuration
- public class PersonConfig2 {
- @Scope("prototype")
- @Bean("person")
- public Person person(){
- return new Person("binghe002", 18);
- }
- }
其实, 使用 @Scope 设置作用域就等同于在 xml 文件中为 bean 设置 scope 作用域, 如下所示.
此时, 我们再次运行 SpringBeanTest 类的 testAnnotationConfig2() 方法, 此时, 从 Spring 容器中获取到的 person1 对象和 person2 对象还是同一个对象吗?
通过输出结果可以看出, 此时, 输出的 person1 对象和 person2 对象已经不是同一个对象了.
单实例 bean 作用域何时创建对象?
接下来, 我们验证下在单实例作用域下, Spring 是在什么时候创建对象的呢?
首先, 我们将 PersonConfig2 类中的 Person 对象的作用域修改成单实例, 并在返回 Person 对象之前打印相关的信息, 如下所示.
- @Configuration
- public class PersonConfig2 {
- @Scope
- @Bean("person")
- public Person person(){
- System.out.println("给容器中添加 Person....");
- return new Person("binghe002", 18);
- }
- }
接下来, 我们在 SpringBeanTest 类中创建 testAnnotationConfig3() 方法, 在 testAnnotationConfig3() 方法中, 我们只创建 Spring 容器, 如下所示.
- @Test
- public void testAnnotationConfig3(){
- ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
- }
此时, 我们运行 SpringBeanTest 类中的 testAnnotationConfig3() 方法, 输出的结果信息如下所示.
从输出的结果信息可以看出, Spring 容器在创建的时候, 就将 @Scope 注解标注为 singleton 的组件进行了实例化, 并加载到 Spring 容器中.
接下来, 我们运行 SpringBeanTest 类中的 testAnnotationConfig2(), 结果信息如下所示.
说明, Spring 容器在启动时, 将单实例组件实例化之后, 加载到 Spring 容器中, 以后每次从容器中获取组件实例对象, 直接返回相应的对象, 而不必在创建新对象.
多实例 bean 作用域何时创建对象?
如果我们将对象的作用域修改成多实例, 那什么时候创建对象呢?
此时, 我们将 PersonConfig2 类的 Person 对象的作用域修改成多实例, 如下所示.
- @Configuration
- public class PersonConfig2 {
- @Scope("prototype")
- @Bean("person")
- public Person person(){
- System.out.println("给容器中添加 Person....");
- return new Person("binghe002", 18);
- }
- }
我们再次运行 SpringBeanTest 类中的 testAnnotationConfig3() 方法, 输出的结果信息如下所示.
可以看到, 终端并没有输出任何信息, 说明在创建 Spring 容器时, 并不会实例化和加载多实例对象, 那多实例对象是什么时候实例化的呢? 接下来, 我们在 SpringBeanTest 类中的 testAnnotationConfig3() 方法中添加一行获取 Person 对象的代码, 如下所示.
- @Test
- public void testAnnotationConfig3(){
- ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
- Object person1 = context.getBean("person");
- }
此时, 我们再次运行 SpringBeanTest 类中的 testAnnotationConfig3() 方法, 结果信息如下所示.
从结果信息中, 可以看出, 当向 Spring 容器中获取 Person 实例对象时, Spring 容器实例化了 Person 对象, 并将其加载到 Spring 容器中.
那么, 问题来了, 此时 Spring 容器是否只实例化一个 Person 对象呢? 我们在 SpringBeanTest 类中的 testAnnotationConfig3() 方法中再添加一行获取 Person 对象的代码, 如下所示.
- @Test
- public void testAnnotationConfig3(){
- ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
- Object person1 = context.getBean("person");
- Object person2 = context.getBean("person");
- }
此时, 我们再次运行 SpringBeanTest 类中的 testAnnotationConfig3() 方法, 结果信息如下所示.
从输出结果可以看出, 当对象的 Scope 作用域为多实例时, 每次向 Spring 容器获取对象时, 都会创建一个新的对象并返回. 此时, 获取到的 person1 和 person2 就不是同一个对象了, 我们也可以打印结果信息来进行验证, 此时在 SpringBeanTest 类中的 testAnnotationConfig3() 方法中打印两个对象是否相等, 如下所示.
- @Test
- public void testAnnotationConfig3(){
- ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
- Object person1 = context.getBean("person");
- Object person2 = context.getBean("person");
- System.out.println(person1 == person2);
- }
此时, 我们再次运行 SpringBeanTest 类中的 testAnnotationConfig3() 方法, 结果信息如下所示.
可以看到, 当对象是多实例时, 每次从 Spring 容器中获取对象时, 都会创建新的实例对象, 并且每个实例对象都不相等.
单实例 bean 注意的事项
单例 bean 是整个应用共享的, 所以需要考虑到线程安全问题, 之前在玩 springmvc 的时候, springmvc 中 controller 默认是单例的, 有些开发者在 controller 中创建了一些变量, 那么这些变量实际上就变成共享的了, controller 可能会被很多线程同时访问, 这些线程并发去修改 controller 中的共享变量, 可能会出现数据错乱的问题; 所以使用的时候需要特别注意.
多实例 bean 注意的事项
多例 bean 每次获取的时候都会重新创建, 如果这个 bean 比较复杂, 创建时间比较长, 会影响系统的性能, 这个地方需要注意.
自定义 Scope
如果 Spring 内置的几种 sope 都无法满足我们的需求的时候, 我们可以自定义 bean 的作用域.
1. 如何实现自定义 Scope
自定义 Scope 主要分为三个步骤, 如下所示.
(1) 实现 Scope 接口
我们先来看下 Scope 接口的定义, 如下所示.
- package org.springframework.beans.factory.config;
- import org.springframework.beans.factory.ObjectFactory;
- import org.springframework.lang.Nullable;
- public interface Scope {
- /**
- * 返回当前作用域中 name 对应的 bean 对象
- * name: 需要检索的 bean 的名称
- * objectFactory: 如果 name 对应的 bean 在当前作用域中没有找到, 那么可以调用这个 ObjectFactory 来创建这个对象
- **/
- Object get(String name, ObjectFactory<?> objectFactory);
- /**
- * 将 name 对应的 bean 从当前作用域中移除
- **/
- @Nullable
- Object remove(String name);
- /**
- * 用于注册销毁回调, 如果想要销毁相应的对象, 则由 Spring 容器注册相应的销毁回调, 而由自定义作用域选择是不是要销毁相应的对象
- */
- void registerDestructionCallback(String name, Runnable callback);
- /**
- * 用于解析相应的上下文数据, 比如 request 作用域将返回 request 中的属性.
- */
- @Nullable
- Object resolveContextualObject(String key);
- /**
- * 作用域的会话标识, 比如 session 作用域将是 sessionId
- */
- @Nullable
- String getConversationId();
- }
(2) 将 Scope 注册到容器
需要调用 org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope 的方法, 看一下这个方法的声明
- /**
- * 向容器中注册自定义的 Scope
- *scopeName: 作用域名称
- * scope: 作用域对象
- **/
- void registerScope(String scopeName, Scope scope);
(3) 使用自定义的作用域
定义 bean 的时候, 指定 bean 的 scope 属性为自定义的作用域名称.
2. 自定义 Scope 实现案例
例如, 我们来实现一个线程级别的 bean 作用域, 同一个线程中同名的 bean 是同一个实例, 不同的线程中的 bean 是不同的实例.
这里, 要求 bean 在线程中是共享的, 所以我们可以通过 ThreadLocal 来实现, ThreadLocal 可以实现线程中数据的共享.
此时, 我们在 io.mykit.spring.plugins.register.scope 包下新建 ThreadScope 类, 如下所示.
- package io.mykit.spring.plugins.register.scope;
- import org.springframework.beans.factory.ObjectFactory;
- import org.springframework.beans.factory.config.Scope;
- import org.springframework.lang.Nullable;
- import java.util.HashMap;
- import java.util.Map;
- import java.util.Objects;
- /**
- * 自定义本地线程级别的 bean 作用域, 不同的线程中对应的 bean 实例是不同的, 同一个线程中同名的 bean 是同一个实例
- */
- public class ThreadScope implements Scope {
- public static final String THREAD_SCOPE = "thread";
- private ThreadLocal<Map<String, Object>> beanMap = new ThreadLocal() {
- @Override
- protected Object initialValue() {
- return new HashMap<>();
- }
- };
- @Override
- public Object get(String name, ObjectFactory<?> objectFactory) {
- Object bean = beanMap.get().get(name);
- if (Objects.isNull(bean)) {
- bean = objectFactory.getObject();
- beanMap.get().put(name, bean);
- }
- return bean;
- }
- @Nullable
- @Override
- public Object remove(String name) {
- return this.beanMap.get().remove(name);
- }
- @Override
- public void registerDestructionCallback(String name, Runnable callback) {
- //bean 作用域范围结束的时候调用的方法, 用于 bean 清理
- System.out.println(name);
- }
- @Nullable
- @Override
- public Object resolveContextualObject(String key) {
- return null;
- }
- @Nullable
- @Override
- public String getConversationId() {
- return Thread.currentThread().getName();
- }
- }
在 ThreadScope 类中, 我们定义了一个常量 THREAD_SCOPE, 在定义 bean 的时候给 scope 使用.
接下来, 我们在 io.mykit.spring.plugins.register.config 包下创建 PersonConfig3 类, 并使用 @Scope("thread") 注解标注 Person 对象的作用域为 Thread 范围, 如下所示.
- package io.mykit.spring.plugins.register.config;
- import io.mykit.spring.bean.Person;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.context.annotation.Scope;
- /**
- * @author binghe
- * @version 1.0.0
- * @description 测试 @Scope 注解设置的作用域
- */
- @Configuration
- public class PersonConfig3 {
- @Scope("thread")
- @Bean("person")
- public Person person(){
- System.out.println("给容器中添加 Person....");
- return new Person("binghe002", 18);
- }
- }
最后, 我们在 SpringBeanTest 类中创建 testAnnotationConfig4() 方法, 在 testAnnotationConfig4() 方法中创建 Spring 容器, 并向 Spring 容器中注册 ThreadScope 对象, 接下来, 使用循环创建两个 Thread 线程, 并分别在每个线程中获取两个 Person 对象, 如下所示.
- @Test
- public void testAnnotationConfig4(){
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig3.class);
- // 向容器中注册自定义的 scope
- context.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());
- // 使用容器获取 bean
- for (int i = 0; i <2; i++) {
- new Thread(() -> {
- System.out.println(Thread.currentThread() + "," + context.getBean("person"));
- System.out.println(Thread.currentThread() + "," + context.getBean("person"));
- }).start();
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
此时, 我们运行 SpringBeanTest 类的 testAnnotationConfig4() 方法, 输出的结果信息如下所示.
从输出中可以看到, bean 在同样的线程中获取到的是同一个 bean 的实例, 不同的线程中 bean 的实例是不同的.
注意: 这里, 我将 Person 类进行了相应的调整, 去掉 Lombok 的注解, 手动写构造函数和 setter 与 getter 方法, 如下所示.
- package io.mykit.spring.bean;
- import java.io.Serializable;
- /**
- * @author binghe
- * @version 1.0.0
- * @description 测试实体类
- */
- public class Person implements Serializable {
- private static final long serialVersionUID = 7387479910468805194L;
- private String name;
- private Integer age;
- public Person() {
- }
- public Person(String name, Integer age) {
- this.name = name;
- this.age = age;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public Integer getAge() {
- return age;
- }
- public void setAge(Integer age) {
- this.age = age;
- }
- }
好了, 咱们今天就聊到这儿吧! 别忘了给个在看和转发, 让更多的人看到, 一起学习一起进步!!
项目工程源码已经提交到 GitHub: https://github.com/sunshinelyz/spring-annotation
写在最后
如果觉得文章对你有点帮助, 请微信搜索并关注「 冰河技术 」微信公众号, 跟冰河学习 Spring 注解驱动开发. 公众号回复 "spring 注解" 关键字, 领取 Spring 注解驱动开发核心知识图, 让 Spring 注解驱动开发不再迷茫.
来源: https://www.cnblogs.com/binghe001/p/13067071.html