最近看到这样一段代码,是关于在业务代码中操作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注入,有没有线程安全的问题呢?
实践是检验真理的唯一标准。 写了个测试程序,如下,用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并不存在线程安全问题,大家可以放心大胆的用,使用起来也很方便,不用写一长串代码了。