本文将先容在Spring MVC开拓的web系统中,获取request工具的几种方法,并谈论其线程安全性。
原创不易,如果以为文章对你有帮助,欢迎点赞、评论。文章有疏漏之处,欢迎批评示正。
欢迎转载,转载请注明原文链接:http://www.cnblogs.com/kismetv/p/8757260.html

目录
概述
如何测试线程安全性
方法1:Controller中加参数
方法2:自动注入
方法3:基类中自动注入
方法4:手动调用
方法5:@ModelAttribute方法
总结
概述
在利用Spring MVC开拓Web系统时,常常须要在处理要求时利用request工具,比如获取客户端ip地址、要求的url、header中的属性(如cookie、授权信息)、body中的数据等。由于在Spring MVC中,处理要求的Controller、Service等工具都是单例的,因此获取request工具时最须要把稳的问题,便是request工具是否是线程安全的:当有大量并发要求时,能否担保不同要求/线程中利用不同的request工具。
这里还有一个问题须要把稳:前面所说的“在处理要求时”利用request工具,究竟是在哪里利用呢?考虑到获取request工具的方法有眇小的不同,大体可以分为两类:
1) 在Spring的Bean中利用request工具:既包括Controller、Service、Repository等MVC的Bean,也包括了Component等普通的Spring Bean。为了方便解释,后文中Spring中的Bean一律简称为Bean。
2) 在非Bean中利用request工具:如普通的Java工具的方法中利用,或在类的静态方法中利用。
此外,本文谈论是环绕代表要求的request工具展开的,但所用方法同样适用于response工具、InputStream/Reader、OutputStream/ Writer等;个中InputStream/Reader可以读取要求中的数据,OutputStream/ Writer可以向相应写入数据。
末了,获取request工具的方法与Spring及MVC的版本也有关系;本文基于Spring4进行谈论,且所做的实验都是利用4.1.1版本。
如何测试线程安全性
既然request工具的线程安全问题须要特殊关注,为了便于后面的谈论,下面先解释如何测试request工具是否是线程安全的。
测试的基本思路,是仿照客户端大量并发要求,然后在做事器判断这些要求是否利用了相同的request工具。
判断request工具是否相同,最直不雅观的办法是打印出request工具的地址,如果相同则解释利用了相同的工具。然而,在险些所有web做事器的实现中,都利用了线程池,这样就导致先后到达的两个要求,可能由同一个线程处理:在前一个要求处理完成后,线程池收回该线程,并将该线程重新分配给了后面的要求。而在同一线程中,利用的request工具很可能是同一个(地址相同,属性不同)。因此即便是对付线程安全的方法,不同的要求利用的request工具地址也可能相同。
为了避免这个问题,一种方法是在要求处理过程中使线程休眠几秒,这样可以让每个线程事情的韶光足够长,从而避免同一个线程分配给不同的要求;另一种方法,是利用request的其他属性(如参数、header、body等)作为request是否线程安全的依据,由于即便不同的要求先后利用了同一个线程(request工具地址也相同),只要利用不同的属性分别布局了两次request工具,那么request工具的利用便是线程安全的。本文利用第二种方法进行测试。
客户端测试代码如下(创建1000个线程分别发送要求):
public
class
Test {
public
static
void
main(String[] args)
throws
Exception {
String prefix = UUID.randomUUID().toString().replaceAll(
\"大众-\"大众
,
\公众\"大众
) +
\公众::\公众
;
for
(
int
i =
0
; i <
1000
; i++) {
final
String value = prefix + i;
new
Thread() {
@Override
public
void
run() {
try
{
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet =
new
HttpGet(
\"大众http://localhost:8080/test?key=\"大众
+ value);
httpClient.execute(httpGet);
httpClient.close();
}
catch
(IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
做事器中Controller代码如下(暂时省略了获取request工具的代码):
@Controller
public
class
TestController {
// 存储已有参数,用于判断参数是否重复,从而判断线程是否安全
public
static
Set<String> set =
new
HashSet<>();
@RequestMapping
(
\"大众/test\"大众
)
public
void
test()
throws
InterruptedException {
// …………………………通过某种办法得到了request工具………………………………
// 判断线程安全
String value = request.getParameter(
\公众key\公众
);
if
(set.contains(value)) {
System.out.println(value +
\"大众\t重复涌现,request并发不屈安!
\公众
);
}
else
{
System.out.println(value);
set.add(value);
}
// 仿照程序实行了一段韶光
Thread.sleep(
1000
);
}
}
如果request工具线程安全,做事器中打印结果如下所示:
如果存在线程安全问题,做事器中打印结果可能如下所示:
如无分外解释,本文后面的代码中将省略掉测试代码。
方法1:Controller中加参数
代码示例
这种方法实现最大略,直接上Controller代码:
@Controller
public
class
TestController {
@RequestMapping
(
\"大众/test\"大众
)
public
void
test(HttpServletRequest request)
throws
InterruptedException {
// 仿照程序实行了一段韶光
Thread.sleep(
1000
);
}
}
该方法实现的事理是,在Controller方法开始处理要求时,Spring会将request工具赋值到方法参数中。除了request工具,可以通过这种方法获取的参数还有很多,详细可以拜会:https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods
Controller中获取request工具后,如果要在其他方法中(如service方法、工具类方法等)利用request工具,须要在调用这些方法时将request工具作为参数传入。
线程安全性
测试结果:线程安全
剖析:此时request工具是方法参数,相称于局部变量,毫无疑问是线程安全的。
优缺陷
这种方法的紧张缺陷是request工具写起来冗余太多,紧张表示在两点:
1) 如果多个controller方法中都须要request工具,那么在每个方法中都须要添加一遍request参数
2) request工具的获取只能从controller开始,如果利用request工具的地方在函数调用层级比较深的地方,那么全体调用链上的所有方法都须要添加request参数
实际上,在全体要求处理的过程中,request工具是贯穿始终的;也便是说,除了定时器等分外情形,request工具相称于线程内部的一个全局变量。而该方法,相称于将这个全局变量,传来传去。
方法2:自动注入
代码示例
先上代码:
@Controller
public
class
TestController{
@Autowired
private
HttpServletRequest request;
//自动注入request
@RequestMapping
(
\公众/test\公众
)
public
void
test()
throws
InterruptedException{
//仿照程序实行了一段韶光
Thread.sleep(
1000
);
}
}
线程安全性
测试结果:线程安全
剖析:在Spring中,Controller的scope是singleton(单例),也便是说在全体web系统中,只有一个TestController;但是个中注入的request却是线程安全的,缘故原由在于:
利用这种办法,当Bean(本例的TestController)初始化时,Spring并没有注入一个request工具,而是注入了一个代理(proxy);当Bean中须要利用request工具时,通过该代理获取request工具。
下面通过详细的代码对这一实现进行解释。
在上述代码中加入断点,查看request工具的属性,如下图所示:
在图中可以看出,request实际上是一个代理:代理的实现拜会AutowireUtils的内部类ObjectFactoryDelegatingInvocationHandler:
/
Reflective InvocationHandler for lazy access to the current target object.
/
@SuppressWarnings
(
\"大众serial\"大众
)
private
static
class
ObjectFactoryDelegatingInvocationHandler
implements
InvocationHandler, Serializable {
private
final
ObjectFactory<?> objectFactory;
public
ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {
this
.objectFactory = objectFactory;
}
@Override
public
Object invoke(Object proxy, Method method, Object[] args)
throws
Throwable {
// ……省略无关代码
try
{
return
method.invoke(
this
.objectFactory.getObject(), args);
// 代理实现核心代码
}
catch
(InvocationTargetException ex) {
throw
ex.getTargetException();
}
}
}
也便是说,当我们调用request的方法method时,实际上是调用了由objectFactory.getObject()天生的工具的method方法;objectFactory.getObject()天生的工具才是真正的request工具。
连续不雅观察上图,创造objectFactory的类型为WebApplicationContextUtils的内部类RequestObjectFactory;而RequestObjectFactory代码如下:
/
Factory that exposes the current request object on demand.
/
@SuppressWarnings
(
\公众serial\"大众
)
private
static
class
RequestObjectFactory
implements
ObjectFactory<ServletRequest>, Serializable {
@Override
public
ServletRequest getObject() {
return
currentRequestAttributes().getRequest();
}
@Override
public
String toString() {
return
\"大众Current HttpServletRequest\"大众
;
}
}
个中,要得到request工具须要先调用currentRequestAttributes()方法得到RequestAttributes工具,该方法的实现如下:
/
Return the current RequestAttributes instance as ServletRequestAttributes.
/
private
static
ServletRequestAttributes currentRequestAttributes() {
RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes();
if
(!(requestAttr
instanceof
ServletRequestAttributes)) {
throw
new
IllegalStateException(
\公众Current request is not a servlet request\公众
);
}
return
(ServletRequestAttributes) requestAttr;
}
天生RequestAttributes工具的核心代码在类RequestContextHolder中,个中干系代码如下(省略了该类中的无关代码):
public
abstract
class
RequestContextHolder {
public
static
RequestAttributes currentRequestAttributes()
throws
IllegalStateException {
RequestAttributes attributes = getRequestAttributes();
// 此处省略不干系逻辑…………
return
attributes;
}
public
static
RequestAttributes getRequestAttributes() {
RequestAttributes attributes = requestAttributesHolder.get();
if
(attributes ==
null
) {
attributes = inheritableRequestAttributesHolder.get();
}
return
attributes;
}
private
static
final
ThreadLocal<RequestAttributes> requestAttributesHolder =
new
NamedThreadLocal<RequestAttributes>(
\"大众Request attributes\"大众
);
private
static
final
ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new
NamedInheritableThreadLocal<RequestAttributes>(
\"大众Request context\"大众
);
}
通过这段代码可以看出,天生的RequestAttributes工具是线程局部变量(ThreadLocal),因此request工具也是线程局部变量;这就担保了request工具的线程安全性。
优缺陷
该方法的紧张优点:
1) 注入不局限于Controller中:在方法1中,只能在Controller中加入request参数。而对付方法2,不仅可以在Controller中注入,还可以在任何Bean中注入,包括Service、Repository及普通的Bean。
2) 注入的工具不限于request:除了注入request工具,该方法还可以注入其他scope为request或session的工具,如response工具、session工具等;并担保线程安全。
3) 减少代码冗余:只须要在须要request工具的Bean中注入request工具,便可以在该Bean的各个方法中利用,与方法1比较大大减少了代码冗余。
但是,该方法也会存在代码冗余。考虑这样的场景:web系统中有很多controller,每个controller中都会利用request工具(这种场景实际上非常频繁),这时就须要写很多次注入request的代码;如果还须要注入response,代码就更繁琐了。下面解释自动注入方法的改进方法,并剖析其线程安全性及优缺陷。
方法3:基类中自动注入
代码示例
与方法2比较,将注入部分代码放入到了基类中。
基类代码:
public
class
BaseController {
@Autowired
protected
HttpServletRequest request;
}
Controller代码如下;这里列举了BaseController的两个派生类,由于此时测试代码会有所不同,因此做事端测试代码没有省略;客户端也须要进行相应的修正(同时向2个url发送大量并发要求)。
@Controller
public
class
TestController
extends
BaseController {
// 存储已有参数,用于判断参数value是否重复,从而判断线程是否安全
public
static
Set<String> set =
new
HashSet<>();
@RequestMapping
(
\"大众/test\公众
)
public
void
test()
throws
InterruptedException {
String value = request.getParameter(
\"大众key\"大众
);
// 判断线程安全
if
(set.contains(value)) {
System.out.println(value +
\公众\t重复涌现,request并发不屈安!
\公众
);
}
else
{
System.out.println(value);
set.add(value);
}
// 仿照程序实行了一段韶光
Thread.sleep(
1000
);
}
}
@Controller
public
class
Test2Controller
extends
BaseController {
@RequestMapping
(
\"大众/test2\"大众
)
public
void
test2()
throws
InterruptedException {
String value = request.getParameter(
\"大众key\公众
);
// 判断线程安全(与TestController利用一个set进行判断)
if
(TestController.set.contains(value)) {
System.out.println(value +
\"大众\t重复涌现,request并发不屈安!
\公众
);
}
else
{
System.out.println(value);
TestController.set.add(value);
}
// 仿照程序实行了一段韶光
Thread.sleep(
1000
);
}
}
线程安全性
测试结果:线程安全
剖析:在理解了方法2的线程安全性的根本上,很随意马虎理解方法3是线程安全的:当创建不同的派生类工具时,基类中的域(这里是注入的request)在不同的派生类工具中会霸占不同的内存空间,也便是说将注入request的代码放在基类中对线程安全性没有任何影响;测试结果也证明了这一点。
优缺陷
与方法2比较,避免了在不同的Controller中重复注入request;但是考虑到java只许可继续一个基类,以是如果Controller须要继续其他类时,该方法便不再好用。
无论是方法2和方法3,都只能在Bean中注入request;如果其他方法(如工具类中static方法)须要利用request工具,则须要在调用这些方法时将request参数通报进去。下面先容的方法4,则可以直接在诸如工具类中的static方法中利用request工具(当然在各种Bean中也可以利用)。
方法4:手动调用
代码示例
123456789@Controller
public
class
TestController {
@RequestMapping
(
\"大众/test\"大众
)
public
void
test()
throws
InterruptedException {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
// 仿照程序实行了一段韶光
Thread.sleep(
1000
);
}
}
线程安全性
测试结果:线程安全
剖析:该方法与方法2(自动注入)类似,只不过方法2中通过自动注入实现,本方法通过手动方法调用实现。因此本方法也是线程安全的。
优缺陷
优点:可以在非Bean中直接获取。缺陷:如果利用的地方较多,代码非常繁琐;因此可以与其他方法合营利用。
方法5:@ModelAttribute方法
代码示例
下面这种方法及其变种(变种:将request和bindRequest放在子类中)在网上常常见到:
12345678910111213@Controller
public
class
TestController {
private
HttpServletRequest request;
@ModelAttribute
public
void
bindRequest(HttpServletRequest request) {
this
.request = request;
}
@RequestMapping
(
\"大众/test\"大众
)
public
void
test()
throws
InterruptedException {
// 仿照程序实行了一段韶光
Thread.sleep(
1000
);
}
}
线程安全性
测试结果:线程不屈安
剖析:@ModelAttribute表明用在Controller中润色方法时,其浸染是Controller中的每个@RequestMapping方法实行前,该方法都会实行。因此在本例中,bindRequest()的浸染是在test()实行前为request工具赋值。虽然bindRequest()中的参数request本身是线程安全的,但由于TestController是单例的,request作为TestController的一个域,无法担保线程安全。
总结
综上所述,Controller中加参数(方法1)、自动注入(方法2和方法3)、手动调用(方法4)都是线程安全的,都可以用来获取request工具。如果系统中request工具利用较少,则利用哪种办法均可;如果利用较多,建议利用自动注入(方法2 和方法3)来减少代码冗余。如果须要在非Bean中利用request工具,既可以在上层调用时通过参数传入,也可以直接在方法中通过手动调用(方法4)得到。
参考文献
https://docs.spring.io/spring/docs/4.1.x/spring-framework-reference/html/beans.html#beans-factory-scopes-other-injection
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods
https://stackoverflow.com/questions/10541934/spring-aop-and-aspect-thread-safety-for-an-autowired-httpservletrequest-bean
http://www.phpchina.com/portal.php?mod=view&aid=40966
https://stackoverflow.com/questions/22674044/inject-httpservletrequest-into-controller
https://stackoverflow.com/questions/3320674/spring-how-do-i-inject-an-httpservletrequest-into-a-request-scoped-bean
https://my.oschina.net/sluggarddd/blog/678603?fromerr=XhvpvVTi
https://stackoverflow.com/questions/8504258/spring-3-mvc-accessing-httprequest-from-controller
原创不易,如果以为文章对你有帮助,欢迎点赞、评论。文章有疏漏之处,欢迎批评示正。
欢迎转载,转载请注明原文链接:http://www.cnblogs.com/kismetv/p/8757260.html