常見的scope
Spring及其其他組件提供了多種Scope,但是我們在使用Spring和他們的組件時用的最多的Scope只有幾個。
- singleton:Spring默認的Scope,表示在Spring同一容器中只會存在一個實例,它會在Spring第一次創(chuàng)建完成之后緩存起來,后面都不會再創(chuàng)建,再次獲取時會從緩存中獲取。這個也是目前使用最多的一個Scope。
- prototype:該Scope表示每次獲取該范圍內(nèi)的實例都會生成一個新的實例。
- request:表示每個request作用域內(nèi)只會創(chuàng)建一次。
- session:表示在每個session作用域內(nèi)只會創(chuàng)建一次。
- application:表示在ServletContext作用域內(nèi)只會創(chuàng)建一次。
上面這些作用域就是我們平常接觸過最多的作用域了,而在Spring中只提供singleton和prototype兩種,而后面的三種都是在web環(huán)境中才提供的。
設置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。下面我們從容器中循環(huán)獲取5次實例,看看每次獲取的實例結(jié)果有什么不同。
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
從打印的結(jié)果可以看出,scope為singleton的實例每次獲取的都是同一個對象,而prototype每次獲取的實例都是不同的。
如何自定義Scope
如果Spring提供的Scope無法滿足我們的要求,我們是可以自定義Scope的。在說如何定義Scope之前我們了解下面幾個知識點。
如何注冊Scope
自定義的Scope如果只是創(chuàng)建肯定是沒有用,要想自定義的Scope生效必須先將自定義的Scope注冊到容器中。Spring中提供Scope注冊的接口為ConfigurableBeanFactory,該接口中定義了注冊Scope的方法,定義如下:
void registerScope(String scopeName, Scope scope);
該接口的只需要我們提供Scope的名字和Scope實例對象即可,而它的實現(xiàn)在AbstractBeanFactory中,且在Spring中只有一處實現(xiàn),具體實現(xiàn)如下:
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我們可以自己定義,且還能覆蓋之前的實現(xiàn)。而它的內(nèi)部只是使用了一個LinkedHashMap來存放這些Scope。
同樣我們還可以使用CustomScopeConfigurer來完成自定義Scope的注冊。查看其源碼,其核心還是通過registerScope該方法來向容器注冊Scope。該類實現(xiàn)了BeanFactoryPostProcessor接口,而實現(xiàn)了該接口的類可以在BeanFactory實例化之后對象還沒創(chuàng)建之前執(zhí)行我們自己的擴展。通過postProcessBeanFactory方法的實現(xiàn)可以了解該類是如何將自定義的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");
}
});
}
}
通過內(nèi)部實現(xiàn),可以看見其最后還是調(diào)用的registerScope方法對我們定義的Scope進行注冊。
所以我們在自定義Scope的時候,既可以自己調(diào)用registerScope方法手動注冊,同樣還可以使用CustomScopeConfigurer來完成自定義Scope的注冊。
Scope接口
如果想自定義Scope,我們自定義的Scope就必須實現(xiàn)Scope接口。該接口的定義如下:
public interface Scope {
/**
* 獲取該Scope中的實例,如果不存在,則會調(diào)用objectFactory創(chuàng)建
*/
Object get(String name, ObjectFactory<?> objectFactory);
/**
* 刪除該Scope中的實例
*/
@Nullable
Object remove(String name);
/**
* 注冊實例銷毀回調(diào)邏輯
*/
void registerDestructionCallback(String name, Runnable callback);
/**
* 用于解析相應的上下文數(shù)據(jù),比如request作用域?qū)⒎祷豶equest中的屬性
*/
@Nullable
Object resolveContextualObject(String key);
/**
* 作用域的會話標識,比如session作用域?qū)⑹莝essionId
*/
@Nullable
String getConversationId();
}
通常我們自定義Scope主要就是實現(xiàn)get和remove方法,而其他方法我們可以根據(jù)自己的情況來決定要不要實現(xiàn)。
ScopedProxyMode-作用域代理模式
在Scope的注解中有一個proxyMode可以設置,該值主要用來設置代理模式。該值在Spring中有四個取值,可以通過ScopedProxyMode枚舉類查看所有的取值。其中No和DEFAULT取值的效果相同,另外還有兩種分別為INTERFACES和TARGET_CLASS。INTERFACES代表使用JDK原生的方式來實現(xiàn)動態(tài)代理,而TARGET_CLASS代表使用CGLIB來實現(xiàn)動態(tài)代理。
知道了ScopeProxyMode四種值的區(qū)別,但是為什么要用代理呢?例如在Web環(huán)境中,我定義了一個Person的是Scope為session,也就是說對于同一個會話獲取到的Person實例是同一個。但是現(xiàn)在存在一個問題,我應用啟動時,這個時候沒有用戶訪問,如果我把Person實例注入到別的實例中,這個時候豈不是不能注入了。但是實際情況是可以的,而且注入的那個Person實例還不是null。實際上Spring注入的是一個代理對象,而這個代理對象是通過JDK還是CGLIB實現(xiàn)的,這個就取決于我們設置的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());
}
}
程序啟動后,可以看見打印出來的結(jié)果如下:
class com.buydeem.springbootdemo.service.Person$$EnhancerBySpringCGLIB$$32dc2cee
從打印的結(jié)果可以看出,它是代理對象。上面Person中的Scope是我們手動設置的ScopeName和proxyMode屬性值,SpringWeb中其實已經(jīng)提供了@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代理的不同,還請自行查詢資料了解。
實現(xiàn)自定義Scope
如何實現(xiàn)自定義Scope簡單的說就只有兩步,第一步就對Scope接口進行實現(xiàn),第二部就是將自定義的Scope注冊到容器中。這里我就以SimpleThreadScope作為示例來演示具體如何實現(xiàn)。該類是Spring中提供的一個基于ThreadLocal實現(xiàn)的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實現(xiàn)的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{
/**
* 創(chuàng)建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();
}
}
最后打印的結(jié)果如下:
main 線程中的user:com.buydeem.scope.User@76494737,是否相等:true
t2 線程中的user:com.buydeem.scope.User@45d33648,是否相等:true
t1 線程中的user:com.buydeem.scope.User@4a418f9c,是否相等:true
從上面結(jié)果可以看出,同一個線程獲取的實例是相同的,而不同的線程獲取的實例是不同的。