@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)就可以了,这里就不重复贴代码了。
问题解决。以上为个人经验,希望能给大家一个参考,也希望大家多多支持。