解决@Scope(“prototype“)不生效的问题

目录
  • @Scope(“prototype“)不生效
  • @Scope(“prototype“)正确用法——解决Bean多例问题
    • 1.问题,Spring管理的某个Bean需要使用多例
    • 2.问题升级
    • 3. Spring给出的解决问题的办法(解决Bean链中某个Bean需要多例的问题)

@Scope(“prototype“)不生效

使用spring的时候,我们一般都是使用@Component实现bean的注入,这个时候我们的bean如果不指定@Scope,默认是单例模式,另外还有很多模式可用,用的最多的就是多例模式了,顾名思义就是每次使用都会创建一个新的对象,比较适用于写一些job,比如在多线程环境下可以使用全局变量之类的

创建一个测试任务,这里在网上看到大部分都是直接@Scope(“prototype”),这里测试是不生效的,再加上proxyMode才行,代码如下

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

import org.springframework.beans.factory.config.ConfigurableBeanFactory;

import org.springframework.context.annotation.Scope;

import org.springframework.context.annotation.ScopedProxyMode;

import org.springframework.scheduling.annotation.Async;

import org.springframework.stereotype.Component;

@Component

@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)

public class TestAsyncClient {

    private int a = 0;

    @Async

    public void test(int a) {

        this.a = a;

        CommonAsyncJobs.list.add(this.a + "");

    }

}

测试

?

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

import cn.hutool.core.collection.CollectionUtil;

import lombok.extern.slf4j.Slf4j;

import org.junit.Test;

import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.scheduling.annotation.EnableAsync;

import org.springframework.test.context.junit4.SpringRunner;

import java.util.Set;

import java.util.Vector;

@Slf4j

@EnableAsync

@SpringBootTest

@RunWith(SpringRunner.class)

public class CommonAsyncJobs {

    @Autowired

    private TestAsyncClient testAsyncClient;

    // 多线程环境下,普通的list.add不适用,用Vector处理就行了,效率低,但无所谓反正测试的

    public static Vector<String> list = new Vector<>();

    @Test

    public void testAsync() throws Exception {

        // 循环里面异步处理

        int a = 100000;

        for (int i = 0; i < a; i++) {

            testAsyncClient.test(i);

        }

        System.out.println("多线程结果:" + list.size());

        System.out.println("单线程结果:" + a);

        Set<String> set = CollectionUtil.newHashSet();

        Set<String> exist = CollectionUtil.newHashSet();

        for (String s : list) {

            if (set.contains(s)) {

                exist.add(s);

            } else {

                set.add(s);

            }

        }

        // 没重复的值,说明多线程环境下,全局变量没有问题

        System.out.println("重复的值:" + exist.size());

        System.out.println("重复的值:" + exist);

        // 单元测试内主线程结束会终止子线程任务

        Thread.sleep(Long.MAX_VALUE);

    }

}

结果

没重复的值,说明多线程环境下,全局变量没有问题

在这里插入图片描述

@Scope(“prototype“)正确用法——解决Bean多例问题

1.问题,Spring管理的某个Bean需要使用多例

在使用了Spring的web工程中,除非特殊情况,我们都会选择使用Spring的IOC功能来管理Bean,而不是用到时去new一个。

Spring管理的Bean默认是单例的(即Spring创建好Bean,需要时就拿来用,而不是每次用到时都去new,又快性能又好),但有时候单例并不满足要求(比如Bean中不全是方法,有成员,使用单例会有线程安全问题,可以搜索线程安全与线程不安全的相关文章),你上网可以很容易找到解决办法,即使用@Scope("prototype")注解,可以通知Spring把被注解的Bean变成多例

如下所示:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RestController;

  

@RestController

@RequestMapping(value = "/testScope")

public class TestScope {

  

    private String name;

    @RequestMapping(value = "/{username}",method = RequestMethod.GET)

    public void userProfile(@PathVariable("username") String username) {

        name = username;

        try {

            for(int i = 0; i < 100; i++) {

                System.out.println(Thread.currentThread().getId() + "name:" + name);

                Thread.sleep(2000);

            }

        } catch (Exception e) {

            e.printStackTrace();

        }

        return;

    }

}

分别发送请求http://localhost:8043/testScope/aaa和http://localhost:8043/testScope/bbb,控制台输出:

34name:aaa
34name:aaa
35name:bbb
34name:bbb

(34和35是两个线程的ID,每次运行都可能不同,但是两个请求使用的线程的ID肯定不一样,可以用来区分两个请求。)可以看到第二个请求bbb开始后,将name的内容改为了“bbb”,第一个请求的name也从“aaa”改为了“bbb”。要想避免这种情况,可以使用@Scope("prototype"),注解加在TestScope这个类上。加完注解后重复上面的请求,发现第一个请求一直输出“aaa”,第二个请求一直输出“bbb”,成功。

2.问题升级

多个Bean的依赖链中,有一个需要多例

第一节中是一个很简单的情况,真实的Spring Web工程起码有Controller、Service、Dao三层,假如Controller层是单例,Service层需要多例,这时候应该怎么办呢?

2.1一次失败的尝试

首先我们想到的是在Service层加注解@Scope("prototype"),如下所示:

controller类代码

?

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

import com.example.test.service.Order;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RestController;

  

@RestController

@RequestMapping(value = "/testScope")

public class TestScope {

  

    @Autowired

    private Order order;

  

    private String name;

  

    @RequestMapping(value = "/{username}", method = RequestMethod.GET)

    public void userProfile(@PathVariable("username") String username) {

        name = username;

        order.setOrderNum(name);

        try {

            for (int i = 0; i < 100; i++) {

                System.out.println(

                        Thread.currentThread().getId()

                                + "name:" + name

                                + "--order:"

                                + order.getOrderNum());

                Thread.sleep(2000);

            }

        } catch (Exception e) {

            e.printStackTrace();

        }

        return;

    }

}

Service类代码

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

import org.springframework.context.annotation.Scope;

import org.springframework.stereotype.Service;

  

@Service

@Scope("prototype")

public class Order {

    private String orderNum;

  

    public String getOrderNum() {

        return orderNum;

    }

  

    public void setOrderNum(String orderNum) {

        this.orderNum = orderNum;

    }

  

    @Override

    public String toString() {

        return "Order{" +

                "orderNum='" + orderNum + '\'' +

                '}';

    }

}

分别发送请求http://localhost:8043/testScope/aaa和http://localhost:8043/testScope/bbb,控制台输出:

32name:aaa--order:aaa
32name:aaa--order:aaa
34name:bbb--order:bbb
32name:bbb--order:bbb

可以看到Controller的name和Service的orderNum都被第二个请求从“aaa”改成了“bbb”,Service并不是多例,失败。

2.2 一次成功的尝试

我们再次尝试,在Controller和Service都加上@Scope("prototype"),结果成功,这里不重复贴代码,读者可以自己试试。

2.3 成功的原因(对2.1、2.2的理解)

Spring定义了多种作用域,可以基于这些作用域创建bean,包括:

  • 单例( Singleton):在整个应用中,只创建bean的一个实例。
  • 原型( Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。

对于以上说明,我们可以这样理解:虽然Service是多例的,但是Controller是单例的。如果给一个组件加上@Scope("prototype")注解,每次请求它的实例,spring的确会给返回一个新的。问题是这个多例对象Service是被单例对象Controller依赖的。而单例服务Controller初始化的时候,多例对象Service就已经注入了;当你去使用Controller的时候,Service也不会被再次创建了(注入时创建,而注入只有一次)。

2.4 另一种成功的尝试(基于2.3的猜想)

为了验证2.3的猜想,我们在Controller钟每次去请求获取Service实例,而不是使用@Autowired注入,代码如下:

Controller类

?

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

import com.example.test.service.Order;

import com.example.test.utils.SpringBeanUtil;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RestController;

  

@RestController

@RequestMapping(value = "/testScope")

public class TestScope {

  

    private String name;

  

    @RequestMapping(value = "/{username}", method = RequestMethod.GET)

    public void userProfile(@PathVariable("username") String username) {

        name = username;

        Order order = SpringBeanUtil.getBean(Order.class);

        order.setOrderNum(name);

        try {

            for (int i = 0; i < 100; i++) {

                System.out.println(

                        Thread.currentThread().getId()

                                + "name:" + name

                                + "--order:"

                                + order.getOrderNum());

                Thread.sleep(2000);

            }

        } catch (Exception e) {

            e.printStackTrace();

        }

        return;

    }

}

用于获取Spring管理的Bean的类

?

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

package com.example.test.utils;

  

import org.springframework.beans.BeansException;

import org.springframework.context.ApplicationContext;

import org.springframework.context.ApplicationContextAware;

import org.springframework.stereotype.Component;

  

@Component

public class SpringBeanUtil implements ApplicationContextAware {

  

    /**

     * 上下文对象实例

     */

    private static ApplicationContext applicationContext;

  

    @Override

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

        this.applicationContext = applicationContext;

    }

  

    /**

     * 获取applicationContext

     *

     * @return

     */

    public static ApplicationContext getApplicationContext() {

        return applicationContext;

    }

  

    /**

     * 通过name获取 Bean.

     *

     * @param name

     * @return

     */

    public static Object getBean(String name) {

        return getApplicationContext().getBean(name);

    }

  

    /**

     * 通过class获取Bean.

     *

     * @param clazz

     * @param <T>

     * @return

     */

    public static <T> T getBean(Class<T> clazz) {

        return getApplicationContext().getBean(clazz);

    }

  

    /**

     * 通过name,以及Clazz返回指定的Bean

     *

     * @param name

     * @param clazz

     * @param <T>

     * @return

     */

    public static <T> T getBean(String name, Class<T> clazz) {

        return getApplicationContext().getBean(name, clazz);

    }

}

Order的代码不变。

分别发送请求http://localhost:8043/testScope/aaa和http://localhost:8043/testScope/bbb,控制台输出:

31name:aaa--order:aaa
33name:bbb--order:bbb
31name:bbb--order:aaa
33name:bbb--order:bbb

可以看到,第二次请求的不会改变第一次请求的name和orderNum。问题解决。我们在2.3节中给出的的理解是对的。

3. Spring给出的解决问题的办法(解决Bean链中某个Bean需要多例的问题)

虽然第二节解决了问题,但是有两个问题:

  • 方法一,为了一个多例,让整个一串Bean失去了单例的优势;
  • 方法二,破坏IOC注入的优美展现形式,和new一样不便于管理和修改。

Spring作为一个优秀的、用途广、发展时间长的框架,一定有成熟的解决办法。经过一番搜索,我们发现,注解@Scope("prototype")(这个注解实际上也可以写成@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE,使用常量比手打字符串不容易出错)还有很多用法。

首先value就分为四类:

  • ConfigurableBeanFactory.SCOPE_PROTOTYPE,即“prototype”
  • ConfigurableBeanFactory.SCOPE_SINGLETON,即“singleton”
  • WebApplicationContext.SCOPE_REQUEST,即“request”
  • WebApplicationContext.SCOPE_SESSION,即“session”

他们的含义是:

  • singleton和prototype分别代表单例和多例;
  • request表示请求,即在一次http请求中,被注解的Bean都是同一个Bean,不同的请求是不同的Bean;
  • session表示会话,即在同一个会话中,被注解的Bean都是使用的同一个Bean,不同的会话使用不同的Bean。

使用session和request产生了一个新问题,生成controller的时候需要service作为controller的成员,但是service只在收到请求(可能是request也可能是session)时才会被实例化,controller拿不到service实例。为了解决这个问题,@Scope注解添加了一个proxyMode的属性,有两个值ScopedProxyMode.INTERFACES和ScopedProxyMode.TARGET_CLASS,前一个表示表示Service是一个接口,后一个表示Service是一个类。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

百创科技

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值