スプリングのスコープスコープ、遊び方は?

一緒に書く習慣を身につけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加して11日目です。クリックしてイベントの詳細をご覧ください

Springコンテナ内のコンポーネントは、デフォルトではシングルトンです。Springが起動すると、これらのオブジェクトはインスタンス化および初期化され、Springコンテナに配置されます。その後、オブジェクトが取得されるたびに、オブジェクトはSpringコンテナから直接取得されます。オブジェクトを作成します。Springコンテナからオブジェクトを取得するたびに新しいインスタンスオブジェクトを作成する必要がある場合はどうなりますか?この時点で、@Scopeアノテーションを使用してコンポーネントのスコープを設定する必要があります。

@Scopeアノテーションは、コンポーネントのスコープを設定できます。まず、以下に示すように、@Scopeアノテーションクラスのソースコードを見てみましょう。

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;
}
复制代码

ソースコードからわかるように、@Scopeアノテーションには次の値を設定できます。

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

まず、ConfigurableBeanFactoryインターフェイスに入り、次に示すように、ConfigurableBeanFactoryクラスに2つの定数定義があることがわかりました。

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

次に、WebApplicationContextクラスのSCOPE_REQUESTとSCOPE_SESSIONは、Webコンテナを使用してSpringアプリケーションを実行するときに、WebApplicationContextクラスのSCOPE_REQUESTとSCOPE_SESSIONの値を@Scopeアノテーションで設定でき、SCOPE_REQUESTの値を設定できます。はリクエストであり、SCOPE_SESSIONの値はセッションです。

要約すると、@Scopeアノテーションの値は次のとおりです。

  • singleton:コンポーネントがSpringコンテナ内の単一インスタンスであることを示します。これはSpringのデフォルト値です。Springはコンポーネントをインスタンス化し、起動時にSpringコンテナにロードします。その後、コンポーネントがSpringコンテナ、インスタンスオブジェクトを再度作成せずに、インスタンスオブジェクトを直接返します。Springコンテナからオブジェクトを取得します。友人は、オブジェクトがMapオブジェクトから取得されていることを理解できます。
  • 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使用。

次に、com.hanpang.configパッケージの下にPersonConfigクラスを作成し、@ Scope( "thread")アノテーションを使用して、以下に示すように、PersonオブジェクトのスコープをThreadスコープとしてマークします。

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);
    }
}
复制代码

最後に、SpringBeanTestクラスにtestAnnotationConfig4()メソッドを作成し、testAnnotationConfig4()メソッドにSpringコンテナを作成し、ThreadScopeオブジェクトをSpringコンテナに登録します。次に、ループを使用して2つのスレッドスレッドを作成し、各スレッドに以下に示すように、それぞれ2つのPersonオブジェクトを取得します。

@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();
        }
    }
}
复制代码

出力からわかるように、Beanは同じスレッドで同じBeanインスタンスを取得し、異なるスレッドでのBeanインスタンスは異なります。

注:ここでは、以下に示すように、それに応じてPersonクラスを調整し、Lombokのアノテーションを削除し、コンストラクターとセッターおよびゲッターのメソッドを手動で作成しました。

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;
    }
}
复制代码

おすすめ

転載: juejin.im/post/7085263304460861448