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

2022-07-14,,,

@scope(“prototype“)不生效

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

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

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 + "");
    }
}

测试

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变成多例

如下所示:

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类代码

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类代码

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类

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的类

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是一个类。

本文遇到的问题中,将@scope注解改成@scope(value = webapplicationcontext.scope_request, proxymode = scopedproxymode.target_class)就可以了,这里就不重复贴代码了。

问题解决。以上为个人经验,希望能给大家一个参考,也希望大家多多支持。

《解决@Scope(“prototype“)不生效的问题.doc》

下载本文的Word格式文档,以方便收藏与打印。