Spring之Bean的作用域

2021-03-23  本文已影响0人  一个菜鸟JAVA

常见的scope

Spring及其其他组件提供了多种Scope,但是我们在使用Spring和他们的组件时用的最多的Scope只有几个。

  1. singleton:Spring默认的Scope,表示在Spring同一容器中只会存在一个实例,它会在Spring第一次创建完成之后缓存起来,后面都不会再创建,再次获取时会从缓存中获取。这个也是目前使用最多的一个Scope。
  2. prototype:该Scope表示每次获取该范围内的实例都会生成一个新的实例。
  3. request:表示每个request作用域内只会创建一次。
  4. session:表示在每个session作用域内只会创建一次。
  5. 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

从上面结果可以看出,同一个线程获取的实例是相同的,而不同的线程获取的实例是不同的。

上一篇 下一篇

猜你喜欢

热点阅读