记一次为了节省代码没有在方法体中声明HttpServletRequest,而用autowire直接注入所钻的坑

结论

给心急的人。 直接在Controller的成员变量上使用@Autowire声明HttpServletRequest,这是线程安全的!

@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,一般需要在方法上声明,如:

public Result save(HttpServletRequest request){
// dosomething();
}

那么我每个方法都要用到uid的岂不是每个方法都要声明一个request参数,为了节省着个冗余步骤。我写了一个基类。

public class CommonController{

    @Autowire
HttpServletReqeust request; public String getUid(){
return (String)request.getAttribute("uid");
}
}

后来我就担心,因为controller是单例的,这么写会不会导致后面的reqeust覆盖前面的request,在并发条件下有线程安全问题。 于是我就到segmentFault上提问,大部分网友说到,确实有线程问题!segmentFault问题地址

验证过程

因为网友大部分的观点是只能在方法上声明,我自然不想就此放弃多写那么多代码,于是开始我的验证过程。 热心的程序员们给我提供了好几种解决方案,我既然花力气证明了,就把结果放在这里,分享给大家。

方法1

第一个方法就是在controller的方法中显示声明HttpServletReqeust,代码如下:

@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销毁了,所以又从起始地址开始创建,这样一切就解释得通了!

猜测完毕

验证猜想:

我不让他有销毁的时间不就可以了吗 测试代码

@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

输出

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相同。

二轮验证

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处理。

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有可能覆盖掉之前的!

验证代码

@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;
}
}
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();
}
}
}

截取了部分输出结果

可以看到57、71、85、47被覆盖了,丢失了部分request!

这么做是线程不安全的!

方法3

使用CommonController作为基类,将request Autowire。

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却是不同的,具体里面如何实现我也就没有继续深挖了。

但是结论是出来的,就如文章最开始所说一样。

SpringMVC在Controller层中注入request的坑的更多相关文章

  1. SpringMVC Controller中注入Request成员域和在方法中定义中HttpServletRequest有啥区别

    先说结论,在Controller中注入Request是线程安全的. 以下是解释: 我们先来看看这两者有什么不同 在controller注入成员变量request 可以看到注入的是一个代理对象 写在方法 ...

  2. 【转】在SpringMVC Controller中注入Request成员域

    原文链接:https://www.cnblogs.com/abcwt112/p/7777258.html 原文作者:abcwt112 主题 在工作中遇到1个问题....我们定义了一个Controlle ...

  3. 在SpringMVC Controller中注入Request成员域

    主题 在工作中遇到1个问题....我们定义了一个Controller基类,所有Springmvc自定义的controller都继承它....在它内部定义一个@Autowired HttpServlet ...

  4. token获取在controller层中

    集合判断是否为空 注意:token获取在controller层中,token中存的所有数据都要在controller中获取 在自己的接口里调用别的接口需要判断一下返回值是否为空

  5. Spring Mvc 在非controller层 实现获取request对象

    一般我们在Controller层,会编写类似这样的方法 @Controller @RequestMapping(value="/detail") public class GetU ...

  6. SpringMVC的controller层接收来自jsp页面通过<a href="/user/userUpdateInfo/>的中文乱码问题

    这种情况是,jsp页面的中文正常显示,数据的中文也是正常显示,但是在Controller层接收到的中文是乱码,如下图所示: 解决方法:在Controller层对前台传递的中文乱码进行处理,将它转换成u ...

  7. controller层中,参数的获取方式以及作用域的问题

    package com.krry.web; import javax.servlet.http.HttpServletRequest; import org.springframework.stere ...

  8. SpringMVC的controller方法中注解方式传List参数使用@RequestBody

    在SpringMVC控制器方法中使用注解方式传List类型的参数时,要使用@RequestBody注解而不是@RequestParam注解: //创建文件夹 @RequestMapping(value ...

  9. SpringMVC基础-controller方法中的参数注解

    @PathVariable  映射 URL 绑定的占位符 带占位符的 URL 是 Spring3.0 新增的功能,该功能在 SpringMVC 向 REST 目标挺进发展过程中具有里程碑的意义 通过 ...

随机推荐

  1. JDK中线程组ThreadGroup

    如果线程有100条...分散的不好管理... 线程同样可以分组ThreadGroup类. 线程组表示一个线程的集合.此外,线程组也可以包含其他线程组.线程组构成一棵树,在树中,除了初始线程组外,每个线 ...

  2. 《垃圾回收的算法与实现》——Python垃圾回收

    Python垃圾回收 python采用引用计数法进行垃圾回收 Python内存分配 python在分配内存空间时,在malloc之上堆放了3个独立的分层. python内存分配时主要由arena.po ...

  3. lucene 初探

    前言: window文件管理右上角, 有个搜索功能, 可以根据文件名进行搜索. 那如果从文件名上判断不出内容, 我岂不是要一个一个的打开文件, 查看文件的内容, 去判断是否是我要的文件? 几个, 十几 ...

  4. 复刻smartbits的国产网络测试工具minismb-如何测试DPI引擎

    复刻smartbits的网络性能测试工具MiniSMB,是一款专门用于测试智能路由器,网络交换机的性能和稳定性的软硬件相结合的工具.可以通过此以太网测试工具测试任何ip网络设备的端口吞吐率,带宽,并发 ...

  5. JSP 基础(二)

    五 注释  5.1 JSP注释     <%--注释内容--%> 5.2 HTML注释    <!--注释内容--> 5.3 Java注释 六 JSP指令 在JSP中有三种类型 ...

  6. 第四章 使用Servlet处理HTTP请求

      回顾上一章的知识: 请求网络资源HTTP会发出多个请求并得到响应 设置响应状态行关键是设置状态码 在向客户端输出内容之前要设置状态码和响应头 设置响应头让Servlet发挥最大功能 消息体可以用文 ...

  7. 【JS】for in循环对象,hasOwnProperty()的作用

    var obj = { name:"echolun", age:", sex:"male" }, objName=[], //用来装对象属性名 obj ...

  8. [转]Magento2开发教程 - 如何向数据库添加新表

    本文转自:https://www.cnblogs.com/xz-src/p/6920365.html Magento 2具有特殊的机制,允许你创建数据库表,修改现有的,甚至添加一些数据到他们(如安装数 ...

  9. 获取ip 笔记

    使用命令 Request.UserHostAddress;//IP地址 Request.UserHostName;//用户名. 1. 在ASP.NET中专用属性: 获取服务器电脑名:Page.Serv ...

  10. 【C#设计模式-抽象工厂模式】

    一.抽象工厂模式的定义: 为创建一组相关或相互依赖的对象提供一个接口,而且无需指定他们的具体类. 二.抽象工厂模式的结构: 抽象工厂模式是所有形态的工厂模式中最为抽象和最具一般性的一种形态.抽象工厂模 ...