文档章节

SpringMVC在Controller层中注入request的坑

sluggarddd
 sluggarddd
发布于 2016/05/20 15:40
字数 1617
阅读 4890
收藏 12

###结论 给心急的人。 直接在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

输出

reqeust的hashCode

当时我是懵逼的,**说好的线程安全呢!**这特么不是同一个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();
        }
    }
}

截取了部分输出结果

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。

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

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

© 著作权归作者所有

sluggarddd
粉丝 7
博文 14
码字总数 11821
作品 0
程序员
私信 提问
加载中

评论(12)

wo-无悔
wo-无悔

引用来自“wo-无悔”的评论

每一个请求都会先从RequestContextFilter中过来,执行里面的initContextHolders(request, attributes),它会把每个请求的request和response放ThreadLocal<RequestAttributes>里,而注入的的httpServletRequest 就是从ThreadLocal里面取,你说线程安全不安全:trollface:

引用来自“天山只影”的评论

“注入的的httpServletRequest 就是从ThreadLocal里面取”,请问这个过程的源码应该在哪个类中查看?
RequestContextFilter类的这个方法 RequestContextHolder.setRequestAttributes。你查看一下RequestContextHolder的源码,你会看见ThreadLocal requestAttributesHolder
天山只影
天山只影

引用来自“wo-无悔”的评论

每一个请求都会先从RequestContextFilter中过来,执行里面的initContextHolders(request, attributes),它会把每个请求的request和response放ThreadLocal<RequestAttributes>里,而注入的的httpServletRequest 就是从ThreadLocal里面取,你说线程安全不安全:trollface:
“注入的的httpServletRequest 就是从ThreadLocal里面取”,请问这个过程的源码应该在哪个类中查看?
海博1600
海博1600
在单例的Controller中,使用@Autowired完成HttpServletRequest的IOC,没有出现线程单例
wo-无悔
wo-无悔
每一个请求都会先从RequestContextFilter中过来,执行里面的initContextHolders(request, attributes),它会把每个请求的request和response放ThreadLocal<RequestAttributes>里,而注入的的httpServletRequest 就是从ThreadLocal里面取,你说线程安全不安全:trollface:
handosme_w
handosme_w
---
猜测

服务器中每个thread所申请的request的内存空间在这个服务器启动的时候就是固定的,那么我每次请求,他都会在他所申请到的内存空间(可能是类似数组这样的结构)中新建一个request,(类似于数组的起点总是同一个内存地址),那么我发起一个请求,他就会在起始位置新建一个Request传递给Servlet并开始处理,处理结束后就会销毁,那么他下一个请求所新建的Request,因为之前的request销毁了,所以又从起始地址开始创建,这样一切就解释得通了!


我来补充一下,这里其实是tomcat的一个优化处理。
在tomcat维护了一个 org.apache.coyote.AbstractProtocol.RecycledProcessors 可回收的请求处理器池,每个请求被处理完后回调用org.apache.coyote.ajp.AjpProcessor#recycle 方法回收处理器,这里面就包括了对request的回收org.apache.coyote.Request#recycle. 这里可以知道每次请求结束后request对象不是真的被销毁了,而是间接的被RecycledProcessors持有,来实现request的重复使用!
全力以赴
但是访问不同的controller,hashcode却是不同的。这句话我用jMeter做了一下并发测试,test两个方法发现hashcode是相同的。然后这样注入的方式都是一个request.我实在不明白,为什么要去共享request?每个请求的request应该都不同,这是servlet的初衷
921977939qqcom
921977939qqcom
正好急着用呢,楼主威武
k
kenyon_elf
第三种方法很值得研究,在单例的Controller中,使用@Autowired完成HttpServletRequest的IOC,居然没有出现线程安全问题,这一点很有趣。
我们知道@Autowired这种IOC是在框架初始化后一次性完成的,而Request是个每次请求都是动态的东西。
根据楼主测试:同一个Controller中hashcode是相同的,不同Controller中hashcode是不同的。
我猜测:HttpServletRequest本身是个接口,在框架初始IOC的时候使用了HttpServletRequest一个实现类创建一个Request对象,从而完成了初始注入(这个时候hashcode已经固定了),而这个实现类应该是基于类似ThreadLocal的手段重写了所有方法,像这样:getParame() ---实际实现为-->getParame(){returnthreadLocal.get("CURRENT_REQUEST").getParam()}。
即:获得当前线程真正的Request对象并调用方法,Controller里的Request对象仅仅是一个壳而已,底层依旧产生了线程独立的(拥有独立hashcode的)Request对象,只是被包在这壳里。
(以上是我个人的猜测,并没有实际证实)
amoxChen
amoxChen
楼主精神,望其项背
sucanber
sucanber
干好
探讨 SpringMVC 能否注入 Request 和 Response

版权声明:本文版权归Jitwxs所有,欢迎转载,但未经作者同意必须保留原文链接。 https://blog.csdn.net/yuanlaijike/article/details/89930553 一、引言 当我们第一次接触到 Java Web 开发,...

Jitwxs
05/07
0
0
Spring中获取request的几种方法

前言 本文将介绍在Spring MVC开发的web系统中,获取request对象的几种方法,并讨论其线程安全性。 概述 在使用Spring MVC开发Web系统时,经常需要在处理请求时使用request对象,比如获取客户...

jackcooper2015
2018/07/19
0
0
编辑 Spring 中获取 request 的几种方法,及其线程安全性分析

原文出处:编程迷思 概述 在使用Spring MVC开发Web系统时,经常需要在处理请求时使用request对象,比如获取客户端ip地址、请求的url、header中的属性(如cookie、授权信息)、body中的数据等...

编程迷思
2018/07/18
0
0
Spring4.1.6 常用注解

常用的spring注解有如下几种: @Controller @Service @Autowired @RequestMapping @RequestParam @ModelAttribute @Cacheable @CacheFlush @Resource @PostConstruct @PreDestroy @Repositor......

大糊涂
2015/06/10
180
0
Spring 获取 request 的几种方法及其线程安全性分析

本文将介绍在Spring MVC开发的Web系统中,获取request对象的几种方法,并讨论其线程安全性。 一、概述 在使用Spring MVC开发Web系统时,经常需要在处理请求时使用request对象,比如获取客户端...

Java架构
2018/07/11
0
0

没有更多内容

加载失败,请刷新页面

加载更多

MySQL8.0.17 - Multi-Valued Indexes 简述

本文主要简单介绍下8.0.17新引入的功能multi-valued index, 顾名思义,索引上对于同一个Primary key, 可以建立多个二级索引项,实际上已经对array类型的基础功能做了支持 (感觉官方未来一定...

阿里云官方博客
20分钟前
2
0
make4.1降级 make-3.81、2错误

在编译 make-3.82 的时候出现如下错误提示 glob/glob.c:xxx: undefined reference to `__alloca'` 修改 /glob/glob.c // #if !defined __alloca && !defined __GNU_LIBRARY__ # ifdef __GNUC......

Domineering
21分钟前
1
0
Rainbond集群的安装和运维的原理

本文将解读Rainbond集群的安装和运维的原理,使用户基本了解Rainbond的安装机制和运维重点,便于用户搭建大型Rainbond集群。 1.Rainbond集群节点概述 1.1 节点分类 属性 类型 说明 manage 管...

好雨云帮
32分钟前
3
0
好程序员大数据学习路线分享UDF函数

1.为什么需要UDF? 1)、因为内部函数没法满足需求。 2)、hive它本身就是一个灵活框架,允许用自定义模块功能,如可以自定义UDF、serde、输入输出等。 2.UDF是什么? UDF:user difine fun...

好程序员官方
35分钟前
3
0
Groovy中 Base64 URL和文件名安全编码

Base64 URL和文件名安全编码 Groovy支持Base64编码很长一段时间。 从Groovy 2.5.0开始,我们还可以使用Base64 URL和Filename Safe编码来使用encodeBase64Url方法对字节数组进行编码。 结果是...

白石
38分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部