写在前面

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从当前作用域中移除**/@NullableObject remove(String name);/*** 用于注册销毁回调,如果想要销毁相应的对象,则由Spring容器注册相应的销毁回调,而由自定义作用域选择是不是要销毁相应的对象*/void registerDestructionCallback(String name, Runnable callback);/*** 用于解析相应的上下文数据,比如request作用域将返回request中的属性。*/@NullableObject resolveContextualObject(String key);/*** 作用域的会话标识,比如session作用域将是sessionId*/@NullableString 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() {@Overrideprotected Object initialValue() {return new HashMap<>();}};@Overridepublic 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@Overridepublic Object remove(String name) {return this.beanMap.get().remove(name);}@Overridepublic void registerDestructionCallback(String name, Runnable callback) {//bean作用域范围结束的时候调用的方法,用于bean清理System.out.println(name);}@Nullable@Overridepublic Object resolveContextualObject(String key) {return null;}@Nullable@Overridepublic 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);//向容器中注册自定义的scopecontext.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());//使用容器获取beanfor (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注解驱动开发不再迷茫。

【Spring注解驱动开发】使用@Scope注解设置组件的作用域相关推荐

  1. Spring注解驱动开发——AOP常用注解

    一.用于开启注解AOP支持的 @EnableAspectJAutoProxy (一) 作用 表示开启spring对注解aop的支持.它有两个属性,分别是指定采用的代理方式和 是否暴露代理对象,通过Ao ...

  2. factorybean 代理类不能按照类型注入_Spring注解驱动开发之四——@Import快速导入组件、FactoryBean 定义工厂注册组件...

    本文包含以下内容: @Import快速导入组件 配合ImportSelector 导入组件 配合ImportBeanDefinitionRegistrar 导入组件 FactoryBean 定义工厂注 ...

  3. 【视频分享】尚硅谷Java视频教程_Spring注解驱动开发视频教程

    <Spring注解驱动开发>是一套帮助我们深入了解Spring原理机制的教程: 现今SpringBoot.SpringCloud技术非常火热,作为Spring之上的框架,他们大量使用到了S ...

  4. Spring注解驱动开发学习总结8:自动装配注解@Autowire、@Resource、@Inject

    Spring注解驱动开发学习总结8:自动装配注解@Autowire.@Resource.@Inject 1.自动装配@Autowire.@Resource.@Inject 1.1 构建bookDao ...

  5. SPRING注解驱动开发-雷神课程超详细笔记

    SPRING注解驱动开发-雷神课程超详细笔记 时间:2021-03-21 2022-04-06更新:最近翻起一年多前写的笔记复习,还是收获颇多,很多当时无法理解的知识现在慢慢能理解了,可能是工作一年的 ...

  6. Spring注解驱动开发第26讲——总有人让我给他讲讲@EnableAspectJAutoProxy注解

    @EnableAspectJAutoProxy注解 在配置类上添加@EnableAspectJAutoProxy注解,便能够开启注解版的AOP功能.也就是说,如果要使注解版的AOP功能起作用的话,那么 ...

  7. 【Spring注解驱动开发】二狗子让我给他讲讲@EnableAspectJAutoProxy注解

    写在前面 最近,二狗子入职了新公司,新入职的那几天确实有点飘.不过慢慢的,他发现他身边的人各个身怀绝技啊,有Spring源码的贡献者,有Dubbo源码的贡献者,有MyBatis源码的贡献者,还有研究A ...

  8. spring注解驱动开发-10 Servlet3.0

    Spring AOP实现 前言 servlet3.0简介 ServletContainerInitializer shared libraries(共享库) / runtimes pluggabili ...

  9. spring注解驱动开发-4 Spring 自动装配

    Spring 自动装配 前言 Spring 自动装配的几种方式 1.@Autowired @Qualifier("组件id") @Primary 2.@Resource方式 3.@ ...

最新文章

  1. 状态输出导航栏html,Vue实现导航栏效果(选中状态刷新不消失)_百厌_前端开发者...
  2. GDCM:gdcm::Coder的测试程序
  3. 既生Flash,又何生EEPROM?
  4. Web前端开发人员和设计师必读文章推荐【系列六】
  5. 通过共现矩阵和余弦相似度实现机器对单词的认知、python实现
  6. linux怎么创建牡蛎_Linux文件也有快捷方式?有的,你会用吗?
  7. Java程序员必会的工具库,让你的代码量减少90%!
  8. 拓端tecdat|R语言分类回归决策树交互式修剪和更美观地可视化分析细胞图像分割数据集
  9. 华为OJ 初级:等差数列
  10. json转数组(json数组对象)
  11. jzoj 5778 没有硝烟的战争
  12. org.springframework.core.styler包解读
  13. 九宫格一条线连起来_9个圆圈用4条线连起来-九个点用四条线连接-数学-霍甲心同学...
  14. 会员注册与验证码demo 带注释(html、css、js)
  15. 分布式唯一ID几种生成方案
  16. 西邮Linux2019面试题
  17. 华为鸿蒙系统深圳,鸿蒙操作系统面世 华为称“把不可能变为可能”
  18. Android支付接入
  19. el-table合计
  20. 云栖大会人脸识别闸机【技术亮点篇7】--人脸识别闸机可挑战12万组人脸数据

热门文章

  1. 【NOJ1147】【算法实验三】木乃伊迷宫
  2. list 分批_java 分批读取list
  3. 玩游戏不拖沓,续航也很强,QCY G1真无线电竞耳机体验
  4. 数睿数据深度 | 企业级无代码开发 新思路 新模式 新动能
  5. vivos7和华为nova7的区别 哪个好
  6. 古老计算机的显卡,IT之家网友分享:带你看古老经典的PC硬件
  7. 宝鸡市2021高考成绩查询,宝鸡市所有高中排名一览表,宝鸡市高中排名2021
  8. v-if影响iview table sortable的选中
  9. layui+tp Excel表格信息入库,员工信息批量入库 (小笔记)
  10. Chrome浏览器无法打开特定的网页