文章目录
这是我Spring Frame 专栏的第三篇文章,在 Spring注解驱动开发(二):使用@Configuration和@Bean向容器中注册组件这篇文章中,我向你介绍了如何使用@Configuration配合@Bean 向容器中注入组件,并且详细向你介绍了以下问题:
- @Configuration 属性
proxyBeanMethods
的作用 - @Bean 一定要配合 @Configuration 使用吗
如果你未读过那篇文章,但是对上面的两个问题感兴趣,我建议你去读一下
1. 背景介绍
当我们利用 @Bean 注解向容器中注入组件时,默认是单例的(Spring 上下文环境会缓存这个对象),之后我们获取对象的时候,都是获取这个共享对象而非重新创建一个对象实例,但是有些时候我们希望每次从Spring Ioc 容器中获取对象时,都要创建一个新的实例对象,那么该如何处理呢?Spring 为我们提供了 @Scope
注解来设置组件的作用域.
2. @Scope详解
@Scope 注解可以设置 Bean 实例的作用域,我们先看一下其源码,看看有什么发现:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {
/**
* Alias for {@link #scopeName}.
* @see #scopeName
*/
@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;
}
从源码中可以看到,这里面主要就是 value/scopeName
属性,我们可以用它来指定Bean作用域范围,从它的注释中可以发现,Spring 为我们提供了四种作用域:
- ConfigurableBeanFactory#SCOPE_PROTOTYPE
- ConfigurableBeanFactory#SCOPE_SINGLETON
- org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
- org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
前两个针对的是所有 Spring 应用,而后两个针对的是 Spring Web 应用,我给你分开解释
2.1 SCOPE_PROTOTYPE与SCOPE_SINGLETON
这两个作用域针对的是所有 Spring 应用,我们先进入 ConfigurableBeanFactory
看一下这两个值到底是什么:
public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
String SCOPE_SINGLETON = "singleton";
String SCOPE_PROTOTYPE = "prototype";
}
如果你学过设计模式(如果你并未学过,我强烈建议你去学一下,对你的职业发展很有帮助),一定对这两个单词不陌生:
singleton
表示单例模式,整个Spring 上下文就有一个实例prototype
表示原型模式,每次从Spring获取对应对象都是新创建的
当你不指定value
属性值的时候,默认就是singleton
的
2.2 SCOPE_REQUEST与SCOPE_SESSION
同样的,我们先看一眼这两个值:
public interface WebApplicationContext extends ApplicationContext {
String SCOPE_REQUEST = "request";
String SCOPE_SESSION = "session";
}
这两个值是在Spring Web 环境下起作用,如果你学过Java Web ,学过Servlet的四大作用域,我想你你对这两个值也不会陌生
request
:表示当前对象在一次请求中只有一个实例,每次请求的实例是不同的session
: 表示一次会话中只有一个实例,不同的会话实例是不同的
🐳 PS: 一次会话,你可以理解为一次开关浏览器,一次浏览器中会话是不变的
2.3 总结
通过上面的讲解,我相信你对@Scope 来控制Bean 作用域有了一定了解,接下来我带你实战演示一下singleton
与prototype
的使用,剩余的两个Web环境下的取值在我们开发环境很少使用,这里我就不展开讲解了
3. 单例Bean
3.1 作用域
其实 @Bean 注解标注的对象实例在Spring 上下文中默认就是单例的,这就意味着你标不标注@Scope(value=“singleton”)都是一样的,下面我们先编写代码测试一下
向Spring 中注入 Person 对象
@Configuration
public class CustomerConfig {
@Scope(value = "singleton")
@Bean
public Person person1() {
// 构造器注入
return new Person(1, "jack", "male");
}
}
这里我们从Spring Ioc 容器中两次获取person对象,查看获取到对象是否为同一对象实例(这里我用==直接比较对象内存地址是否相等)
public class IcoMain {
public static void main(String[] args) {
// 换成注解配置上下文环境
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
Person person1 = context.getBean(Person.class);
Person person2 = context.getBean(Person.class);
System.out.println("person1==person2: "+(person1==person2));
context.close();
}
}
结果验证了我们每次从容器中获取的 Person 实例都是同一个:
3.2 加载时机
既然这个对象是单例的,那么它到底是何时注入Ioc 容器的呢?
为了演示对象加载时机,我们修改我们的配置类 CustomerConfig:
@Configuration
public class CustomerConfig {
@Scope(value = "singleton")
@Bean
public Person person1() {
// 加上这段话
System.out.println("++++++++Person 对象加载完成++++++++");
return new Person(1, "jack", "male");
}
}
接着我们在测试类Spring初始化完成之后加上一句话,表示容器初始化完成:
public class IcoMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
// 加上这段打印
System.out.println("--------------Spring Ioc 容器初始化完成--------------");
Person person1 = context.getBean(Person.class);
Person person2 = context.getBean(Person.class);
System.out.println("person1==person2: "+(person1==person2));
context.close();
}
}
接下来启动测试类查看结果:
从结果我们可以得出结论: 单例Bean随 Spring Ioc 初始化而创建
3.3 注意问题
从上面我们知道了@Bean + @Scope 默认生成的 bean 实例就是单例的。
单例意味着什么呢?它表示这个 bean 在这个Spring 上下文里就是 唯一的,共享的,
那么 A 修改了 bean 的属性,那么 B 会查看到什么结果呢?我们来测试一下:
public class IcoMain {
public static void main(String[] args) {
// 换成注解配置上下文环境
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
System.out.println("--------------Spring Ioc 容器初始化完成--------------");
Person person1 = context.getBean(Person.class);
System.out.println("修改 person1 前: "+person1);
person1.setSex("female");
Person person2 = context.getBean(Person.class);
System.out.println("修改 person1 后: "+person2);
System.out.println("person1==person2: "+(person1==person2));
context.close();
}
}
程序中,我先获取person1 并设置了它的sex属性,之后我们再次从容器中获取person1,查看修改前后获取的 person1 发生了什么变化:
从结果可以验证: A 修改 person1 ,B能查看到修改后的结果。
⭐️ 这时你要注意到单例对象的共享性,以免在多线程环境下产生问题
4. 多实例bean
接下来我们将person1()方法的 @Scope注解 的value
值修改为protorype
4.1 作用域
下图为修改后的 CustomerConfig:
@Configuration
public class CustomerConfig {
@Scope(value = "prototype")
@Bean
public Person person1() {
return new Person(1, "jack", "female");
}
}
接下来修改测试类:
public class IcoMain {
public static void main(String[] args) {
// 换成注解配置上下文环境
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
System.out.println("--------------Spring Ioc 容器初始化完成--------------")
Person person1 = context.getBean(Person.class);
Person person2 = context.getBean(Person.class);
System.out.println("person1==person2: "+(person1==person2));
context.close();
}
}
运行测试类:
从结果可以得出,我们每次从容器中获取的 perosn1 都是新创建的
4.2 加载时机
对比单实例Bean随着Spring 容器初始化而加载,我们来探讨一下多实例Bean何时被加载,你可以先自己思考一下
在person1()方法创建对象之前打印一段话,具体代码如下所示:
@Configuration
public class CustomerConfig {
@Scope(value = "prototype")
@Bean
public Person person1() {
System.out.println("++++++++Person 对象加载完成++++++++");
return new Person(1, "jack", "female");
}
}
接下来编写测试类
public class IcoMain {
public static void main(String[] args) {
// 换成注解配置上下文环境
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
System.out.println("--------------Spring Ioc 容器初始化完成--------------");
Person person1 = context.getBean(Person.class);
Person person2 = context.getBean(Person.class);
System.out.println("person1==person2: "+(person1==person2));
context.close();
}
}
接下来运行测试类,查看一下多实例Bean的加载时机:
相信通过结果你可以发现,多实例Bean是在我们获取实例时才加载的
4.3 注意问题
如果你每次从Spring容器中读取bean实例的时候,并不会修改bean实例的属性信息,我还是建议你将bean实例设置为 单例 的,毕竟:
多实例bean每次获取的时候都会重新创建,如果这个bean比较复杂,创建时间比较长,那么就会影响系统的性能
4. 自定义Scope 作用域
Spring 强大的地方在与它有很强的 扩展性,我们甚至可以自定义Scope 作用域.自定义Scope步骤为:
-
实现
Scope
接口 -
向Spring Ioc 中注入 Scope 实现类
-
在对应Bean上设置对应的Scope value 值
这里我们来实现一个线程级别的bean作用域,即同一个线程中获取的同名Bean都是相同的,这里面我要利用ThreadLocal 存储bean实例:
- 定义Scope 接口实现类
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<>(4);
}
};
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Object o = beanMap.get().get(name);
if(ObjectUtils.isEmpty(o)) {
// 不存在时从ObjectFactory中获取对应对象并放入到作用域中
o = objectFactory.getObject();
beanMap.get().put(name,o);
}
return o;
}
@Override
public Object remove(String name) {
return beanMap.get().remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
callback = () -> {
beanMap.get().remove(name);
System.out.println("===========对象销毁===========");
};
}
/**
* 解析相应的上下文数据
* @param key
* @return
*/
@Override
public Object resolveContextualObject(String key) {
return null;
}
/**
* 作用域的会话标志,
* @return
*/
@Override
public String getConversationId() {
return Thread.currentThread().getName();
}
}
- 在 Ioc 容器中注入这个 Scope
public class IcoMain {
public static void main(String[] args) {
showScope();
}
static void showScope() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
//注册自定义的scope 这个scope 对应的值为 thread
context.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());
for(int i = 0;i < 2;i++) {
new Thread(()->{
Object th1 = context.getBean("person1");
Object th2 = context.getBean("person1");
System.out.println(Thread.currentThread().getName()+"===>"+th1.hashCode());
System.out.println(Thread.currentThread().getName()+"===>"+th2.hashCode());
}).start();
}
}
}
- 设置对应bean的作用域
@Configuration
public class CustomerConfig {
@Scope(value = "thread")
@Bean
public Person person1() {
// 构造器注入
System.out.println("++++++++Person 对象加载完成++++++++");
return new Person(1, "jack", "female");
}
}
查看运行结果:
从结果可以看见同一个线程获取的bean实例是相同的
5. 总结
今天我向你传输了以下内容
- @Scope 注解的详细介绍
- 单例Bean实例的创建时机,可能出现的问题
- 多实例Bean实例的创建时机,可能出现的问题
- 如何自定义Scope作用域
最后,我希望你看完本篇文章后,能够利用@Scope注解控制Bean的作用域,也希望你指出我在文章中的错误点,希望我们一起进步,也希望你能给我的文章点个赞,原创不易!