Spring之Bean的作用域
常见的scope
Spring及其其他组件提供了多种Scope,但是我们在使用Spring和他们的组件时用的最多的Scope只有几个。
- singleton:Spring默认的Scope,表示在Spring同一容器中只会存在一个实例,它会在Spring第一次创建完成之后缓存起来,后面都不会再创建,再次获取时会从缓存中获取。这个也是目前使用最多的一个Scope。
- prototype:该Scope表示每次获取该范围内的实例都会生成一个新的实例。
- request:表示每个request作用域内只会创建一次。
- session:表示在每个session作用域内只会创建一次。
- application:表示在ServletContext作用域内只会创建一次。
上面这些作用域就是我们平常接触过最多的作用域了,而在Spring中只提供singleton和prototype两种,而后面的三种都是在web环境中才提供的。
设置Bean的作用域
在定义Spring Bean的时候可以设置Bean的作用域,常用的就是在xml定义Bean的时候设置或者使用@Scope在注解中设置Bean的作用域。
xml设置:
<bean id="person" class="com.buydeem.bean.Person" scope="singleton">
<constructor-arg name="name" value="mac"/>
<constructor-arg name="age" value="18"/>
</bean>
@Scope设置:
@Bean
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON)
public User user(){
User user = new User();
user.setName("mac");
user.setAge(18);
return user;
}
使用示例
public class ScopeDemo1 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(ConfigClazz.class);
context.refresh();
for (int i = 0; i < 5; i++) {
User mac = (User) context.getBean("mac");
User tom = (User) context.getBean("tom");
System.out.println("mac = "+mac);
System.out.println("tom = "+tom);
}
}
}
class ConfigClazz{
@Bean
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON)
public User mac(){
User user = new User();
user.setName("mac");
user.setAge(18);
return user;
}
@Bean
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public User tom(){
User user = new User();
user.setName("tom");
user.setAge(19);
return user;
}
}
@Getter
@Setter
class User {
private String name;
private Integer age;
}
上面我们定义了两个User类型的Bean,但是它们的Scope是不一样的,一个为singleton,另一个为prototype。下面我们从容器中循环获取5次实例,看看每次获取的实例结果有什么不同。
mac = com.buydeem.scope.User@6e9a5ed8
tom = com.buydeem.scope.User@7e057f43
mac = com.buydeem.scope.User@6e9a5ed8
tom = com.buydeem.scope.User@6c284af
mac = com.buydeem.scope.User@6e9a5ed8
tom = com.buydeem.scope.User@5890e879
mac = com.buydeem.scope.User@6e9a5ed8
tom = com.buydeem.scope.User@6440112d
mac = com.buydeem.scope.User@6e9a5ed8
tom = com.buydeem.scope.User@31ea9581
从打印的结果可以看出,scope为singleton的实例每次获取的都是同一个对象,而prototype每次获取的实例都是不同的。
如何自定义Scope
如果Spring提供的Scope无法满足我们的要求,我们是可以自定义Scope的。在说如何定义Scope之前我们了解下面几个知识点。
如何注册Scope
自定义的Scope如果只是创建肯定是没有用,要想自定义的Scope生效必须先将自定义的Scope注册到容器中。Spring中提供Scope注册的接口为ConfigurableBeanFactory,该接口中定义了注册Scope的方法,定义如下:
void registerScope(String scopeName, Scope scope);
该接口的只需要我们提供Scope的名字和Scope实例对象即可,而它的实现在AbstractBeanFactory中,且在Spring中只有一处实现,具体实现如下:
public void registerScope(String scopeName, Scope scope) {
Assert.notNull(scopeName, "Scope identifier must not be null");
Assert.notNull(scope, "Scope must not be null");
if (SCOPE_SINGLETON.equals(scopeName) || SCOPE_PROTOTYPE.equals(scopeName)) {
throw new IllegalArgumentException("Cannot replace existing scopes 'singleton' and 'prototype'");
}
Scope previous = this.scopes.put(scopeName, scope);
if (previous != null && previous != scope) {
if (logger.isDebugEnabled()) {
logger.debug("Replacing scope '" + scopeName + "' from [" + previous + "] to [" + scope + "]");
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Registering scope '" + scopeName + "' with implementation [" + scope + "]");
}
}
}
从源码我们可以了解到,对于singleton和prototype这两个Scope我们是不能自定义的,而其他的Scope我们可以自己定义,且还能覆盖之前的实现。而它的内部只是使用了一个LinkedHashMap来存放这些Scope。
同样我们还可以使用CustomScopeConfigurer来完成自定义Scope的注册。查看其源码,其核心还是通过registerScope该方法来向容器注册Scope。该类实现了BeanFactoryPostProcessor接口,而实现了该接口的类可以在BeanFactory实例化之后对象还没创建之前执行我们自己的扩展。通过postProcessBeanFactory方法的实现可以了解该类是如何将自定义的Scope注册到BeanFactory中的。
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (this.scopes != null) {
this.scopes.forEach((scopeKey, value) -> {
if (value instanceof Scope) {
beanFactory.registerScope(scopeKey, (Scope) value);
}
else if (value instanceof Class) {
Class<?> scopeClass = (Class<?>) value;
Assert.isAssignable(Scope.class, scopeClass, "Invalid scope class");
beanFactory.registerScope(scopeKey, (Scope) BeanUtils.instantiateClass(scopeClass));
}
else if (value instanceof String) {
Class<?> scopeClass = ClassUtils.resolveClassName((String) value, this.beanClassLoader);
Assert.isAssignable(Scope.class, scopeClass, "Invalid scope class");
beanFactory.registerScope(scopeKey, (Scope) BeanUtils.instantiateClass(scopeClass));
}
else {
throw new IllegalArgumentException("Mapped value [" + value + "] for scope key [" +
scopeKey + "] is not an instance of required type [" + Scope.class.getName() +
"] or a corresponding Class or String value indicating a Scope implementation");
}
});
}
}
通过内部实现,可以看见其最后还是调用的registerScope方法对我们定义的Scope进行注册。
所以我们在自定义Scope的时候,既可以自己调用registerScope方法手动注册,同样还可以使用CustomScopeConfigurer来完成自定义Scope的注册。
Scope接口
如果想自定义Scope,我们自定义的Scope就必须实现Scope接口。该接口的定义如下:
public interface Scope {
/**
* 获取该Scope中的实例,如果不存在,则会调用objectFactory创建
*/
Object get(String name, ObjectFactory<?> objectFactory);
/**
* 删除该Scope中的实例
*/
@Nullable
Object remove(String name);
/**
* 注册实例销毁回调逻辑
*/
void registerDestructionCallback(String name, Runnable callback);
/**
* 用于解析相应的上下文数据,比如request作用域将返回request中的属性
*/
@Nullable
Object resolveContextualObject(String key);
/**
* 作用域的会话标识,比如session作用域将是sessionId
*/
@Nullable
String getConversationId();
}
通常我们自定义Scope主要就是实现get和remove方法,而其他方法我们可以根据自己的情况来决定要不要实现。
ScopedProxyMode-作用域代理模式
在Scope的注解中有一个proxyMode可以设置,该值主要用来设置代理模式。该值在Spring中有四个取值,可以通过ScopedProxyMode枚举类查看所有的取值。其中No和DEFAULT取值的效果相同,另外还有两种分别为INTERFACES和TARGET_CLASS。INTERFACES代表使用JDK原生的方式来实现动态代理,而TARGET_CLASS代表使用CGLIB来实现动态代理。
知道了ScopeProxyMode四种值的区别,但是为什么要用代理呢?例如在Web环境中,我定义了一个Person的是Scope为session,也就是说对于同一个会话获取到的Person实例是同一个。但是现在存在一个问题,我应用启动时,这个时候没有用户访问,如果我把Person实例注入到别的实例中,这个时候岂不是不能注入了。但是实际情况是可以的,而且注入的那个Person实例还不是null。实际上Spring注入的是一个代理对象,而这个代理对象是通过JDK还是CGLIB实现的,这个就取决于我们设置的ScopeProxyMode的值了。
Person的定义如下:
@Component
@Scope(scopeName = "session",proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Person {
}
ScopeProxyModeTest定义如下:
@Component
public class ScopeProxyModeTest {
@Autowired
private Person person;
@PostConstruct
public void init(){
System.out.println(person.getClass());
}
}
程序启动后,可以看见打印出来的结果如下:
class com.buydeem.springbootdemo.service.Person$$EnhancerBySpringCGLIB$$32dc2cee
从打印的结果可以看出,它是代理对象。上面Person中的Scope是我们手动设置的ScopeName和proxyMode属性值,SpringWeb中其实已经提供了@SessionScope注解,其定义如下:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {
/**
* Alias for {@link Scope#proxyMode}.
* <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
*/
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
从SessionScope注解的定义也可以看出,它默认指定的proxyMode就是TARGET_CLASS,也就是使用CGLIB的方式。关于JDK和CGLIB代理的不同,还请自行查询资料了解。
实现自定义Scope
如何实现自定义Scope简单的说就只有两步,第一步就对Scope接口进行实现,第二部就是将自定义的Scope注册到容器中。这里我就以SimpleThreadScope作为示例来演示具体如何实现。该类是Spring中提供的一个基于ThreadLocal实现的Scope,源码如下:
public class SimpleThreadScope implements Scope {
private static final Log logger = LogFactory.getLog(SimpleThreadScope.class);
private final ThreadLocal<Map<String, Object>> threadScope =
new NamedThreadLocal<Map<String, Object>>("SimpleThreadScope") {
@Override
protected Map<String, Object> initialValue() {
return new HashMap<>();
}
};
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = this.threadScope.get();
Object scopedObject = scope.get(name);
if (scopedObject == null) {
scopedObject = objectFactory.getObject();
scope.put(name, scopedObject);
}
return scopedObject;
}
@Override
@Nullable
public Object remove(String name) {
Map<String, Object> scope = this.threadScope.get();
return scope.remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
logger.warn("SimpleThreadScope does not support destruction callbacks. " +
"Consider using RequestScope in a web environment.");
}
@Override
@Nullable
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return Thread.currentThread().getName();
}
}
从源码中可以知道这是一个基于ThreadLocal实现的Scope,它的效果就是单同一个线程获取该域中的实例会是相同的,而其他线程获取的则是不同的。
Spring并没有将该Scope注册到容器中,所以我们在使用时需要自己手动将该Scope注入到容器中,注册和使用的代码如下:
public class ScopeDemo2 {
public static void main(String[] args) throws InterruptedException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Demo2Config.class);
context.refresh();
User user = (User) context.getBean("user");
System.out.printf("main 线程中的user:%s,是否相等:%s\n",user,user == context.getBean("user"));
new Thread(()->{
User user_t1 = (User) context.getBean("user");
System.out.printf("t1 线程中的user:%s,是否相等:%s\n",user_t1,user_t1 == context.getBean("user"));
},"t1").start();
new Thread(()->{
User user_t2 = (User) context.getBean("user");
System.out.printf("t2 线程中的user:%s,是否相等:%s\n",user_t2,user_t2 == context.getBean("user"));
},"t2").start();
Thread.sleep(1000L);
}
}
class Demo2Config{
/**
* 创建CustomScopeConfigurer实例,注册SimpleThreadScope域
* @return
*/
@Bean
public CustomScopeConfigurer customScopeConfigurer(){
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
//该域名与Bean中指定的域名需要保持一致
configurer.addScope("threadScope",new SimpleThreadScope());
return configurer;
}
@Bean
//域名与注册的域名保持一致
@Scope(scopeName = "threadScope")
public User user(){
return new User();
}
}
最后打印的结果如下:
main 线程中的user:com.buydeem.scope.User@76494737,是否相等:true
t2 线程中的user:com.buydeem.scope.User@45d33648,是否相等:true
t1 线程中的user:com.buydeem.scope.User@4a418f9c,是否相等:true
从上面结果可以看出,同一个线程获取的实例是相同的,而不同的线程获取的实例是不同的。