• 【Spring】——4、使用@Scope注解设置组件的作用域


    在这里插入图片描述

    📫作者简介:zhz小白
    公众号:小白的Java进阶之路
    专业技能:
    1、Java基础,并精通多线程的开发,熟悉JVM原理
    2、熟悉Java基础,并精通多线程的开发,熟悉JVM原理,具备⼀定的线上调优经验
    3、熟悉MySQL数据库调优,索引原理等,⽇志原理等,并且有出过⼀篇专栏
    4、了解计算机⽹络,对TCP协议,滑动窗⼝原理等有⼀定了解
    5、熟悉Spring,Spring MVC,Mybatis,阅读过部分Spring源码
    6、熟悉SpringCloud Alibaba体系,阅读过Nacos,Sentinel,Seata,Dubbo,Feign,Gateway核⼼源码与设计,⼆次开发能⼒
    7、熟悉消息队列(Kafka,RocketMQ)的原理与设计
    8、熟悉分库分表ShardingSphere,具有真实⽣产的数据迁移经验
    9、熟悉分布式缓存中间件Redis,对其的核⼼数据结构,部署架构,⾼并发问题解决⽅案有⼀定的积累
    10、熟悉常⽤设计模式,并运⽤于实践⼯作中
    11、了解ElasticSearch,对其核⼼的原理有⼀定的了解
    12、了解K8s,Jekins,GitLab
    13、了解VUE,GO
    14、⽬前有正在利⽤闲暇时间做互游游戏,开发、运维、运营、推销等

    本人著作git项目:https://gitee.com/zhouzhz/star-jersey-platform,有兴趣的可以私聊博主一起编写,或者给颗star
    领域:对支付(FMS,FUND,PAY),订单(OMS),出行行业等有相关的开发领域
    🔥如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主~

    @Scope注解概述

    @Scope注解能够设置组件的作用域,我们先来看看@Scope注解类的源码,如下所示。

    /*
     * Copyright 2002-2018 the original author or authors.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      https://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    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.
     *
     * 

    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. * *

    NOTE: {@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. * *

    In this context, scope 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. * *

    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. *

    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. *

    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. *

    Analogous to {@code } support in Spring XML. * @see ScopedProxyMode */ ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT; }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99

    从@Scope注解类的源码中可以看出,在@Scope注解中可以设置如下值:

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

    我们可以看到@Scope注解的注释中有一句话,表明它是有两个实现类的,所以我们直接上实现类中看。
    在这里插入图片描述

    第一个实现类:ConfigurableBeanFactory

    在这里插入图片描述

    我们可以发现SCOPE_SINGLETON就是singleton,而SCOPE_PROTOTYPE就是prototype。

    第二个实现类:WebApplicationContext

    在这里插入图片描述

    我们可以发现SCOPE_REQUEST的值就是request,SCOPE_SESSION的值就是session。
    request和session作用域是需要Web环境来支持的,这两个值基本上使用不到。当我们使用Web容器来运行Spring应用时,如果需要将组件的实例对象的作用域设置为request和session,那么我们通常会使用

    request.setAttribute(“key”, object);
    session.setAttribute(“key”, object);

    这两种形式来将对象实例设置到request和session中,而不会使用@Scope注解来进行设置。

    @Scope注解取值如下

    在这里插入图片描述

    单实例bean作用域

    我们在MainConfig中添加一个Bean对象,放入spring容器中,如下:

    package com.zhz.config;
    
    import com.zhz.bean.Person;
    import com.zhz.filter.MyTypeFilter;
    import org.springframework.context.annotation.*;
    import org.springframework.stereotype.Controller;
    import org.springframework.stereotype.Service;
    
    /**
     * @author zhouhengzhe
     * @description: todo
     * @date 2022/11/4 10:27
     * @since v1
     */
    
    @Configuration
    public class MainConfig {
    
        /**
         * @Bean注解是给IOC容器中注册一个bean,类型自然就是返回值的类型,id默认是用方法名作为id
         */
        @Bean(name = "person")
        public Person person1() {
            return new Person("zhz", 20);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    然后我们在IOCTest中,执行如下代码:

    package com.zhz.test;
    
    import com.zhz.bean.Person;
    import com.zhz.config.MainConfig;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    /**
     * @author zhouhengzhe
     * @description: todo
     * @date 2022/11/4 10:58
     * @since v1
     */
    public class IOCTest {
    
        @SuppressWarnings("resource")
        @Test
        public void test() {
            ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
            Person person = applicationContext.getBean(Person.class);
            Person person1 = applicationContext.getBean(Person.class);
            System.out.println(person == person1);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    我们可以发现他们是同一个对象,因此可知Spring容器默认是单例Bean,所以只要启动之后,Spring就会把实例对象加载到Bean当中,然后接下来的每一次去获取实例对象,都会是同一个引用地址返回。
    在这里插入图片描述

    结论:对象在Spring容器中默认是单实例的,Spring容器在启动时就会将实例对象加载到Spring容器中,之后,每次从Spring容器中获取实例对象,都是直接将对象返回,而不必再创建新的实例对象了

    多实例bean作用域

    我们在MainConfig中添加一个Bean对象,放入spring容器中,并且添加**@Scope(“prototype”)到Bean对象中,**代码如下:

    package com.zhz.config;
    
    import com.zhz.bean.Person;
    import com.zhz.filter.MyTypeFilter;
    import org.springframework.context.annotation.*;
    
    /**
     * @author zhouhengzhe
     * @description: todo
     * @date 2022/11/4 10:27
     * @since v1
     */
    
    @Configuration
    public class MainConfig {
    
        /**
         * @Bean注解是给IOC容器中注册一个bean,类型自然就是返回值的类型,id默认是用方法名作为id
         */
        @Scope("prototype")
        @Bean(name = "person")
        public Person person1() {
            return new Person("zhz", 20);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    然后我们运行测试类IOCTest,代码如下:

    package com.zhz.test;
    
    import com.zhz.bean.Person;
    import com.zhz.config.MainConfig;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    /**
     * @author zhouhengzhe
     * @description: todo
     * @date 2022/11/4 10:58
     * @since v1
     */
    public class IOCTest {
    
        @SuppressWarnings("resource")
        @Test
        public void test() {
            ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
            Person person = applicationContext.getBean(Person.class);
            Person person1 = applicationContext.getBean(Person.class);
            System.out.println(person == person1);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    演示效果如下:
    在这里插入图片描述

    总结:从以上输出结果中也可以看出,此时,输出的person对象和person2对象已经不是同一个对象了。说明他是一个原型实例

    单实例bean作用域如何创建对象?

    我们思考一下单例bean是什么时候创建的呢?
    我们来做个实现,首先我们再MainConfig中创建一个对象,如下:

    @Bean(name = "person")
        public Person person1() {
            System.out.println("单例bean创建");
            return new Person("zhz", 20);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后我们写一个测试用例,如下:

      @Test
        public void test() {
            ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
        }
    
    • 1
    • 2
    • 3
    • 4

    运行结果:
    在这里插入图片描述
    因此我们可以知道Spring容器在创建的时候,就将@Scope注解标注为singleton的组件进行了实例化,并加载到了Spring容器中,以后每次从容器中获取组件实例对象时,都是直接返回相应的对象,而不必再创建新的对象了。

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

    我们也根据上面的测试案例去测一遍多例Bean是怎么创建对象的,测试代码如下:

    @Scope("prototype")
        @Bean(name = "person")
        public Person person1() {
        System.out.println("单例bean创建");
        return new Person("zhz", 20);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    测试用例1:

      @Test
        public void test() {
            ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
        }
    
    • 1
    • 2
    • 3
    • 4

    运行结果:
    在这里插入图片描述

    测试用例2:

    @Test
        public void test() {
            ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
            Person person = applicationContext.getBean(Person.class);
            Person person1 = applicationContext.getBean(Person.class);
            System.out.println(person == person1);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    由测试用例1和测试用例2我们可以发现当对象是多实例时,每次从Spring容器中获取对象时,都会创建新的实例对象,并且每个实例对象都不相等

    单实例bean注意的事项

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

    多实例bean注意的事项

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

    自定义Scope的实现

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

    如何实现自定义Scope呢?

    1、实现Scope接口

    public interface Scope {
    
        /**
    	 * 返回当前作用域中name对应的bean对象
    	 * @param name 需要检索的bean对象的名称
    	 * @param 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();
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    2、将自定义Scope注册到容器中

    需要调用org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope这个方法,咱们看一下这个方法的声明。
    在这里插入图片描述

    3、使用自定义的作用域

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

    一个自定义Scope实现案例

    • 我们来实现一个线程级别的bean作用域,同一个线程中同名的bean是同一个实例,不同的线程中的bean是不同的实例。
    • 要求bean在线程中是共享的,所以我们可以通过ThreadLocal来实现,ThreadLocal可以实现线程中数据的共享。

    我们在com.zhz.scope包下新建一个ThreadScope类。

    package com.zhz.scope;
    
    import com.alibaba.ttl.TransmittableThreadLocal;
    import org.springframework.beans.factory.ObjectFactory;
    import org.springframework.beans.factory.config.Scope;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Objects;
    
    /**
     * @author zhouhengzhe
     * @description: 自定义本地线程级别的bean作用域,不同的线程中的bean是不同的实例,同一个线程中同名的bean是同一个实例
     * @date 2022/11/5 23:46
     * @since v1
     */
    public class ThreadScope implements Scope {
    
        public static final String THREAD_SCOPE = "thread";
    
        private ThreadLocal<Map<String, Object>> beanMap = new TransmittableThreadLocal<>();
    
    
        /**
         * 返回当前作用域中name对应的bean对象
         *
         * @param name:需要检索的bean对象的名称
         * @param objectFactory:如果name对应的bean对象在当前作用域中没有找到,那么可以调用这个objectFactory来创建这个bean对象
         */
        @Override
        public Object get(String name, ObjectFactory<?> objectFactory) {
            Map<String, Object> map = beanMap.get();
            Object bean=null;
            if (Objects.isNull(map)) {
                map = new HashMap<>(16);
                bean = objectFactory.getObject();
                map.put(name, bean);
                beanMap.set(map);
            }
            bean = map.get(name);
            return bean;
        }
    
        /**
         * 将name对应的bean对象从当前作用域中移除
         */
        @Override
        public Object remove(String name) {
            return this.beanMap.get().remove(name);
        }
    
        /**
         * 用于注册销毁回调,若想要销毁相应的对象,则由Spring容器注册相应的销毁回调,而由自定义作用域选择是不是要销毁相应的对象
         * bean作用域范围结束的时候调用的方法,用于bean的清理
         */
        @Override
        public void registerDestructionCallback(String name, Runnable callback) {
            System.out.println("name");
        }
    
        /**
         * 用于解析相应的上下文数据,比如request作用域将返回request中的属性
         */
        @Override
        public Object resolveContextualObject(String key) {
            return null;
        }
    
        /**
         * 作用域的会话标识,比如session作用域的会话标识是sessionId
         */
        @Override
        public String getConversationId() {
            return Thread.currentThread().getName();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    我们在com.zhz.config包下创建一个配置类,例如MainConfig,并使用@Scope(“thread”)注解标注Person对象的作用域为Thread范围:

     @Scope("thread")
        @Bean(name = "person")
        public Person person1() {
            System.out.println("单例bean创建");
            return new Person("zhz", 20);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    测试类:

     @Test
        public void test1() {
            AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
            ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
            // 向容器中注册自定义的Scope
            beanFactory.registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());
            for (int i = 0; i < 2; i++) {
                new Thread(()->{
                    System.out.println(Thread.currentThread().getName() + ","+applicationContext.getBean("person"));
                    System.out.println(Thread.currentThread().getName() + ","+applicationContext.getBean("person"));
                }).start();
            }
            try {
                TimeUnit.SECONDS.sleep(1);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    演示效果:
    在这里插入图片描述

    由上可知:bean在同样的线程中获取到的是同一个bean的实例,不同的线程中bean的实例是不同的。

  • 相关阅读:
    35 | 如何准备测试数据?
    前端vue论坛项目(七)------构建 UserProfileView 用户页面
    【每日一题】设计循环队列(C语言)
    Array.from(new Set)去重 与Array.map()
    PHP+经贸时间轴 毕业设计-附源码211617
    电商数据API接口 | 节省你的电商数据采集成本
    【Java】Java对象的上转型对象与下转型
    关于无感刷新Token,我是这样子做的
    计算机毕业设计springboot家庭支出网页管理系统668mf源码+系统+程序+lw文档+部署
    云原生之容器化:2、Kubernetes基础组件和部署要点
  • 原文地址:https://blog.csdn.net/zhouhengzhe/article/details/127896363