最近看到这样一段代码,是关于在业务代码中操作HttpServletRequest的,如下:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
HttpServletRequest request;
@GetMapping("login")
public void login() {
Object xx = request.getSession().getAttribute("xx");
request.getSession().setAttribute("xx", "xx");
}
}
在UserController中,直接将HttpServletRequest设置为共享变量request,由@Autowired完成注入。
由于Tomcat是多线程处理请求的,意味着会有多个线程同时操作request,给我的第一感觉是:难道没有线程安全问题吗?
正是出于并发问题的考虑,所以我操作request时,一般都是手动从RequestContextHolder中获取的,如下:
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
因为RequestContextHolder内部是使用ThreadLocal
来维护Request的,线程间隔离,所以不存在线程安全问题,这样使用是没有问题的。
那用@Autowired注入,有没有线程安全的问题呢?
@Autowired并发测试
实践是检验真理的唯一标准。
写了个测试程序,如下,用Jmeter开启多个线程并发请求,结果如下:
从控制台的输出结果可以看到,虽然request是共享变量,多线程在同时操作,但是Session是彼此隔离的,互不影响的,并没有串号。
是不是感觉很神奇???这有点不符合常理,Spring是如何做到这一点的呢?
源码跟踪
要想知道Spring底层是如何做到的,必须要看源码。当然了,没必要全看,可以Debug跟踪,只看重要的源码。
源码太多,这里就不贴了,笔者整理了一下调用链路,如下图:
我试着用语言描述一下,大概逻辑是:
通过@Autowired注入的Request对象,其实并非是原生的HttpServletRequest对象,而是由Spring通过JDK动态代理技术生成的一个代理对象。
代理对象只是一个空壳,本身不具备功能,所有的操作都让RequestObjectFactory.getObject()
返回的对象去处理了。
而RequestObjectFactory.getObject()
底层就是从RequestContextHolder
的ThreadLocal变量requestAttributesHolder获取的。
说白了,从代码上看似是多个线程并发操作一个共享变量request,其实Spring底层通过一个代理对象让客户端去操作了ThreadLocal中的request,
即每个线程都只操作自己的request,是线程隔离的,所以也就不存在并发安全问题了。
Spring的代码一层套一层,可能不是很好表述,为此笔者写了一个模拟程序,大致说明了【通过动态代理来让共享变量线程间隔离】的问题,大家可以参考下。
/**
* @author: pch
* @description: 模拟Spring注入HttpServletRequest的底层原理代码
* @date: 2020/10/21
**/
public class Demo {
// 共享变量request
static HttpServletRequest request;
static {
// 模拟Spring注入的过程,这里用静态代码块来完成赋值
request = (HttpServletRequest) Proxy.newProxyInstance(HttpServletRequest.class.getClassLoader(),
new Class[]{HttpServletRequest.class}, new RequestProxy());
}
public static void main(String[] args) {
// 开启三个线程
for (int i = 0; i < 3; i++) {
new Thread(()->{
initRequest();
request.getSession().put("thread", Thread.currentThread().getName());
try {
// 为了效果明显,sleep一秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 结果正确,Session之间没有串号
System.err.println(Thread.currentThread().getName() + ":" + request.getSession().get("thread"));
}).start();
}
}
// 模拟请求过来,初始化
private static void initRequest() {
//绑定request到ThreadLocal
RequestHolder.setRequest(new MyHttpServletRequest());
}
}
// Request真正的持有者
class RequestHolder{
private static ThreadLocal<HttpServletRequest> holder = new ThreadLocal<>();
static void setRequest(HttpServletRequest request){
holder.set(request);
}
static HttpServletRequest getRequest(){
return holder.get();
}
}
// Request代理
class RequestProxy implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 代理对象中,从RequestHolder中获取request再去执行,代理对象本身只是一个简单包装
return method.invoke(RequestHolder.getRequest(), args);
}
}
// 模拟HttpServletRequest接口
interface HttpServletRequest {
Map getSession();
}
// 实现
class MyHttpServletRequest implements HttpServletRequest{
private Map map = new HashMap();
@Override
public Map getSession() {
return map;
}
}
尾巴
综上所述,由@Autowired注入的共享变量request并不存在线程安全问题,大家可以放心大胆的用,使用起来也很方便,不用写一长串代码了。