Spring进阶-Bean的作用域

前言

用过Spring的都知道在我们定义完BeanDefinition后,Spring会根据我们的BeanDefinition去创建实例。但是你有没有想过,Spring根据BeanDefinition创建的实例是单例还是每次都创建一个新的实例呢?假如我们现在需要保证一个单例该怎么办?如果我们要保证每次获取的实例都是新的又该怎么办?关于上面这些问题,我们可以通过设置BeanDefinition中的scope属性来解决。

Bean的作用域

可能在大多数时候我们并没有关注Bean的作用域,在默认情况下Spring创建的Bean的作用域为singleton,你可以简单的理解为单例。如果不是很明白直接看下面的示例:

public class App1 {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(App1.class);
        context.refresh();

        User mac1 = context.getBean("mac", User.class);
        User mac2 = context.getBean("mac", User.class);
        System.out.println(mac1 == mac2);

        User jack1 = context.getBean("jack", User.class);
        User jack2 = context.getBean("jack", User.class);
        System.out.println(jack1 == jack2);

    }

    @Bean
    @Scope(scopeName = "singleton")
    public User mac(){
        User user = new User();
        user.setName("mac");
        user.setAge(18);
        return user;
    }

    @Bean
    @Scope(scopeName = "prototype")
    public User jack(){
        User user = new User();
        user.setName("jack");
        user.setAge(18);
        return user;
    }
}

@Getter
@Setter
class User{
    private String name;
    private Integer age;
}

上面我们定义了两个Bean它们的名称分别为macjack,对于这两个Bean的Scope我们分别设置成singletonprototype。运行程序打印结果为truefalse,从结果可以看出不同的scope会对Bean产生不一样的影响。在Spring中默认提供了两个Scope分别为singletonprototype,如果在Web环境下还增加了4中默认scope。

  • singleton:Spring默认Scope。它表示的是在同一个容器中只会存在一个实例,它会在Spring第一次创建后缓存起来,后续再次从容器中获取时将返回缓存的对象,这种scope也是目前使用最广泛的。
  • prototype:该Scope表示每次从容器中获取实例都会创建一个新的实例。
  • request:Bean的范围控制在一次http请求周期内,该scope是web环境提供的。
  • session:Bean的范围控制在http会话周期内,该scope是web环境提供的。
  • application:Bean的范围控制在ServletContext周期内,该scope是web环境提供的。
  • websocket:Bean的范围控制在websocket周期内,该scope是web环境提供的。

如何自定义Scope

上面我们简单的介绍了Spring中默认提供的一些scope,但实际开发中Spring提供的作用域可能并不能满足我们的需求,这时候我们就可以通过自定义scope来实现我们自己的需求。

注册scope

在Spring中大多数我们自定义的东西都需要注册到Spring中才能使用,在自定义scope时同样需要如此操作。如果还有印象的话,你可能还记得在我们之前讲《Spring进阶-BeanFactory》中有提及过,ConfigurableBeanFactory提供了scope相关的接口

public interface ConfigurableBeanFactory extends HierarchicalBeanFactorySingletonBeanRegistry {
    
    void registerScope(String scopeName, Scope scope);
    
 String[] getRegisteredScopeNames();
    
 Scope getRegisteredScope(String scopeName);
}

该接口提供了注册、获取scope名称、根据scope名称获取scope相关方法。而registerScope方法的实现只有一处,它就是位于AbstractBeanFactory中,其源码如下所示:

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 + "]");
   }
  }
 }

从实现逻辑来看还是比较简单的,在内部使用一个Map类型的变量scopes存储Scope实例。同时从源码可以看出,对于singletonprototype这两个scope我们是不可以注册的。

Scope接口

上面了解了如何注册Scope,接下来就是如何自定义一个Scope。在Spring中提供了一个接口Scope,实现这个接口的方法就可以自定义一个Scope。

public interface Scope {
 //从该Scope中获取实例,如果获取不到则通过objectFactory创建。这个方法通常来说是需要我们实现的
 Object get(String name, ObjectFactory<?> objectFactory);
 //从该Scope中删除实例
 Object remove(String name);
 //注册实例销毁逻辑
 void registerDestructionCallback(String name, Runnable callback);
 //用于解析相应上下文中的数据,例如在request域中可以用来返回request中的属性
 Object resolveContextualObject(String key);
 //作用域中的会员标志,例如session作用域中就是sessionId
 String getConversationId();
}

SimpleThreadScope

通过前面两点我们知道了如何自定义一个Scope。现在我们来自己实现一个简单的Scope,用这个例子来说明。在Spring中默认提供了一个SimpleThreadScope作用域,但是默认情况下该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();
  // NOTE: Do NOT modify the following to use Map::computeIfAbsent. For details,
  // see https://github.com/spring-projects/spring-framework/issues/25801.
  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();
 }
}

从代码的实现可以看出,SimpleThreadScope是一个通过ThreadLocal实现的Scope的。简单的说就是对于同一个线程从容器中获取的Bean实例都会是相同的,而不同的线程获取的Bean实例都是不同的。这个结论我们在后面的实例代码中会给出。接下来我们就是将这个Scope注册到Spring中,然后使用就可以了。

public class App2 {
    private static final String THREAD_LOCAL_SCOPE = "thread_local_scope";
    public static void main(String[] args) {
        //创建IOC容器
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        //注册作用域
        factory.registerScope(THREAD_LOCAL_SCOPE,new SimpleThreadScope());
        //读取配置
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        reader.loadBeanDefinitions("spring-scope-1.xml");

        //同一个线程获取的是相同的
        Student mainStudent1 = factory.getBean("student", Student.class);
        Student mainStudent2 = factory.getBean("student", Student.class);
        System.out.println("mainStudent1 == mainStudent2 => " + (mainStudent1 == mainStudent2));

        //不同线程获取的是不同
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Student t1Student1 = factory.getBean("student", Student.class);
                System.out.println("mainStudent1 == t1Student1 => " + (mainStudent1 == t1Student1));
                System.out.println(mainStudent1);
                System.out.println(t1Student1);
            }
        });
        t1.setName("t1");
        t1.start();

    }
}


@Getter
@Setter
@ToString
class Student{
    private String name;
    private String classNo;

    public Student() {
        this.name = Thread.currentThread().getName();
        this.classNo = "测试班级";
    }
}
  • spring-scope-1.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd"
>

    
    <bean id="student" class="com.buydeem.share.scope.Student" scope="thread_local_scope"/>
</beans>
  • 运行结果
mainStudent1 == mainStudent2 => true
mainStudent1 == t1Student1 => false
Student(name=main, classNo=测试班级)
Student(name=t1, classNo=测试班级)

上面的示例很简单,我们在XML中定义了一个Student,同时我们将它的作用域设置为thread_local_scope,这个作用域就是我们自己定义的Scope。在Java示例代码中,我们创建完IOC容器后,然后将自定义的scope注册到容器中。从运行结果可以看出,对于同一个线程(main)而言,它们从容器中获取的实例都是同一个。而对于不同线程(t1和main)它们获取的实例最后比较发现是不一样。

scope对依赖注入的影响

在前面讲《Spring进阶-依赖注入》一文中没有提到这一点,这里正好讲到了Scope所以一起来说明一下。如果两个Bean的作用域相同,相同作用域之间的相互依赖基本上没有问题。但如果两个Bean的作用域不相同,如果不做特殊处理那么很可能会出现一些怪异的问题。在依赖注入时,Bean只会在实例化时注入它的依赖Bean。这样会导致的一个问题是,如果作用域短的Bean被注入到作用域长的Bean中则会产生一些奇怪的问题。可能语言表达的难以理解,直接上代码。

  • Java示例代码
public class App3 {
    public static void main(String[] args) {
        //创建IOC容器
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        //读取配置
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        reader.loadBeanDefinitions("spring-scope-2.xml");

        PersonService ps1 = factory.getBean("personService", PersonService.class);
        for (int i = 0; i < 3; i++) {
            System.out.println(ps1.getPerson());
        }

        for (int i = 0; i < 3; i++) {
            Person person = factory.getBean("person", Person.class);
            System.out.println(person);
        }

    }
}

@Getter
@Setter
@ToString
class Person{
    private static final AtomicInteger COUNTER = new AtomicInteger();
    private static final String NAME_TEMPLATE = "user_%d";
    private String name;

    public Person() {
        this.name = String.format(NAME_TEMPLATE,COUNTER.getAndIncrement());
    }
}


@Getter
@Setter
class PersonService{

    private Person person;

}
  • spring-scope-2.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd"
>


    <bean id="person" class="com.buydeem.share.scope.Person" scope="prototype"/>

    <bean id="personService" class="com.buydeem.share.scope.PersonService" scope="singleton">
        <property name="person" ref="person"/>
    </bean>

</beans>

上面的示例很简单,在XML配置文件中我们配置了两个Bean分别为personpersonService,对于这两个Bean我们设置它们的scope分别为prototypesingleton。我们将作用域短的person注入到作用域长的personSerivce中,现在我想问的是最后程序的运行结果是什么?

Person(name=user_0)
Person(name=user_0)
Person(name=user_0)
Person(name=user_1)
Person(name=user_2)
Person(name=user_3)

从运行结果我们可以看出,通过PersonService实例获取的Person实例都是同一个,而直接从IOC容器中获取的并不是同一个实例。这就是Scope对依赖注入的影响。这个问题的产生原因就是依赖注入只会进行一次,所以我们通过PersonService获取的就是第一次注入的对象,这就导致了我们即使设置了scope为prototype它也不会生效。对于这个问题如何解决,Spring官方给出了下面几种解决方案。

BeanFactoryAware和ApplicationContextAware

这种方式比较简单,就是通过实现BeanFactoryAware或者ApplicationContextAware能获取到IOC容器,而依赖注入项我们直接从IOC容器中获取。不过这种方式Spring官方并不是特别推荐。

Lookup Method Injection

该方式是通过查找方法注入依赖,内部原理就是通过CGLIB来生成子类,然后重写方法来获取依赖项。我们修改代码如下:

@Getter
class PersonService{

    private Person person;

    public Person getPerson(){
        return createPerson();
    }

    protected Person createPerson(){
        return null;
    }
}

配置文件修改如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd"
>


    <bean id="person" class="com.buydeem.share.scope.Person" scope="prototype"/>

    <bean id="personService" class="com.buydeem.share.scope.PersonService" scope="singleton">
        <lookup-method name="createPerson" bean="person"/>
    </bean>

</beans>

再次运行代码,运行结果如下:

Person(name=user_0)
Person(name=user_1)
Person(name=user_2)
Person(name=user_3)
Person(name=user_4)
Person(name=user_5)

从运行结果可以看出,即使是通过PersonService实例获取Person实例,它每次返回的都不是同一个对象。

scoped-proxy

对于上面的第二种方式可能你不太理解,我们换用这种方式。我们可以直接在person定义中增加如下配置:

<aop:scoped-proxy proxy-target-class="true"/>

整个配置文件修改如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd"
>


    <bean id="person" class="com.buydeem.share.scope.Person" scope="prototype">
        <aop:scoped-proxy proxy-target-class="true"/>
    </bean>

    <bean id="personService" class="com.buydeem.share.scope.PersonService" scope="singleton">
        <property name="person" ref="person"/>
    </bean>

</beans>

对于PersonService方法我们还是保持与之前定义的一致,再次运行代码可以发现,这种方式同样能解决问题。这种方式与Lookup Method Injection有点类似,都是通过动态代理来生成代理类。你可以这么理解,在依赖注入的时候注入的并不是一个Person类,而是一个Person的一个代理类。其实你通过代码打印出Person实例的类,从类名可以看出它们其实是一个代理类。而Java的动态代理我们都知道有两种方式来实现,一个是JDK提供的通过接口的方式,另一种则是CGLIB库提供的生成子类的方式。而proxy-target-class则可以控制是哪种方式。

小结

关于Spring的Scope相关知识目前只介绍到这里,当然还有其他很多点没有说到。后续如果有涉及到该部分内容,我们后续再聊。

其实不是不想说了,是因为周五我要下班了!      — 来自某一个菜鸟程序员


原文始发于微信公众号(一只菜鸟程序员):Spring进阶-Bean的作用域

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/72967.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!