Alcance Spring's Scope, ¿cómo jugar?

¡Acostúmbrate a escribir juntos! Este es el día 11 de mi participación en el "Nuevo plan diario de Nuggets · Desafío de actualización de abril", haga clic para ver los detalles del evento .

Los componentes en el contenedor Spring son singleton por defecto. Cuando se inicia Spring, estos objetos se instancian e inicializan, y se colocan en el contenedor Spring. Después de eso, cada vez que se obtiene un objeto, se obtiene directamente del contenedor Spring, en lugar de Crear objetos. ¿Qué sucede si tiene que crear un nuevo objeto de instancia cada vez que obtiene un objeto del contenedor Spring? En este punto, debe usar la anotación @Scope para establecer el alcance del componente.

La anotación @Scope puede establecer el alcance del componente. Veamos primero el código fuente de la clase de anotación @Scope, como se muestra a continuación:

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

Como se puede ver en el código fuente, los siguientes valores se pueden establecer en la anotación @Scope.

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

Primero, ingresamos a la interfaz ConfigurableBeanFactory y encontramos que hay dos definiciones constantes en la clase ConfigurableBeanFactory, como se muestra a continuación.

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

Entonces, SCOPE_REQUEST y SCOPE_SESSION en la clase WebApplicationContext es que cuando usamos el contenedor web para ejecutar la aplicación Spring, los valores de SCOPE_REQUEST y SCOPE_SESSION en la clase WebApplicationContext se pueden establecer en la anotación @Scope, y el valor de SCOPE_REQUEST es la solicitud y el valor de SCOPE_SESSION es la sesión. .

En resumen, los valores en la anotación @Scope son los siguientes.

  • singleton: indica que el componente es una instancia única en el contenedor Spring. Este es el valor predeterminado de Spring. Spring creará una instancia del componente y lo cargará en el contenedor Spring cuando se inicie. Después de eso, cada vez que se obtenga un componente del Contenedor Spring, devuelva el objeto de instancia directamente sin tener que crear el objeto de instancia nuevamente. Obtenga el objeto del contenedor Spring, los amigos pueden entender que el objeto se obtiene del objeto 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使用。

A continuación, creamos la clase PersonConfig en el paquete com.hanpang.config y usamos la anotación @Scope("thread") para marcar el alcance del objeto Person como el alcance del subproceso, como se muestra a continuación.

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

Finalmente, creamos el método testAnnotationConfig4() en la clase SpringBeanTest, creamos el contenedor Spring en el método testAnnotationConfig4() y registramos el objeto ThreadScope con el contenedor Spring. A continuación, usamos un bucle para crear dos hilos Thread, y en cada hilo respectivamente Obtenga dos objetos Person como se muestra a continuación.

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

Como puede ver en la salida, el bean obtiene la misma instancia de bean en el mismo subproceso, y la instancia de bean en diferentes subprocesos es diferente.

Nota: Aquí, ajusté la clase Person en consecuencia, eliminé la anotación de Lombok y escribí manualmente el constructor y los métodos setter y getter, como se muestra a continuación.

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

Supongo que te gusta

Origin juejin.im/post/7085263304460861448
Recomendado
Clasificación