Spring's Scope scope, how to play?

Get into the habit of writing together! This is the 11th day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

The components in the Spring container are singleton by default. When Spring starts, these objects are instantiated and initialized, and placed in the Spring container. After that, each time an object is obtained, it is obtained directly from the Spring container, instead of Create objects. What if you have to create a new instance object every time you get an object from the Spring container? At this point, you need to use the @Scope annotation to set the scope of the component.

The @Scope annotation can set the scope of the component. Let's first look at the source code of the @Scope annotation class, as shown below:

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;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {
	@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 "";
    
	ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}
复制代码

As can be seen from the source code, the following values ​​can be set in the @Scope annotation.

ConfigurableBeanFactory#SCOPE_PROTOTYPE
ConfigurableBeanFactory#SCOPE_SINGLETON
org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
复制代码

First, we entered the ConfigurableBeanFactory interface and found that there are two constant definitions in the ConfigurableBeanFactory class, as shown below.

public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
	String SCOPE_SINGLETON = "singleton";
	String SCOPE_PROTOTYPE = "prototype";
    /*****************此处省略N多行代码*******************/
}
复制代码

Then, the SCOPE_REQUEST and SCOPE_SESSION in the WebApplicationContext class is that when we use the Web container to run the Spring application, the values ​​of SCOPE_REQUEST and SCOPE_SESSION in the WebApplicationContext class can be set in the @Scope annotation, and the value of SCOPE_REQUEST is the request, and the value of SCOPE_SESSION is the session. .

In summary, the values ​​in the @Scope annotation are as follows.

  • singleton: Indicates that the component is a single instance in the Spring container. This is the default value of Spring. Spring will instantiate the component and load it into the Spring container when it starts. After that, every time a component is obtained from the Spring container, Return the instance object directly without having to create the instance object again. Get the object from the Spring container, the friends can understand that the object is obtained from the Map object.
  • prototype:表示组件在Spring容器中是多实例的,Spring在启动的时候并不会对组件进行实例化操作,而是每次从Spring容器中获取组件对象时,都会创建一个新的实例对象并返回。
  • request:每次请求都会创建一个新的实例对象,request作用域用在spring容器的web环境中。
  • session:在同一个session范围内,创建一个新的实例对象,也是用在web环境中。
  • application:全局web应用级别的作用于,也是在web环境中使用的,一个web应用程序对应一个bean实例,通常情况下和singleton效果类似的,不过也有不一样的地方,singleton是每个spring容器中只有一个bean实例,一般我们的程序只有一个spring容器,但是,一个应用程序中可以创建多个spring容器,不同的容器中可以存在同名的bean,但是sope=aplication的时候,不管应用中有多少个spring容器,这个应用中同名的bean只有一个。

其中,request和session作用域是需要Web环境支持的,这两个值基本上使用不到,如果我们使用Web容器来运行Spring应用时,如果需要将组件的实例对象的作用域设置为request和session,我们通常会使用request.setAttribute("key",object)和session.setAttribute("key", object)的形式来将对象实例设置到request和session中,通常不会使用@Scope注解来进行设置。

作用域测试

@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)//默认值
@Bean
public Person person04(){
  System.out.println("person04--singleton");
  return new Person();
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)//多例模式
@Bean
public Person person05(){
  System.out.println("person05--prototype");
  return new Person();
}
复制代码
@Test
public void beanScopeTestPrototype(){//容器加载未实例化,每次调用进行实例化
  //接口回调
  ApplicationContext context = new AnnotationConfigApplicationContext(SpringAnnotationConfig.class);
  //多例模式获取对象
  Person p5_01 = context.getBean("person05",Person.class);
  Person p5_02 = context.getBean("person05",Person.class);
  System.out.println(p5_01);
  System.out.println(p5_01);
  System.out.println(p5_01==p5_02);//false
}

@Test
public void beanScopeTest(){//容器初始化,就进行实例化
  //接口回调
  ApplicationContext context = new AnnotationConfigApplicationContext(SpringAnnotationConfig.class);
  //默认情况下,是单例模式(static)
  Person p4_01 = context.getBean("person04",Person.class);
  Person p4_02 = context.getBean("person04",Person.class);
  System.out.println(p4_01);
  System.out.println(p4_02);
  System.out.println(p4_01==p4_02);//true
}
复制代码
  • 单实例bean作用域何时创建对象?

Spring容器在启动时,将单实例组件实例化之后,加载到Spring容器中,以后每次从容器中获取组件实例对象,直接返回相应的对象,而不必在创建新对象。

  • 多实例bean作用域何时创建对象?

当对象是多实例时,每次从Spring容器中获取对象时,都会创建新的实例对象,并且每个实例对象都不相等。

注意的事项

  • 单例模式

单例bean是整个应用共享的,所以需要考虑到线程安全问题,之前在玩springmvc的时候,springmvc中controller默认是单例的,有些开发者在controller中创建了一些变量,那么这些变量实际上就变成共享的了,controller可能会被很多线程同时访问,这些线程并发去修改controller中的共享变量,可能会出现数据错乱的问题;所以使用的时候需要特别注意。

  • 多例模式

多例bean每次获取的时候都会重新创建,如果这个bean比较复杂,创建时间比较长,会影响系统的性能,这个地方需要注意。

自定义Scope

如果Spring内置的几种sope都无法满足我们的需求的时候,我们可以自定义bean的作用域。

1.如何实现自定义Scope

自定义Scope主要分为三个步骤,如下所示。

(1)实现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();

}
复制代码

(2)将Scope注册到容器

需要调用org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope的方法,看一下这个方法的声明

/**
* 向容器中注册自定义的Scope
*scopeName:作用域名称
* scope:作用域对象
**/
void registerScope(String scopeName, Scope scope);
复制代码

(3)使用自定义的作用域

定义bean的时候,指定bean的scope属性为自定义的作用域名称。

2.自定义Scope实现案例

例如,我们来实现一个线程级别的bean作用域,同一个线程中同名的bean是同一个实例,不同的线程中的bean是不同的实例。

这里,要求bean在线程中是共享的,所以我们可以通过ThreadLocal来实现,ThreadLocal可以实现线程中数据的共享。

此时,我们在io.mykit.spring.plugins.register.scope包下新建ThreadScope类,如下所示。

package com.hanpang.spring.scope;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.lang.Nullable;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 自定义本地线程级别的bean作用域,不同的线程中对应的bean实例是不同的,同一个线程中同名的bean是同一个实例
 */
public class ThreadScope implements Scope {

    public static final String THREAD_SCOPE = "thread";

    private ThreadLocal<Map<String, Object>> beanMap = new ThreadLocal() {
        @Override
        protected Object initialValue() {
            return new HashMap<>();
        }
    };

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object bean = beanMap.get().get(name);
        if (Objects.isNull(bean)) {
            bean = objectFactory.getObject();
            beanMap.get().put(name, bean);
        }
        return bean;
    }

    @Nullable
    @Override
    public Object remove(String name) {
        return this.beanMap.get().remove(name);
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        //bean作用域范围结束的时候调用的方法,用于bean清理
        System.out.println(name);
    }

    @Nullable
    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Nullable
    @Override
    public String getConversationId() {
        return Thread.currentThread().getName();
    }
}
复制代码

在ThreadScope类中,我们定义了一个常量THREAD_SCOPE,在定义bean的时候给scope使用。

Next, we create the PersonConfig class under the com.hanpang.config package, and use the @Scope("thread") annotation to mark the scope of the Person object as the Thread scope, as shown below.

package com.hanpang.config;

import com.hanpang.model.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

/**
 * @author binghe
 * @version 1.0.0
 * @description 测试@Scope注解设置的作用域
 */
@Configuration
public class PersonConfig {

    @Scope("thread")
    @Bean("person")
    public Person person(){
        System.out.println("给容器中添加Person....");
        return new Person("binghe002", 18);
    }
}
复制代码

Finally, we create the testAnnotationConfig4() method in the SpringBeanTest class, create the Spring container in the testAnnotationConfig4() method, and register the ThreadScope object with the Spring container. Next, use a loop to create two Thread threads, and in each thread respectively Get two Person objects as shown below.

@Test
public void testAnnotationConfig4(){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig3.class);
    //向容器中注册自定义的scope
    context.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());

    //使用容器获取bean
    for (int i = 0; i < 2; i++) { 
        new Thread(() -> {
            System.out.println(Thread.currentThread() + "," + context.getBean("person"));
            System.out.println(Thread.currentThread() + "," + context.getBean("person"));
        }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

As you can see from the output, the bean gets the same bean instance in the same thread, and the bean instance in different threads is different.

Note: Here, I adjusted the Person class accordingly, removed Lombok's annotation, and manually wrote the constructor and setter and getter methods, as shown below.

package com.hanpang.bean;

import java.io.Serializable;

public class Person implements Serializable {
    private static final long serialVersionUID = 7387479910468805194L;
    private String name;
    private Integer age;

    public Person() {
    }

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}
复制代码

Guess you like

Origin juejin.im/post/7085263304460861448