Spring注解驱动开发(三):利用@Scope注解控制Bean的作用域


这是我Spring Frame 专栏的第三篇文章,在 Spring注解驱动开发(二):使用@Configuration和@Bean向容器中注册组件这篇文章中,我向你介绍了如何使用@Configuration配合@Bean 向容器中注入组件,并且详细向你介绍了以下问题:

  • @Configuration 属性proxyBeanMethods的作用
  • @Bean 一定要配合 @Configuration 使用吗

如果你未读过那篇文章,但是对上面的两个问题感兴趣,我建议你去读一下

1. 背景介绍

当我们利用 @Bean 注解向容器中注入组件时,默认是单例的(Spring 上下文环境会缓存这个对象),之后我们获取对象的时候,都是获取这个共享对象而非重新创建一个对象实例,但是有些时候我们希望每次从Spring Ioc 容器中获取对象时,都要创建一个新的实例对象,那么该如何处理呢?Spring 为我们提供了 @Scope 注解来设置组件的作用域.

2. @Scope详解

@Scope 注解可以设置 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 "";

	ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;

}

从源码中可以看到,这里面主要就是 value/scopeName 属性,我们可以用它来指定Bean作用域范围,从它的注释中可以发现,Spring 为我们提供了四种作用域:

  • ConfigurableBeanFactory#SCOPE_PROTOTYPE
  • ConfigurableBeanFactory#SCOPE_SINGLETON
  • org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
  • org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
    前两个针对的是所有 Spring 应用,而后两个针对的是 Spring Web 应用,我给你分开解释
2.1 SCOPE_PROTOTYPE与SCOPE_SINGLETON

这两个作用域针对的是所有 Spring 应用,我们先进入 ConfigurableBeanFactory看一下这两个值到底是什么:

public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
	String SCOPE_SINGLETON = "singleton";
	String SCOPE_PROTOTYPE = "prototype";
}

如果你学过设计模式(如果你并未学过,我强烈建议你去学一下,对你的职业发展很有帮助),一定对这两个单词不陌生:

  • singleton表示单例模式,整个Spring 上下文就有一个实例
  • prototype表示原型模式,每次从Spring获取对应对象都是新创建的
    当你不指定value 属性值的时候,默认就是singleton
2.2 SCOPE_REQUEST与SCOPE_SESSION

同样的,我们先看一眼这两个值:

public interface WebApplicationContext extends ApplicationContext {
	String SCOPE_REQUEST = "request";
	String SCOPE_SESSION = "session";
}

这两个值是在Spring Web 环境下起作用,如果你学过Java Web ,学过Servlet的四大作用域,我想你你对这两个值也不会陌生

  • request:表示当前对象在一次请求中只有一个实例,每次请求的实例是不同的
  • session: 表示一次会话中只有一个实例,不同的会话实例是不同的
    🐳 PS: 一次会话,你可以理解为一次开关浏览器,一次浏览器中会话是不变的
2.3 总结

通过上面的讲解,我相信你对@Scope 来控制Bean 作用域有了一定了解,接下来我带你实战演示一下singletonprototype的使用,剩余的两个Web环境下的取值在我们开发环境很少使用,这里我就不展开讲解了

3. 单例Bean
3.1 作用域

其实 @Bean 注解标注的对象实例在Spring 上下文中默认就是单例的,这就意味着你标不标注@Scope(value=“singleton”)都是一样的,下面我们先编写代码测试一下
向Spring 中注入 Person 对象

@Configuration
public class CustomerConfig {

    @Scope(value = "singleton")
    @Bean
    public Person person1() {
        // 构造器注入
        return new Person(1, "jack", "male");
    }
}

这里我们从Spring Ioc 容器中两次获取person对象,查看获取到对象是否为同一对象实例(这里我用==直接比较对象内存地址是否相等)

public class IcoMain {
    public static void main(String[] args) {
        // 换成注解配置上下文环境
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
        Person person1 = context.getBean(Person.class);
        Person person2 = context.getBean(Person.class);
        System.out.println("person1==person2: "+(person1==person2));
        context.close();
    }
}

结果验证了我们每次从容器中获取的 Person 实例都是同一个:
在这里插入图片描述

3.2 加载时机

既然这个对象是单例的,那么它到底是何时注入Ioc 容器的呢?
为了演示对象加载时机,我们修改我们的配置类 CustomerConfig:

@Configuration
public class CustomerConfig {
    @Scope(value = "singleton")
    @Bean
    public Person person1() {
        // 加上这段话
        System.out.println("++++++++Person 对象加载完成++++++++");
        return new Person(1, "jack", "male");
    }
}

接着我们在测试类Spring初始化完成之后加上一句话,表示容器初始化完成:

public class IcoMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
        // 加上这段打印
        System.out.println("--------------Spring Ioc 容器初始化完成--------------");
        Person person1 = context.getBean(Person.class);
        Person person2 = context.getBean(Person.class);
        System.out.println("person1==person2: "+(person1==person2));
        context.close();
    }
}

接下来启动测试类查看结果:
在这里插入图片描述
从结果我们可以得出结论: 单例Bean随 Spring Ioc 初始化而创建

3.3 注意问题

从上面我们知道了@Bean + @Scope 默认生成的 bean 实例就是单例的。
单例意味着什么呢?它表示这个 bean 在这个Spring 上下文里就是 唯一的,共享的
那么 A 修改了 bean 的属性,那么 B 会查看到什么结果呢?我们来测试一下:

public class IcoMain {
    public static void main(String[] args) {

        // 换成注解配置上下文环境
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
        System.out.println("--------------Spring Ioc 容器初始化完成--------------");

        Person person1 = context.getBean(Person.class);
        System.out.println("修改 person1 前: "+person1);
        person1.setSex("female");

        Person person2 = context.getBean(Person.class);
        System.out.println("修改 person1 后: "+person2);

        System.out.println("person1==person2: "+(person1==person2));
        context.close();
    }
}

程序中,我先获取person1 并设置了它的sex属性,之后我们再次从容器中获取person1,查看修改前后获取的 person1 发生了什么变化:
在这里插入图片描述
从结果可以验证: A 修改 person1 ,B能查看到修改后的结果。
⭐️ 这时你要注意到单例对象的共享性,以免在多线程环境下产生问题

4. 多实例bean

接下来我们将person1()方法的 @Scope注解 的value值修改为protorype

4.1 作用域

下图为修改后的 CustomerConfig:

@Configuration
public class CustomerConfig {
    @Scope(value = "prototype")
    @Bean
    public Person person1() {
        return new Person(1, "jack", "female");
    }
}

接下来修改测试类:

public class IcoMain {
    public static void main(String[] args) {

        // 换成注解配置上下文环境
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
        System.out.println("--------------Spring Ioc 容器初始化完成--------------")
        Person person1 = context.getBean(Person.class);
        Person person2 = context.getBean(Person.class);
        System.out.println("person1==person2: "+(person1==person2));
        context.close();
    }
}

运行测试类:
在这里插入图片描述
从结果可以得出,我们每次从容器中获取的 perosn1 都是新创建的

4.2 加载时机

对比单实例Bean随着Spring 容器初始化而加载,我们来探讨一下多实例Bean何时被加载,你可以先自己思考一下
在person1()方法创建对象之前打印一段话,具体代码如下所示:

@Configuration
public class CustomerConfig {

    @Scope(value = "prototype")
    @Bean
    public Person person1() {
        System.out.println("++++++++Person 对象加载完成++++++++");
        return new Person(1, "jack", "female");
    }
}

接下来编写测试类

public class IcoMain {
    public static void main(String[] args) {
        // 换成注解配置上下文环境
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
        System.out.println("--------------Spring Ioc 容器初始化完成--------------");
        Person person1 = context.getBean(Person.class);
        Person person2 = context.getBean(Person.class);
        System.out.println("person1==person2: "+(person1==person2));
        context.close();
    }
}

接下来运行测试类,查看一下多实例Bean的加载时机:
在这里插入图片描述
相信通过结果你可以发现,多实例Bean是在我们获取实例时才加载的

4.3 注意问题

如果你每次从Spring容器中读取bean实例的时候,并不会修改bean实例的属性信息,我还是建议你将bean实例设置为 单例 的,毕竟:
多实例bean每次获取的时候都会重新创建,如果这个bean比较复杂,创建时间比较长,那么就会影响系统的性能

4. 自定义Scope 作用域

Spring 强大的地方在与它有很强的 扩展性,我们甚至可以自定义Scope 作用域.自定义Scope步骤为:

  1. 实现 Scope 接口

  2. 向Spring Ioc 中注入 Scope 实现类

  3. 在对应Bean上设置对应的Scope value 值

    这里我们来实现一个线程级别的bean作用域,即同一个线程中获取的同名Bean都是相同的,这里面我要利用ThreadLocal 存储bean实例:

  • 定义Scope 接口实现类
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<>(4);
        }
    };

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object o = beanMap.get().get(name);
        if(ObjectUtils.isEmpty(o)) {
            // 不存在时从ObjectFactory中获取对应对象并放入到作用域中
            o = objectFactory.getObject();
            beanMap.get().put(name,o);
        }
        return o;

    }

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

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        callback = () -> {
            beanMap.get().remove(name);
            System.out.println("===========对象销毁===========");
        };
    }

    /**
     * 解析相应的上下文数据
     * @param key
     * @return
     */
    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    /**
     * 作用域的会话标志,
     * @return
     */
    @Override
    public String getConversationId() {
        return Thread.currentThread().getName();
    }
}
  • 在 Ioc 容器中注入这个 Scope
public class IcoMain {
    public static void main(String[] args) {
        showScope();
    }

    static void showScope() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
        //注册自定义的scope 这个scope 对应的值为 thread
        context.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());

        for(int i = 0;i < 2;i++) {
            new Thread(()->{
                Object th1 = context.getBean("person1");
                Object th2 = context.getBean("person1");
                System.out.println(Thread.currentThread().getName()+"===>"+th1.hashCode());
                System.out.println(Thread.currentThread().getName()+"===>"+th2.hashCode());
            }).start();
        }
    }
}
  • 设置对应bean的作用域
@Configuration
public class CustomerConfig {

    @Scope(value = "thread")
    @Bean
    public Person person1() {
        // 构造器注入
        System.out.println("++++++++Person 对象加载完成++++++++");
        return new Person(1, "jack", "female");
    }
}

查看运行结果:
在这里插入图片描述
从结果可以看见同一个线程获取的bean实例是相同的

5. 总结

今天我向你传输了以下内容

  • @Scope 注解的详细介绍
  • 单例Bean实例的创建时机,可能出现的问题
  • 多实例Bean实例的创建时机,可能出现的问题
  • 如何自定义Scope作用域

最后,我希望你看完本篇文章后,能够利用@Scope注解控制Bean的作用域,也希望你指出我在文章中的错误点,希望我们一起进步,也希望你能给我的文章点个赞,原创不易!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值