当前位置: 首页 > Web前端 > HTML

Spring中获取request的几种方法,及其线程安全分析

时间:2023-04-02 21:23:55 HTML

概述在使用SpringMVC开发web系统时,在处理请求的时候往往需要用到request对象,比如获取客户端ip地址,请求的url,header中的属性(如cookies,授权信息),body中的数据等。由于在SpringMVC中,处理请求的Controller、Service等对象都是单例的,在获取请求时最需要注意的是object是请求对象是否线程安全:当有大量并发请求时,能否保证在不同的请求/线程中使用不同的请求对象。这里还有一个问题需要注意:前面提到的“处理请求时”中使用的请求对象在哪里?考虑到获取request对象的方法略有不同,大致可以分为两类:(1)在Springbean中使用request对象:包括Controller、Service、Repository等MVCbean,以及常见的如组件SpringBean。为便于说明,下文将Spring中的Bean简称为Bean。(2)在非Bean中使用request对象:如在普通Java对象的方法中,或在类的静态方法中。另外,本文的讨论围绕着代表请求的request对象展开,但使用的方法同样适用于response对象,InputStream/Reader,OutputStream/Writer等;其中InputStream/Reader可以读取请求中的数据,OutputStream/Writer可以将响应发送到数据输入。最后,获取request对象的方法也跟Spring和MVC的版本有关;本文基于Spring4进行讨论,所做的实验均使用4.1.1版本。如何测试线程安全由于请求对象的线程安全需要特别注意,为了方便后面的讨论,下面先说明一下如何测试请求对象是否线程安全。测试的基本思路是模拟客户端大量并发的请求,然后判断这些请求是否在服务端使用同一个请求对象。判断请求对象是否相同最直观的方法就是打印出请求对象的地址。如果它们相同,则表示使用了相同的对象。但是,在几乎所有的web服务器实现中,都使用了线程池,这样先后到达的两个请求可能会被同一个线程处理:前一个请求处理完后,线程池回收该线程,发送线程重新分配给后续要求。在同一个线程中,使用的请求对象很可能是相同的(相同的地址,不同的属性)。因此,即使是线程安全的方法,不同的请求所使用的请求对象的地址也可能是相同的。避免这个问题的一种方法是在请求处理期间让线程休眠几秒钟,这样可以让每个线程工作足够长的时间,避免同一个线程被分配给不同的请求;另一种方式,就是利用请求的其他属性(如参数、header、body等)作为请求是否线程安全的依据,因为即使不同的请求先后使用同一个线程(地址request对象的属性也是一样的),只要两次构造request对象时使用不同的属性,那么request对象的使用就是线程安全的。本文采用第二种方法进行测试。客户端测试代码如下(创建1000个线程分别发送请求):-","")+"::";for(inti=0;i<1000;i++){finalStringvalue=prefix+i;newThread(){br/>@Overridepublicvoidrun(){try{CloseableHttpClienthttpClient=HttpClients.createDefault();HttpGethttpGet=newHttpGet("http://localhost:8080/test?key="+value);httpClient.execute(httpGet);httpClient.close();}catch(IOExceptione){br/>e.printStackTrace();}}}.start();}}}服务器中的Controller代码如下(获取请求对象的代码暂略):@ControllerpublicclassTestController{//存放已有的参数,用于判断参数是否重复,从而判断线程是否安全publicstaticSetset=newConcurrentSkipListSet<>();@RequestMapping("/test")publicvoidtest()抛出我nterruptedException{//…………………………通过某种方式获取到请求对象…………………………//判断线程安全Stringvalue=request.getParameter("key");if(set.contains(value)){System.out.println(value+"trepeated,request并发是不安全的!");}else{System.out.println(value);set.add(value);}//模拟程序已经执行了一段时间Thread.sleep(1000);}}补充:以上代码原来使用HashSet判断值是否重复?被网友批评指正,使用线程不安全的集合类来验证线程安全是不合适的,已经改为ConcurrentSkipListSet,如果请求对象是线程安全的,则服务端打印结果如下:如果存在线程安全问题,服务端打印结果可能如下:图片说明如无特殊说明,本文后面代码中将省略测试代码.br/>方法一:在Controller中添加参数代码示例这种方法最容易实现,直接上传Controller代码:@ControllerpublicclassTestController{br/>@RequestMapping("/test")publicvoidtest(HttpServletRequestrequest)throwsInterruptedException{//模拟program已经执行了一段时间br/>Thread.sleep(1000);}}这个方法实现的原理是,当Controller方法开始处理请求时,Spring会将请求对象赋值给方法参数。除了request对象,还有很多参数可以通过这个方法获取。在Controller中获取到request对象后,如果要在其他方法(如服务方法、工具类方法等)中使用request对象,需要在调用时将request对象作为参数传入这些方法。线程安全测试结果:线程安全分析:此时request对象是一个方法参数,相当于一个局部变量,无疑是线程安全的。优缺点这种方法的主要缺点是request对象写起来过于冗余,主要体现在两点:如果在多个controller方法中都需要request对象,那么request对象的request参数就需要在每个方法中添加获取只能从控制器开始。如果使用request对象的地方是在更深层次的函数调用层,那么整个调用链上的所有方法都需要加上request参数。实际上,在整个请求处理过程中,请求对象始终贯穿始终;也就是说,除了定时器等特殊情况,request对象相当于线程内部的一个全局变量。而这个方法相当于来回传递这个全局变量。方法二:自动注入代码示例第一段代码:@ControllerpublicclassTestController{@AutowiredprivateHttpServletRequestrequest;//自动注入request@RequestMapping("/test")publicvoidtest()throwsInterruptedException{//模拟程序执行了一段时间Thread.sleep(1000);}}线程安全测试结果:线程安全分析:在Spring中,Controller的作用域是单例(singleton),也就是说整个web系统中只有一个TestController;但是注入的请求是线程安全的,原因是:这样,当Bean(本例中为TestController)初始化时,Spring并没有注入一个请求对象,而是注入了一个代理(proxy);当bean需要使用request对象时,通过这个proxy获取request对象。下面通过具体的代码来说明这个的实现。在上面的代码中打断点,查看request对象的属性,如下图所示:Spring中获取request的几种方法,及其线程安全分析从图中可以看出,request其实是一个proxy:proxy的实现见AutowireUtils的内部类ObjectFactoryDe??legatingInvocationHandler:publicObjectFactoryDe??legatingInvocationHandler(ObjectFactoryobjectFactory){this.objectFactory=objectFactory;br/>}@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{//...省略不相关的代码try{returnmethod.invoke(this.objectFactory.getObject(),args);//Proxy实现核心代码}catch(InvocationTargetExceptionex){throwex.getTargetException();}}}也就是说我们在调用request方法method的时候,实际上调用的是objectFactory.getObject生成的对象的method方法();objectFactory.getObject()生成的对象才是真正的请求对象。继续观察上图,发现objectFactory的类型是WebApplicationContextUtils的内部类RequestObjectFactory;RequestObjectFactory的代码如下:/**按需暴露当前请求对象的Factory.br/>*/@SuppressWarnings("serial")privatestaticclassRequestObjectFactoryimplementsObjectFactory,Serializable{br/>@OverridepublicServletRequestgetObject(){returncurrentRequestAttributes().getRequest();br/>}@OverridepublicStringtoString(){return"CurrentHttpServletRequest";}}其中,获取request对象需要调用currentRequestAttributes()方法首先获取RequestAttributes对象。该方法的实现如下:/**将当前的RequestAttributes实例返回为ServletRequestAttributes。*/privatestaticServletRequestAttributescurrentRequestAttributes(){RequestAttributesrequestAttr=RequestContextHolder.curr!(requestAttrinstanceofServletRequestAttributes)){thrownewIllegalStateException("Currentrequest不是servlet请求");}return(ServletRequestAttributes)requestAttr;}生成RequestAttributes对象的核心代码在类RequestContextHolder中,相关代码如下(省略本类无关代码):属性=getRequestAttributes();//此处省略无关逻辑...returnattributes;}publicstaticRequestAttributesgetRequestAttributes(){RequestAttributesattributes=requestAttributesHolder.get();if(attributes==null){attributes=inheritableRequestAttributesHolder.get();}returnattributes;}privatestaticfinalThreadLocalrequestAttributesHolder=newNamedThreadLocal("Requestattributes");privatestaticfinalThreadLocalinheritableRequestAttributesHolder=newNamedInheritableThreadLocalcontext可以看出生成的RequestAttributes对象是线程本地的变量(ThreadLocal),所以请求对象也是一个线程局部变量;这确保了请求对象的线程安全优缺点这种方法的主要优点:1)注入不限于Controller:方法1中,只能向Controller添加请求参数。对于方法2,不仅可以在Controller中注入,还可以在任何Bean中注入,包括Service、Repository和普通Bean。2)注入对象不限于request:该方法除了注入request对象外,还可以注入范围为request或session的其他对象,如response对象、session对象等;并保证线程安全。3)减少代码冗余:只需要将request对象注入到需要request对象的bean中,然后在bean的各个方法中使用即可,与方法1相比大大减少了代码冗余。但是,这种方法也有代码冗余。考虑这样一个场景:web系统中有很多controller,每个controller中都使用了request对象(这种场景其实很频繁),然后需要写很多次来注入request代码;如果还需要注入response,代码就比较复杂了。下面介绍自动注入方式的改进方法,分析其线程安全及其优缺点。方法三:在基类中自动注入代码示例与方法二相比,将代码的注入部分放到了基类中。基类代码:publicclassBaseController{br/>@AutowiredprotectedHttpServletRequest请求;br/>}控制器代码如下;这里有两个BaseController的派生类,因为此时测试代码会不一样,所以服务端测试代码没有省略;client也需要做相应的修改(同时向2个url发送大量并发请求)。@ControllerpublicclassTestControllerextendsBaseController{//存放已有参数,用于判断参数值是否重复,从而判断线程是否安全publicstaticSetset=newConcurrentSkipListSet<>();@RequestMapping("/test")publicvoidtest()throwsInterruptedException{Stringvalue=request.getParameter("key");//判断线程安全if(set.contains(value)){System.out.println(value+"\trepeated,requestConcurrencyisnotsafe!");}else{System.out.println(value);set.add(value);}//模拟程序执行了一段时间Thread.sleep(1000);}}@ControllerpublicclassTest2ControllerextendsBaseController{br/>@RequestMapping("/test2")publicvoidtest2()throwsInterruptedException{Stringvalue=request.getParameter("key");//判断线程安全(使用setwithTestController判断)if(TestController.set.contains(value)){System.out.println(value+"\t重复,请求并发不安全!");}else{System.out.println(value);TestController.set.add(value);}//模拟程序执行了一段时间Thread.sleep(1000);}}线程安全br/>测试结果:线程安全分析:在了解方法2线程安全的基础上,很容易理解方法3是线程安全的:在创建不同的派生类对象时,基类中的字段(这里注入的request)在不同的派生类对象中会占用不同的内存空间,也就是说将request注入的代码放在基类中,对线程安全没有影响;测试结果也证明了这一点的优缺点与方法2类似,比如避免了在不同的Controller中重复注入请求;但是考虑到Java只允许继承一个基类,如果Controller需要继承其他类,这个方法就没有用了。方法2和方法3都只能将request注入到bean中;如果其他方法(如工具类中的静态方法)需要使用request对象,调用这些方法时需要传入request参数。下面介绍的方法4可以直接在工具类的静态方法中使用request对象(当然也可以在各种Bean中使用)。方法四:手动调用代码示例@ControllerpublicclassTestController{br/>@RequestMapping("/test")publicvoidtest()throwsInterruptedException{HttpServletRequestrequest=((ServletRequestAttributes)(RequestContextHolder.currentRequestAttributes())).getRequest();//模拟程序已经执行了一段时间。br/>Thread.sleep(1000);}}线程安全测试结果:线程安全分析:该方法与方法2(自动注入)类似,只是在方法2中,使用了自动注入实现,该方法是通过手动方法调用。因此,这个方法也是线程安全的。优缺点优点:可以在non-Bean中直接获取。缺点:如果多处使用,代码非常繁琐;因此,它可以与其他方法结合使用。方法五:@ModelAttribute方法代码示例br/>下面的方法及其变体(变体:把request和bindRequest放在子类中)在网上经常看到:@ControllerpublicclassTestController{privateHttpServletRequestrequest;br/>@ModelAttributepublicvoidbindRequest(HttpServletRequestrequest){this.request=request;br/>}@RequestMapping("/test")publicvoidtest()throwsInterruptedException{//模拟程序执行了一段时间br/>Thread.sleep(1000);}}线程安全测试结果:线程不安全性分析:当使用@ModelAttribute注解修饰Controller中的某个方法时,其作用是该方法会在Controller中的每个@RequestMapping方法执行之前执行。所以在这个例子中,bindRequest()的作用就是在test()执行之前给request对象赋值。bindRequest()中的参数request虽然是线程安全的,但是由于TestController是一个单例,request是TestController的一个domain,线程安全是无法保证的。小结综上所述,在Controller中添加参数(方法一)、自动注入(方法二和方法三)、手动调用(方法四)都是线程安全的,可以用来获取请求对象。如果request对象在系统中使用较少,两种方法都可以;如果用的比较多,建议使用自动注入(方法二和方法三)来减少代码冗余。如果需要在非Bean中使用request对象,既可以在上层调用时作为参数传入,也可以通过手动调用直接在方法中获取(方法4)。另外,本文在讨论获取请求对象的方法时,重点关注方法的线程安全、代码的繁琐等;在实际开发过程中,还要考虑项目的规范和代码维护等问题(在此感谢网友批评指正)。