3. spring-容器: 注解:@Scope

spring容器中的组件默认是单例的,在容器启动后会实例化这些对象并放到容器中,每次获取对象时,直接从容器中获取。这个特性是合理的,大多数场景也该这么使用。

但如果期望每次从spring中获取对象时都是新实例,此时需要借助@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;

/**
 * When used as a type-level annotation in conjunction with
 * {@link org.springframework.stereotype.Component @Component},
 * {@code @Scope} indicates the name of a scope to use for instances of
 * the annotated type.
 *
 * <p>When used as a method-level annotation in conjunction with
 * {@link Bean @Bean}, {@code @Scope} indicates the name of a scope to use
 * for the instance returned from the method.
 *
 * <p><b>NOTE:</b> {@code @Scope} annotations are only introspected on the
 * concrete bean class (for annotated components) or the factory method
 * (for {@code @Bean} methods). In contrast to XML bean definitions,
 * there is no notion of bean definition inheritance, and inheritance
 * hierarchies at the class level are irrelevant for metadata purposes.
 *
 * <p>In this context, <em>scope</em> means the lifecycle of an instance,
 * such as {@code singleton}, {@code prototype}, and so forth. Scopes
 * provided out of the box in Spring may be referred to using the
 * {@code SCOPE_*} constants available in the {@link ConfigurableBeanFactory}
 * and {@code WebApplicationContext} interfaces.
 *
 * <p>To register additional custom scopes, see
 * {@link org.springframework.beans.factory.config.CustomScopeConfigurer
 * CustomScopeConfigurer}.
 *
 * @author Mark Fisher
 * @author Chris Beams
 * @author Sam Brannen
 * @since 2.5
 * @see org.springframework.stereotype.Component
 * @see org.springframework.context.annotation.Bean
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {

	/**
	 * Alias for {@link #scopeName}.
	 * @see #scopeName
	 */
	@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 "";

	/**
	 * Specifies whether a component should be configured as a scoped proxy
	 * and if so, whether the proxy should be interface-based or subclass-based.
	 * <p>Defaults to {@link ScopedProxyMode#DEFAULT}, which typically indicates
	 * that no scoped proxy should be created unless a different default
	 * has been configured at the component-scan instruction level.
	 * <p>Analogous to {@code <aop:scoped-proxy/>} support in Spring XML.
	 * @see ScopedProxyMode
	 */
	ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;

}

从scopeName上的注释可以看出来scope的scopeName可以设置:

ConfigurableBeanFactory#SCOPE_PROTOTYPE
ConfigurableBeanFactory#SCOPE_SINGLETON
org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
org.springframework.web.context.WebApplicationContext#SCOPE_SESSION

都是一些类中的常量,含义分别如下:

  • singleton: 表示组件是单例,是默认特性。也符合绝大多数的需求。
  • prototype: 表示组件是非单例的,再每次从容器中请求该组件对象时都新创建一个返回。
  • request: 用于web容器环境时(tomcat, jetty),每一个请求都创建一个新的实例对象。
  • session: 也是在web容器环境,在同一个session范围内只创建一个新的实例对象。

注: 后两种情况比较少见,一般会使用request.setAttribute(“key”, value), session.setAttribute(“key”, value)。

实验单例bean作用域

默认情况就是单例bean,以下我们进行验证:

/**
 * 测试单例bean
 */
@Slf4j
public class AnnotationSingletonScopeSample {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig.class);

        MyComponent myComponent1 = context.getBean("myComponent", MyComponent.class);
        MyComponent myComponent2 = context.getBean("myComponent", MyComponent.class);

        System.out.println(myComponent1 == myComponent2);


    }
}

// result
true

myComponent组件未加@Scope注解,是默认bean作用域,即singleton。可以看出从容器中获取两次并且引用相同,可以证明确实是单例。

实验prototype bean作用域

我们在PrototypeMyComponent类上加上@Scope注解,并设置作用域为prototype,如下:

package win.elegentjs.spring.ioc.compos;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

/**
 * 演示设置bean 作用域为原型,即每次从容器中获取组件实例时都是新new出来的
 */
@Component
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeMyComponent {

    @Override
    public String toString() {
        return "this is a PrototypeMyComponent.";
    }
}

验证一下执行结果
/**
 * 测试 prototype bean
 */
@Slf4j
public class AnnotationPrototypeScopeSample {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig.class);

        // 初始化spring容器后,分别获取两次组件bean,看是否相等
        PrototypeMyComponent component1 = context.getBean(PrototypeMyComponent.class);
        PrototypeMyComponent component2 = context.getBean(PrototypeMyComponent.class);

        System.out.println(component1 == component2);


    }
}

// result
false

可以看出当将组件上@Scope设置为prototype时,每次从spring容器中返回的对象都是新对象,它们的引用不相等。

注意点一:不同scope创建bean对象的时机不同

singleton的bean实例默认在容器启动时就已经实例化好了,以后需要从容器中取时直接返回即可。
而prototype的bean实例在容器启动时不会实例化,只有当需要从容器中获取时再实例化。

注意点二:单实例bean要注意线程安全

单例的bean在容器启动后整个应用共享,此时要注意这些bean中如果定义了Field, 就可能会出现线程安全问题。

注意点三:多实例bean的性能问题

因每次bean获取时都要重建,所有如果bean创建比较复杂,或者太频繁,会影响系统性能。

自定义Scope
实现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();

}

将Scope注册到容器

需要调用ConfigurableBeanFactory.registerScope方法注册scope。

/**
 * Register the given scope, backed by the given Scope implementation.
 * @param scopeName the scope identifier
 * @param scope the backing Scope implementation
 */
void registerScope(String scopeName, Scope scope);
使用自定义的Scope作用域

在定义bean的时候可以通过设置@Scope,其中scopeName设置为自定义的Scope name。

自定义Scope实现案例

想实现一个Thread级别的bean作用域,即从同一个线程中从spring容器中取bean是单例的,不同线程之间是多例的。

  • 首先实现Scope接口,如下:
package win.elegentjs.spring.ioc.scope;

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

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

/**
 * 自定义Scope, 期望能满足线程内单例
 */
public class MyThreadScope implements Scope {

    // 定义scope常量,用于@Scope注解scopeName属性引用
    public static final String THREAD_SCOPE = "thread";

    // 初始化ThreadLocal,用于存储线程变量
    private static final ThreadLocal<Map<String, Object>> threadBeanMap = ThreadLocal.withInitial(HashMap::new);

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object bean = threadBeanMap.get().get(name);

        if (bean == null) {
            bean = objectFactory.getObject();
            threadBeanMap.get().put(name, bean);
        }

        return bean;
    }

    @Override
    public Object remove(String name) {
        return threadBeanMap.get().remove(name);
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        // Nothing to do.
    }

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

    @Override
    public String getConversationId() {
        return Thread.currentThread().getName();
    }
}

  • 使用自定义的scope设置组件
package win.elegentjs.spring.ioc.compos;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import win.elegentjs.spring.ioc.scope.MyThreadScope;

/**
 * 演示设置bean 作用域为自定义的ThreadScope,即同线程中获取的Bean是单例的
 */
@Component
@Scope(scopeName = MyThreadScope.THREAD_SCOPE)
public class CustomScopeMyComponent {

    @Override
    public String toString() {
        return super.toString();
    }
}

  • 测试自定义的scope bean
package win.elegentjs.spring.ioc.scope;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import win.elegentjs.spring.ioc.compos.CustomScopeMyComponent;
import win.elegentjs.spring.ioc.config.PersonConfig;

/**
 * 测试 customer scope bean
 *
 * 分别新建两个线程,在每个线程中分别从spring容器中获取bean实例,
 * 如果打印出来的对象值相等说明是单例
 *
 */
@Slf4j
public class AnnotationCustomScopeSample {

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

        // 注册到beanFactory
        context.getBeanFactory().registerScope(MyThreadScope.THREAD_SCOPE, new MyThreadScope());

        for (int i = 0; i < 2; i ++) {
            new Thread(() -> {
                log.info(Thread.currentThread() + " : " + context.getBean(CustomScopeMyComponent.class));
                log.info(Thread.currentThread() + " : " + context.getBean(CustomScopeMyComponent.class));
            }).start();
        }

    }
}

// result
2021-05-21 13:16:49.077 [Thread-1] INFO  w.e.spring.ioc.scope.AnnotationCustomScopeSample-Thread[Thread-1,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@2df26426
2021-05-21 13:16:49.076 [Thread-0] INFO  w.e.spring.ioc.scope.AnnotationCustomScopeSample-Thread[Thread-0,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@563cf744
2021-05-21 13:16:49.079 [Thread-1] INFO  w.e.spring.ioc.scope.AnnotationCustomScopeSample-Thread[Thread-1,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@2df26426
2021-05-21 13:16:49.079 [Thread-0] INFO  w.e.spring.ioc.scope.AnnotationCustomScopeSample-Thread[Thread-0,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@563cf744

可以看出同一个线程实例相同。

注册自定义Scope的另一种方式

@Scope源码上有这么一段注释:

 * <p>To register additional custom scopes, see
 * {@link org.springframework.beans.factory.config.CustomScopeConfigurer
 * CustomScopeConfigurer}.
 *

即可通过CustomScopeConfigurer实现自定义Scope的注册,本质上CustomScopeConfigurer是一个BeanFactoryPostProcessor。关于它的原理和源码等讲解了BeanFactoryPostProcessor后再讲。这里先给出实现代码。

  • 注册CustomScopeConfigurerbean
package win.elegentjs.spring.ioc.config;

import org.springframework.beans.factory.config.CustomScopeConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import win.elegentjs.spring.ioc.scope.MyThreadScope;

/**
 * 通过CustomScopeConfigurer配置自定义Scope
 */
@Configuration
public class CustomScopeConfig {

    @Bean
    public CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        configurer.addScope(MyThreadScope.THREAD_SCOPE, new MyThreadScope());

        return configurer;
    }
}

  • 实际测试
package win.elegentjs.spring.ioc.scope;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import win.elegentjs.spring.ioc.compos.CustomScopeMyComponent;
import win.elegentjs.spring.ioc.config.CustomScopeConfig;
import win.elegentjs.spring.ioc.config.PersonConfig;

/**
 * 测试 customer scope bean
 *
 * 采用CustomScopeConfigurer形式注册自定义Scope
 *
 * 分别新建两个线程,在每个线程中分别从spring容器中获取bean实例,
 * 如果打印出来的对象值相等说明是单例
 *
 */
@Slf4j
public class AnnotationCustomScopeSample2 {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomScopeConfig.class, PersonConfig.class);

        for (int i = 0; i < 2; i ++) {
            new Thread(() -> {
                log.info(Thread.currentThread() + " : " + context.getBean(CustomScopeMyComponent.class));
                log.info(Thread.currentThread() + " : " + context.getBean(CustomScopeMyComponent.class));
            }).start();
        }

    }
}

// result:
2021-05-21 13:54:45.796 [Thread-0] INFO  w.e.spring.ioc.scope.AnnotationCustomScopeSample2-Thread[Thread-0,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@30b7e195
2021-05-21 13:54:45.799 [Thread-0] INFO  w.e.spring.ioc.scope.AnnotationCustomScopeSample2-Thread[Thread-0,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@30b7e195
2021-05-21 13:54:45.796 [Thread-1] INFO  w.e.spring.ioc.scope.AnnotationCustomScopeSample2-Thread[Thread-1,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@1c227b50
2021-05-21 13:54:45.805 [Thread-1] INFO  w.e.spring.ioc.scope.AnnotationCustomScopeSample2-Thread[Thread-1,5,main] : win.elegentjs.spring.ioc.compos.CustomScopeMyComponent@1c227b50

实际效果跟使用beanFactory直接注册一致。

小结

本节主要回顾的@Scope注解的用法,参数,以及自定义扩展。

对于组件默认情况下不加@Scope就是singleton。可选择prototype表示每次从容器中取到的都是新实例。其他还有在web框架中可使用的request和session。(很少用)

当需要自定义的时候,可以可以分三步走:1)自定义Scope。 2)注册自定义Scope(两种方式)3)@Scope注解中引用自定义的Scope名称。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值