> 文章列表 > 使用@Scope注解设置组件的作用域

使用@Scope注解设置组件的作用域

使用@Scope注解设置组件的作用域

前言

Spring容器中的组件默认是单例的,在Spring启动时就会实例化并初始化这些对象,并将其放到Spring容器中,之后,每次获取对象时,直接从Spring容器中获取,而不再创建对象。

1.@Scope注解概述

@Scope注解能够设置组件的作用域,Scope注解类的源码,如下所示

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;}

从**@Scope**注解类的源码中可以看出,在@Scope注解中可以设置如下值:

  1. ConfigurableBeanFactory#SCOPE_PROTOTYPE
  2. ConfigurableBeanFactory#SCOPE_SINGLETON
  3. org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
  4. org.springframework.web.context.WebApplicationContext#SCOPE_SESSION

查看一下ConfigurableBeanFactory接口的源码,发现在该接口中存在两个常量的定义

public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {String SCOPE_SINGLETON = "singleton";String SCOPE_PROTOTYPE = "prototype";
}

SCOPE_SINGLETON就是singleton,而SCOPE_PROTOTYPE就是prototype

而SCOPE_REQUEST的值就是request,SCOPE_SESSION的值就是session

备注:request和session作用域是需要Web环境来支持的,这两个值基本上使用不到,实例对象的作用域设置为request和session

request.setAttribute("key", object);session.setAttribute("key", object);

2.单实例bean作用域

创建MainConfig02配置类,实例化一个Person对象,并将其放置在Spring容器中

package com.tianxia.springannotation.config;import com.tianxia.springannotation.entity.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 配置类* @author liqb* @date 2023-04-23 9:45**/
@Configuration
public class MainConfig02 {/*** 创建person实例* @author liqb* @date 2023-04-23 09:46* @return*/@Bean("person02")public Person person() {return new Person("liqb", 24);}
}

编写测试方法,从Spring容器中按照id获取两个Person对象,并判断这两个对象是否是同一个对象

/*** 测试单实例bean* @author liqb* @date 2023-04-23 09:47*/
@Test
public void testSingleBean() {AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig02.class);// 获取到的这个Person对象默认是单实例的,因为在IOC容器中给我们加的这些组件默认都是单实例的,// 所以说在这儿我们无论多少次获取,获取到的都是我们之前new的那个实例对象Person person = (Person) applicationContext.getBean("person02");Person person2 = (Person) applicationContext.getBean("person02");System.out.println(person == person2);
}

测试结果:true

结论:对象在Spring容器中默认是单实例的,Spring容器在启动时就会将实例对象加载到Spring容器中,之后,每次从Spring容器中获取实例对象,都是直接将对象返回,而不必再创建新的实例对象了。

2.多实例bean作用域

将MainConfig02配置类中Person对象的作用域修改成prototype,如下所示。

package com.tianxia.springannotation.config;import com.tianxia.springannotation.entity.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;/*** 配置类* @author liqb* @date 2023-04-23 9:45**/
@Configuration
public class MainConfig02 {/*** 创建person实例* @author liqb* @date 2023-04-23 09:46* @return*/@Bean("person02")// 通过@Scope注解来指定该bean的作用范围,也可以说成是调整作用域@Scope("prototype")public Person person() {return new Person("liqb", 24);}
}

再次运行测试方法

测试结果:false

结论:当对象是多实例时,每次从Spring容器中获取对象时,都会创建新的实例对象,并且每个实例对象都不相等

3.单实例bean作用域何时创建对象

package com.tianxia.springannotation.config;import com.tianxia.springannotation.entity.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 配置类* @author liqb* @date 2023-04-23 9:45**/
@Configuration
public class MainConfig02 {/*** 创建person实例* @author liqb* @date 2023-04-23 09:46* @return*/@Bean("person02")public Person person() {System.out.println("给容器中添加咱们这个Person对象...");return new Person("liqb", 24);}
}

创建测试方法

/*** 测试单实例bean何时创建对象* @author liqb* @date 2023-04-23 10:01*/
@Test
public void testSingleBeanTime() {AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig02.class);
}

输出的结果信息如下所示

在这里插入图片描述

结论:Spring容器在创建的时候,就将@Scope注解标注为singleton的组件进行了实例化,并加载到了Spring容器中。

/*** 测试单实例bean何时创建对象* @author liqb* @date 2023-04-23 10:01*/
@Test
public void testSingleBeanTime() {AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig02.class);Person person = (Person) applicationContext.getBean("person02");Person person2 = (Person) applicationContext.getBean("person02");System.out.println(person == person2);
}

输出的结果信息如下所示

在这里插入图片描述

结论:Spring容器在启动时,将单实例组件实例化之后,会即刻加载到Spring容器中,以后每次从容器中获取组件实例对象时,都是直接返回相应的对象,而不必再创建新的对象了。

4.多实例bean作用域何时创建对象?

package com.tianxia.springannotation.config;import com.tianxia.springannotation.entity.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;/*** 配置类* @author liqb* @date 2023-04-23 9:45**/
@Configuration
public class MainConfig02 {/*** 创建person实例* @author liqb* @date 2023-04-23 09:46* @return*/@Bean("person02")//通过@Scope注解来指定该bean的作用范围,也可以说成是调整作用域@Scope("prototype")public Person person() {System.out.println("给容器中添加咱们这个Person对象...");return new Person("liqb", 24);}
}

创建测试方法

/*** 测试多实例bean何时创建对象* @author liqb* @date 2023-04-23 10:01*/
@Test
public void testPrototypeBeanTime() {AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig02.class);
}

输出的结果信息如下所示

在这里插入图片描述

结论:当向Spring容器中获取Person实例对象时,Spring容器才会实例化Person对象,再将其加载到Spring容器中去。

/*** 测试多实例bean何时创建对象* @author liqb* @date 2023-04-23 10:01*/
@Test
public void testPrototypeBeanTime() {AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig02.class);Person person = (Person) applicationContext.getBean("person02");Person person2 = (Person) applicationContext.getBean("person02");System.out.println(person == person2);
}

输出的结果信息如下所示

在这里插入图片描述

结论:当对象的Scope作用域为多实例时,每次向Spring容器获取对象时,它都会创建一个新的对象并返回。很显然,以上获取到的person和person2就不是同一个对象了,每次从Spring容器中获取对象时,都会创建新的实例对象,并且每个实例对象都不相等。

5.单实例bean注意的事项

单实例bean是整个应用所共享的,所以需要考虑到线程安全问题,在玩SpringMVC的时候,SpringMVC中的Controller默认是单例的,有

些开发者在Controller中创建了一些变量,那么这些变量实际上就变成共享的了,Controller又可能会被很多线程同时访问,这些线程并发去修

改Controller中的共享变量,此时很有可能会出现数据错乱的问题,所以使用的时候需要特别注意。

6.多实例bean注意的事项

多实例bean每次获取的时候都会重新创建,如果这个bean比较复杂,创建时间比较长,那么就会影响系统的性能,因此这个地方需要注意点。

7.自定义Scope

实现一个线程级别的bean作用域,同一个线程中同名的bean是同一个实例,不同的线程中的bean是不同的实例

package com.tianxia.springannotation.config.configEntity;import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;import java.util.HashMap;
import java.util.Map;
import java.util.Objects;/*** 自定义scope* @author liqb* @date 2023-04-23 10:26**/
public class CustomerScope implements Scope {public static final String CUSTOMER_SCOPE = "thread";private ThreadLocal<Map<String, Object>> beanMap = new ThreadLocal() {@Overrideprotected Object initialValue() {return new HashMap<>();}};/*** 返回当前作用域中name对应的bean对象* @author liqb* @date 2023-04-23 10:29* @param name 需要检索的bean对象的名称* @param objectFactory 如果name对应的bean对象在当前作用域中没有找到,那么可以调用这个objectFactory来创建这个bean对象* @return*/@Overridepublic Object get(String name, ObjectFactory<?> objectFactory) {// 获取bean对象Object bean = beanMap.get().get(name);if (Objects.isNull(bean)) {bean = objectFactory.getObject();beanMap.get().put(name, bean);}return bean;}/*** 将name对应的bean对象从当前作用域中移除* @author liqb* @date 2023-04-23 10:43* @param name bean名称* @return*/@Overridepublic Object remove(String name) {return this.beanMap.get().remove(name);}/*** 用于注册销毁回调,若想要销毁相应的对象,则由Spring容器注册相应的销毁回调,而由自定义作用域选择是不是要销毁相应的对象* @param name bean名称* @param callback 回调方法*/@Overridepublic void registerDestructionCallback(String name, Runnable callback) {System.out.println(name);}/*** 用于解析相应的上下文数据,比如request作用域将返回request中的属性* @author liqb* @date 2023-04-23 10:44* @param key* @return*/@Overridepublic Object resolveContextualObject(String key) {return null;}/*** 作用域的会话标识,比如session作用域的会话标识是sessionId* @author liqb* @date 2023-04-23 10:44* @return*/@Overridepublic String getConversationId() {return Thread.currentThread().getName();}
}

创建一个配置类

package com.tianxia.springannotation.config;import com.tianxia.springannotation.config.configEntity.CustomerScope;
import com.tianxia.springannotation.entity.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;/*** 配置类* @author liqb* @date 2023-04-23 10:45**/
@Configuration
public class MainConfig03 {/*** 创建person实例* @author liqb* @date 2023-04-23 09:46* @return*/@Bean("person03")@Scope(CustomerScope.CUSTOMER_SCOPE)public Person person() {System.out.println("给容器中添加咱们这个Person对象...");return new Person("liqb", 24);}
}

编写测试方法

/*** 测试自定义Scope* @author liqb* @date 2023-04-23 10:47*/
@Test
public void testCustomScope() {AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig03.class);// 向容器中注册自定义的ScopeapplicationContext.getBeanFactory().registerScope(CustomerScope.CUSTOMER_SCOPE, new CustomerScope());// 使用容器获取beanfor (int i = 0; i < 2; i++) {new Thread(() -> {System.out.println(Thread.currentThread() + "," + applicationContext.getBean("person03"));System.out.println(Thread.currentThread() + "," + applicationContext.getBean("person03"));}).start();}try {TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}
}

实验结果
在这里插入图片描述