我们知道springmvc中request是方法级别的,一个方法对应一个request。那么如果我们把request设置为类级别的变量呢?就像这样:

@Controller
@RequestMapping("/admin")
public class AdminController {
    private HttpServletRequest tempRequest;

    @ModelAttribute
    public void getRequest(HttpServletRequest request){
        // 每个方法执行前都将当前的request赋给tempRequest
        tempRequest = request;
    }

    ......
}

根据Sprimg的默认规则,controller对于factorybean默认是单例,controller只有一个。我们知道tomcat对于每一个请求都会生成一个处理线程去处理当前请求,那么在并发量高的情况下,我们都去请求例如:public ModelAndView checkAdmin() 这样一个方法,因为我们设置request为类变量,那么势必会造成后一个request覆盖了前一个request的一些请求参数。如果下面这个例子:

private HttpServletRequest tempRequest;

    @ModelAttribute
    public void getRequest(HttpServletRequest request){
        // 每个方法执行前都将当前的request赋给tempRequest
        tempRequest = request;
    }

    @RequestMapping("/checkAdmin")
    public void checkAdmin() {
        try{
            // 这里让请求线程等待10秒,模拟高并发
            Thread.currentThread().sleep(10000);
        }catch(InterruptedException e){}
        String num = tempRequest.getParameter("num");
        System.out.println("请求" + tempRequest.hashCode() + ", num = " + num);
    }

然后我们在10秒内分别请求:

http://localhost:8081/eHow/admin/checkAdmin?num=1
http://localhost:8081/eHow/admin/checkAdmin?num=2
http://localhost:8081/eHow/admin/checkAdmin?num=3

先看结果:

请求217031088, num = 3
请求217031088, num = 3
请求217031088, num = 3

照道理,应该request 的hashcode(内存地址)不一样,num也不一样。但是通过结果可以发现,虽然说每次请求num都不一样,但是打印结果确是request hashcode相同,num也一样。这是因为我们让请求线程睡眠了10s,10s后再去从tempRequest取值,那么其实tempRequest已经被最后一个覆盖,三次请求其实最后取的都是同一个tempRequest。也就是说,这样设计的request是线程不安全的,并不是将当前request放在当前线程threadlocal中。(threadlocal简单理解就是将一些值与当前线程关联起来,放的时候放在当前线程中,取得时候就从当前线程中去取。)

ok,那么我们试一下,将每次请求的request放在threadlocal中。

// private HttpServletRequest tempRequest;
    // 这个threadlocal就是放当前线程的HttpServletRequest变量
    private static ThreadLocal<HttpServletRequest> local  = new ThreadLocal<HttpServletRequest>();

    @ModelAttribute
    public void getRequest(HttpServletRequest request){
        // 将当前request放入threadlocal中
        local.set(request);
    }

    @RequestMapping("/checkAdmin")
    public void checkAdmin() {
        try{
            Thread.currentThread().sleep(10000);
        }catch(InterruptedException e){}

        // 取的时候从当前的线程中去取request
        HttpServletRequest request = local.get();
        String num = request.getParameter("num");
        System.out.println("请求" + request.hashCode() + ", num = " + num);
    }

请求地址还是和上面一样,看一下结果:

请求2114778512, num = 1
请求693198960, num = 2
请求973234962, num = 3

可以看到,hashcode不一样,num也不同,也就是说确实将每次请求的request放在了当前的线程中,取得时候也可以从当前线程中取得,这样就不存在覆盖的问题,所以说这种方式是线程安全的。

spring应该是考虑到了这一点,所以给我们提供了一个HttpServletRequest的代理bean,通过@AutoWired注入。具体的实现方式其实和我们上面threadlocal差不多。因为controller是单例,所以我们注入一个httpservletrequest的代理bean,当有一个新的请求进来时,就判断当前线程中有没有httpservletrequest对象,没有就在threadlocal中放一个httpservletrequest对象,当我们去取request的一些参数时,代理bean就从当前线程中的httpservletrequest去取。这样就保证了线程安全。
我们试一下:

// 注入代理类
    @Autowired
    private HttpServletRequest tempRequest;

    // 这个threadlocal就是放当前线程的HttpServletRequest变量
    // private static ThreadLocal<HttpServletRequest> local  = new ThreadLocal<HttpServletRequest>();

    //  @ModelAttribute
    //  public void getRequest(HttpServletRequest request){
    //      // 将当前request放入threadlocal中
    //      local.set(request);
    //  }

    @RequestMapping("/checkAdmin")
    public void checkAdmin() {
        try{
            Thread.currentThread().sleep(10000);
        }catch(InterruptedException e){}

        String num = tempRequest.getParameter("num");
        System.out.println("请求" + tempRequest.hashCode() + ", num = " + num);
    }

请求地址还是和上面一样,看一下结果:

请求368958259, num = 1
请求368958259, num = 2
请求368958259, num = 3

咦,为什么hashcode一样,num不一样,跟上面结果不同?我们刚才说到request在去取参数也就是tempRequest.getParameter(“num”) 的时候代理bean会从当前threadlocal去取request。
我们贴一部分这个代理bean相关的源码,看一下原因:

// 这个就是代理的request bean
 private final ObjectFactory objectFactory;  

 public ObjectFactoryDelegatingInvocationHandler(ObjectFactory objectFactory) {  
     this.objectFactory = objectFactory;  
 }  

 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
            if (methodName.equals("equals")) { 
                return (proxy == args[0]);  
            }  
            else if (methodName.equals("hashCode")) { 
                // 可以看到当方法为hashcode的时候,就会取出代理的hashcode
                // 所以hashcode都是一样的
                return System.identityHashCode(proxy);  
            }  
            else if (methodName.equals("toString")) {  
                return this.objectFactory.toString();  
            }  
            try {  
                // 如果方法是其他
                // ObjectFactory的getObjcect就是从当前threadlocal中取数据
                return method.invoke(this.objectFactory.getObject(), args);  
            }  
            catch (InvocationTargetException ex) {  
                throw ex.getTargetException();  
            }  
        }
private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {  

        public ServletRequest getObject() {  
            // currentReuqestAttributes负责从Threadlocal中获取对象
            return currentRequestAttributes().getRequest();  
        }  

        @Override  
        public String toString() {  
            return "Current HttpServletRequest";  
        }  
    }

ok,也就是说除了equals、hashcode以及toString以外的其他方法都是从threadlocal中取。所以才会造成上面hashcode一样,num不一样的情况。所以这样也是线程安全的。

还有一种线程安全,就是方法级别的reqeust。

// 注入代理类
    //  @Autowired
    //  private HttpServletRequest tempRequest;

    // 这个threadlocal就是放当前线程的HttpServletRequest变量
    // private static ThreadLocal<HttpServletRequest> local  = new ThreadLocal<HttpServletRequest>();

    //  @ModelAttribute
    //  public void getRequest(HttpServletRequest request){
    //      // 将当前request放入threadlocal中
    //      local.set(request);
    //  }

    @RequestMapping("/checkAdmin")
    public void checkAdmin(HttpServletRequest request){
        try{
            Thread.currentThread().sleep(10000);
        }catch(InterruptedException e){}
        String num = request.getParameter("num");
        System.out.println("请求" + request.hashCode() + ", num = " + num);

    }

看结果:

请求1814088687, num = 1
请求2071623777, num = 2
请求1324577120, num = 3

可以看到每次输出也是不同的。因为每次请求都是一个不同的request,每个request都是方法级别的,独享的,也就不存在前面覆不覆盖的问题了。

以上就是对springmvc request线程安全的一些测试。