前言
记一次为了节省代码没有在方法体中声明httpservletrequest,而用autowire直接注入所钻的坑
结论:给心急的人。 直接在controller的成员变量上使用@autowire声明httpservletrequest,这是线程安全的!
1
2
3
4
5
6
7
8
9
|
@controller
public class testcontroller{
@autowire
httpservletrequest request;
@requestmapping ( "/" )
public void test(){
request.getattribute( "uid" );
}
}
|
结论如上。
背景
是这样的,由于项目中我在request的头部加入身份验证信息,而我在拦截器截获信息并且验证通过后,会将当前用户的身份加到request的attribute中,方便在controller层拿出来复用。
疑问:为什么不直接在controller上使用@requestheader取出来呢? 因为header里面是加密后的数据,且要经过一些复杂的身份验证判断,所以直接将这一步直接丢在了拦截器执行。
所以当解密后,我将用户信息(如uid)用request.setattribute()
设入request中在controller提取。
而如果需要使用request,一般需要在方法上声明,如:
1
2
3
|
public result save(httpservletrequest request){
// dosomething();
}
|
那么我每个方法都要用到uid的岂不是每个方法都要声明一个request参数,为了节省着个冗余步骤。我写了一个基类。
1
2
3
4
5
6
7
|
public class commoncontroller{
@autowire
httpservletreqeust request;
public string getuid(){
return (string)request.getattribute( "uid" );
}
}
|
后来我就担心,因为controller是单例的,这么写会不会导致后面的reqeust覆盖前面的request,在并发条件下有线程安全问题。 于是我就到segmentfault上提问,大部分网友说到,确实有线程问题!segmentfault问题地址 ###验证过程 因为网友大部分的观点是只能在方法上声明,我自然不想就此放弃多写那么多代码,于是开始我的验证过程。 热心的程序员们给我提供了好几种解决方案,我既然花力气证明了,就把结果放在这里,分享给大家。
方法1
第一个方法就是在controller的方法中显示声明httpservletreqeust,代码如下:
1
2
3
4
5
6
7
8
9
10
|
@requestmapping ( "/test" )
@restcontroller
public class ctest {
logger logger = loggerfactory.getlogger(getclass());
@requestmapping ( "/iiii" )
public string test(httpservletrequest request) {
logger.info(request.hashcode() + "" );
return null ;
}
}
|
在浏览器狂按f5
输出
当时我是懵逼的,**说好的线程安全呢!**这特么不是同一个request吗!特么的在逗我! 为此我还找了很久request是不是重写了hashcode()!
啊,事实是这样的,因为我用浏览器狂按f5,再怎么按他也是模拟不了并发的。那么就相当于,服务器一直在用同一个线程处理我的请求就足够了,至于这个request的hashcode,按照jdk的说法是根据obj在jvm的虚拟地址计算的,后面的事情是我猜的,如果有知道真正真想的还望告知!
猜测
服务器中每个thread所申请的request的内存空间在这个服务器启动的时候就是固定的,那么我每次请求,他都会在他所申请到的内存空间(可能是类似数组这样的结构)中新建一个request,(类似于数组的起点总是同一个内存地址),那么我发起一个请求,他就会在起始位置新建一个request传递给servlet并开始处理,处理结束后就会销毁,那么他下一个请求所新建的request,因为之前的request销毁了,所以又从起始地址开始创建,这样一切就解释得通了!
猜测完毕
验证猜想:
我不让他有销毁的时间不就可以了吗 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@requestmapping ( "/test" )
@restcontroller
public class ctest {
logger logger = loggerfactory.getlogger(getclass());
@requestmapping ( "/oooo" )
public string testa(httpservletrequest request) throws exception {
thread.sleep( 3000 );
logger.info(request.hashcode() + "" );
logger.info(reqeust.getheader( "uid" );
return null ;
}
@requestmapping ( "/iiii" )
public string test(httpservletrequest request) {
logger.info(request.hashcode() + "" );
logger.info(reqeust.getheader( "uid" );
return null ;
}
}
|
如上,我在接口/oooo中休眠3秒,如果他是共用一个reqeust的话,那么后面的请求将覆盖这个休眠中的reqeust,所传入的uid即为接口地址。先发起/oooo后发起/iiii
输出
1
2
3
4
|
controller.ctest: 33 - 364716268
controller.ctest: 34 - iiii
controller.ctest: 26 - 1892130707
controller.ctest: 27 - oooo
|
结论: 1、后发起的/iiii没有覆盖前面/oooo的数据,没有线程安全问题。 2、request的hashcode不一样,因为/oooo的阻塞,导致另一个线程需要去处理,所以他新建了request,而不是向之前一样全部hashcode相同。
二轮验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class httptest {
public static void main(string[] args) throws exception {
for ( int i = 300 ; i > 0 ; i--) {
final int finali = i;
new thread() {
@override
public void run() {
system.out.println( "v###" + finali);
httprequest.get( "http://localhost:8080/test/iiii?" ).header( "uid" , "v###" + finali).send();
}
}.start();
}
}
}
|
在模拟并发条件下,header中的uid300个完全接受,没有覆盖
所以这种方式,没有线程安全问题。
方法2
在commoncontroller中,使用@modelattribute处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class commoncontroller {
// @autowired
protected httpservletrequest request;
@modelattribute
public void bindreq(httpservletrequest request) {
this .request = request;
}
protected string getuid() {
system.out.println(request.tostring());
return request.getattribute( "uid" ) == null ? null : (string) request.getattribute( "uid" );
}
}
|
这样子是有线程安全问题的!后面的request有可能覆盖掉之前的!
验证代码
1
2
3
4
5
6
7
8
9
10
|
@restcontroller
@requestmapping ( "/test" )
public class ctest extends commoncontroller {
logger logger = loggerfactory.getlogger(getclass());
@requestmapping ( "/iiii" )
public string test() {
logger.info(request.getheader( "uid" ));
return null ;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class httptest {
public static void main(string[] args) throws exception {
for ( int i = 100 ; i > 0 ; i--) {
final int finali = i;
new thread() {
@override
public void run() {
system.out.println( "v###" + finali);
httprequest.get( "http://localhost:8080/test/iiii" ).header( "uid" , "v###" + finali).send();
}
}.start();
}
}
}
|
截取了部分输出结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
controller.ctest: 26 - v### 52
controller.ctest: 26 - v### 13
controller.ctest: 26 - v### 57
controller.ctest: 26 - v### 57
controller.ctest: 26 - v### 21
controller.ctest: 26 - v### 10
controller.ctest: 26 - v### 82
controller.ctest: 26 - v### 82
controller.ctest: 26 - v### 93
controller.ctest: 26 - v### 71
controller.ctest: 26 - v### 71
controller.ctest: 26 - v### 85
controller.ctest: 26 - v### 85
controller.ctest: 26 - v### 14
controller.ctest: 26 - v### 47
controller.ctest: 26 - v### 47
controller.ctest: 26 - v### 69
controller.ctest: 26 - v### 22
controller.ctest: 26 - v### 55
controller.ctest: 26 - v### 61
|
可以看到57、71、85、47被覆盖了,丢失了部分request!
这么做是线程不安全的!
方法3
使用commoncontroller作为基类,将request autowire。
1
2
3
4
5
6
7
8
|
public class commoncontroller {
@autowired
protected httpservletrequest request;
protected string getuid() {
system.out.println(request.tostring());
return request.getattribute( "uid" ) == null ? null : (string) request.getattribute( "uid" );
}
}
|
测试接口同上,结果喜人! 100个request没有任何覆盖,我加大范围测了五六次,上千次请求没一个覆盖,可以证明这种写法没有线程安全问题了!
另外还有一点有趣的是,无论使用多少并发,request的hashcode始终是相同的,而且,测试同一个controller中不同的接口,他也相同,使用sleep强行阻塞,hashcode也是相同。但是访问不同的controller,hashcode却是不同的,具体里面如何实现我也就没有继续深挖了。
但是结论是出来的,就如文章最开始所说一样。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。
原文链接:https://my.oschina.net/sluggarddd/blog/678603?fromerr=XhvpvVTi