spring容器中的组件默认是单例的,在容器启动后会实例化这些对象并放到容器中,每次获取对象时,直接从容器中获取。这个特性是合理的,大多数场景也该这么使用。
但如果期望每次从spring中获取对象时都是新实例,此时需要借助@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;
/**
* When used as a type-level annotation in conjunction with
* {@link org.springframework.stereotype.Component @Component},
* {@code @Scope} indicates the name of a scope to use for instances of
* the annotated type.
*
* <p>When used as a method-level annotation in conjunction with
* {@link Bean @Bean}, {@code @Scope} indicates the name of a scope to use
* for the instance returned from the method.
*
* <p><b>NOTE:</b> {@code @Scope} annotations are only introspected on the
* concrete bean class (for annotated components) or the factory method
* (for {@code @Bean} methods). In contrast to XML bean definitions,
* there is no notion of bean definition inheritance, and inheritance
* hierarchies at the class level are irrelevant for metadata purposes.
*
* <p>In this context, <em>scope</em> means the lifecycle of an instance,
* such as {@code singleton}, {@code prototype}, and so forth. Scopes
* provided out of the box in Spring may be referred to using the
* {@code SCOPE_*} constants available in the {@link ConfigurableBeanFactory}
* and {@code WebApplicationContext} interfaces.
*
* <p>To register additional custom scopes, see
* {@link org.springframework.beans.factory.config.CustomScopeConfigurer
* CustomScopeConfigurer}.
*
* @author Mark Fisher
* @author Chris Beams
* @author Sam Brannen
* @since 2.5
* @see org.springframework.stereotype.Component
* @see org.springframework.context.annotation.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 "";
/**
* Specifies whether a component should be configured as a scoped proxy
* and if so, whether the proxy should be interface-based or subclass-based.
* <p>Defaults to {@link ScopedProxyMode#DEFAULT}, which typically indicates
* that no scoped proxy should be created unless a different default
* has been configured at the component-scan instruction level.
* <p>Analogous to {@code <aop:scoped-proxy/>} support in Spring XML.
* @see ScopedProxyMode
*/
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}
从scopeName上的注释可以看出来scope的scopeName可以设置:
ConfigurableBeanFactory#SCOPE_PROTOTYPE
ConfigurableBeanFactory#SCOPE_SINGLETON
org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
都是一些类中的常量,含义分别如下:
- singleton: 表示组件是单例,是默认特性。也符合绝大多数的需求。
- prototype: 表示组件是非单例的,再每次从容器中请求该组件对象时都新创建一个返回。
- request: 用于web容器环境时(tomcat, jetty),每一个请求都创建一个新的实例对象。
- session: 也是在web容器环境,在同一个session范围内只创建一个新的实例对象。
注: 后两种情况比较少见,一般会使用request.setAttribute(“key”, value), session.setAttribute(“key”, value)。
实验单例bean作用域
默认情况就是单例bean,以下我们进行验证:
/**
* 测试单例bean
*/
@Slf4j
public class AnnotationSingletonScopeSample {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig.class);
MyComponent myComponent1 = context.getBean("myComponent", MyComponent.class);
MyComponent myComponent2 = context.getBean("myComponent", MyComponent.class);
System.out.println(myComponent1 == myComponent2);
}
}
// result
true
myComponent组件未加@Scope注解,是默认bean作用域,即singleton。可以看出从容器中获取两次并且引用相同,可以证明确实是单例。
实验prototype bean作用域
我们在PrototypeMyComponent类上加上@Scope注解,并设置作用域为prototype,如下:
package win.elegentjs.spring.ioc.compos;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
* 演示设置bean 作用域为原型,即每次从容器中获取组件实例时都是新new出来的
*/
@Component
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeMyComponent {
@Override
public String toString() {
return "this is a PrototypeMyComponent.";
}
}
验证一下执行结果
/**
* 测试 prototype bean
*/
@Slf4j
public class AnnotationPrototypeScopeSample {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig.class);
// 初始化spring容器后,分别获取两次组件bean,看是否相等
PrototypeMyComponent component1 = context.getBean(PrototypeMyComponent.class);
PrototypeMyComponent component2 = context.getBean(PrototypeMyComponent.class);
System.out.println(component1 == component2);
}
}
// result
false
可以看出当将组件上@Scope设置为prototype时,每次从spring容器中返回的对象都是新对象,它们的引用不相等。
注意点一:不同scope创建bean对象的时机不同
singleton的bean实例默认在容器启动时就已经实例化好了,以后需要从容器中取时直接返回即可。
而prototype的bean实例在容器启动时不会实例化,只有当需要从容器中获取时再实例化。
注意点二:单实例bean要注意线程安全
单例的bean在容器启动后整个应用共享,此时要注意这些bean中如果定义了Field, 就可能会出现线程安全问题。
注意点三:多实例bean的性能问题
因每次bean获取时都要重建,所有如果bean创建比较复杂,或者太频繁,会影响系统性能。
自定义Scope
实现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();
}
将Scope注册到容器
需要调用ConfigurableBeanFactory.registerScope方法注册scope。
/**
* Register the given scope, backed by the given Scope implementation.
* @param scopeName the scope identifier
* @param scope the backing Scope implementation
*/
void registerScope(String scopeName, Scope scope);
使用自定义的Scope作用域
在定义bean的时候可以通过设置@Scope,其中scopeName设置为自定义的Scope name。
自定义Scope实现案例
想实现一个Thread级别的bean作用域,即从同一个线程中从spring容器中取bean是单例的,不同线程之间是多例的。
- 首先实现Scope接口,如下:
package win.elegentjs.spring.ioc.scope;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义Scope, 期望能满足线程内单例
*/
public class MyThreadScope implements Scope {
// 定义scope常量,用于@Scope注解scopeName属性引用
public static final String THREAD_SCOPE = "thread";
// 初始化ThreadLocal,用于存储线程变量
private static final ThreadLocal<Map<String, Object>> threadBeanMap = ThreadLocal.withInitial(HashMap::new);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Object bean = threadBeanMap.get().get(name);
if (bean == null) {
bean = objectFactory.getObject();
threadBeanMap.get().put(name, bean);
}
return bean;
}
@Override
public Object remove(String name) {
return threadBeanMap.get().remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
// Nothing to do.
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return Thread.currentThread().getName();
}
}
- 使用自定义的scope设置组件
package win.elegentjs.spring.ioc.compos;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import win.elegentjs.spring.ioc.scope.MyThreadScope;
/**
* 演示设置bean 作用域为自定义的ThreadScope,即同线程中获取的Bean是单例的
*/
@Component
@Scope(scopeName = MyThreadScope.THREAD_SCOPE)
public class CustomScopeMyComponent {
@Override
public String toString() {
return super.toString();
}
}
- 测试自定义的scope bean
package win.elegentjs.spring.ioc.scope;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import win.elegentjs.spring.ioc.compos.CustomScopeMyComponent;
import win.elegentjs.spring.ioc.config.PersonConfig;
/**
* 测试 customer scope bean
*
* 分别新建两个线程,在每个线程中分别从spring容器中获取bean实例,
* 如果打印出来的对象值相等说明是单例
*
*/
@Slf4j
public class AnnotationCustomScopeSample {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig.class);
// 注册到beanFactory
context.getBeanFactory().registerScope(MyThreadScope.THREAD_SCOPE, new MyThreadScope());
for (int i = 0; i < 2; i ++) {
new Thread(() -> {
log.info(Thread.currentThread() + " : " + context.getBean(CustomScopeMyComponent.class));
log.info(Thread.currentThread() + " : " + context.getBean(CustomScopeMyComponent.class));
}).start();
}
}
}
// result
2021-05-21 13:16:49.077 [Thread-1] INFO w.e.spring.ioc.scope.AnnotationCustomScopeSample-Thread[Thread-1,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@2df26426
2021-05-21 13:16:49.076 [Thread-0] INFO w.e.spring.ioc.scope.AnnotationCustomScopeSample-Thread[Thread-0,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@563cf744
2021-05-21 13:16:49.079 [Thread-1] INFO w.e.spring.ioc.scope.AnnotationCustomScopeSample-Thread[Thread-1,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@2df26426
2021-05-21 13:16:49.079 [Thread-0] INFO w.e.spring.ioc.scope.AnnotationCustomScopeSample-Thread[Thread-0,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@563cf744
可以看出同一个线程实例相同。
注册自定义Scope的另一种方式
@Scope源码上有这么一段注释:
* <p>To register additional custom scopes, see
* {@link org.springframework.beans.factory.config.CustomScopeConfigurer
* CustomScopeConfigurer}.
*
即可通过CustomScopeConfigurer实现自定义Scope的注册,本质上CustomScopeConfigurer是一个BeanFactoryPostProcessor。关于它的原理和源码等讲解了BeanFactoryPostProcessor后再讲。这里先给出实现代码。
- 注册CustomScopeConfigurerbean
package win.elegentjs.spring.ioc.config;
import org.springframework.beans.factory.config.CustomScopeConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import win.elegentjs.spring.ioc.scope.MyThreadScope;
/**
* 通过CustomScopeConfigurer配置自定义Scope
*/
@Configuration
public class CustomScopeConfig {
@Bean
public CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope(MyThreadScope.THREAD_SCOPE, new MyThreadScope());
return configurer;
}
}
- 实际测试
package win.elegentjs.spring.ioc.scope;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import win.elegentjs.spring.ioc.compos.CustomScopeMyComponent;
import win.elegentjs.spring.ioc.config.CustomScopeConfig;
import win.elegentjs.spring.ioc.config.PersonConfig;
/**
* 测试 customer scope bean
*
* 采用CustomScopeConfigurer形式注册自定义Scope
*
* 分别新建两个线程,在每个线程中分别从spring容器中获取bean实例,
* 如果打印出来的对象值相等说明是单例
*
*/
@Slf4j
public class AnnotationCustomScopeSample2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomScopeConfig.class, PersonConfig.class);
for (int i = 0; i < 2; i ++) {
new Thread(() -> {
log.info(Thread.currentThread() + " : " + context.getBean(CustomScopeMyComponent.class));
log.info(Thread.currentThread() + " : " + context.getBean(CustomScopeMyComponent.class));
}).start();
}
}
}
// result:
2021-05-21 13:54:45.796 [Thread-0] INFO w.e.spring.ioc.scope.AnnotationCustomScopeSample2-Thread[Thread-0,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@30b7e195
2021-05-21 13:54:45.799 [Thread-0] INFO w.e.spring.ioc.scope.AnnotationCustomScopeSample2-Thread[Thread-0,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@30b7e195
2021-05-21 13:54:45.796 [Thread-1] INFO w.e.spring.ioc.scope.AnnotationCustomScopeSample2-Thread[Thread-1,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@1c227b50
2021-05-21 13:54:45.805 [Thread-1] INFO w.e.spring.ioc.scope.AnnotationCustomScopeSample2-Thread[Thread-1,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@1c227b50
实际效果跟使用beanFactory直接注册一致。
小结
本节主要回顾的@Scope注解的用法,参数,以及自定义扩展。
对于组件默认情况下不加@Scope就是singleton。可选择prototype表示每次从容器中取到的都是新实例。其他还有在web框架中可使用的request和session。(很少用)
当需要自定义的时候,可以可以分三步走:1)自定义Scope。 2)注册自定义Scope(两种方式)3)@Scope注解中引用自定义的Scope名称。