前言
最近做项目,springboot项目,本来我们在controller的requestmapping取参数值或者返回写时,使用方法参数,但是发现老项目直接注入了成员变量,Spring本身是单例的,如果是成员变量注入,那么也是单例的,怎么实现不同的请求读取不同的参数呢,如果实现线程安全呢,笔者立马想到了ThreadLocal,但是如果要说就是这个原理,那么必须源码证明。
准备demo
简单写一个demo
@RestController
public class DemoController {
@Autowired
private HttpServletRequest request;
@GetMapping("/hello")
public String demo(String param) {
request.getParameterMap().forEach((k,v)-> System.out.println(k + " : " + Arrays.toString(v)));
return param + ":hello";
}
}
只写了 HttpServletRequest,实际上HttpServletResponse亦是如此。
分析demo,注入的HttpServletRequest是接口类型,那么在Boot启动中就会动态代理实现,由于是接口,可以推测是JDK动态代理,debug果然如此。
源码分析
根据debug,JDK动态代理注入了接口的实现类,关键在于InvocationHandler,Spring使用如下:
org.springframework.beans.factory.support.AutowireUtils.ObjectFactoryDelegatingInvocationHandler
里面有关键代码
return method.invoke(this.objectFactory.getObject(), args);
this.objectFactory.getObject()这句决定线程安全
看看Spring Bean下JDK是怎么动态代理注入的
可以看到JDK动态代理在Spring注入的时候,把这个factory注入了InvocationHandler
其中的handler的invoke方法,这里实际上还要其他类的读取埋点。
这里的invoke仅仅是反射,关键还是 HttpServletRequest的对象来源,跟踪读取逻辑
org.springframework.web.context.request.RequestContextHolder
到此就明确了,注入的成员变量,动态代理后使用Threadlocal处理,所以线程安全,因为每次请求都是线程请求,那么原始的HttpServletRequest对象怎么塞进去的呢,就要看filter的了
org.springframework.web.filter.RequestContextFilter
在doFilter时,执行
同理在org.springframework.web.servlet.FrameworkServlet也会再次读取和写入
这里是为了,如果filter被去除的时候可以有值,再次保底,并且在结束时rest
只不过这个rest有点奇怪
对象并没有清除,还在,说明即使 FrameworkServlet后还可以获取,因为有filter可能会在这个后面执行,如果干掉,很可能就不能读取了。会在RequestContextFilter后面remove;如果我们去掉这个filter,那么需要自定义一个filter实现remove防止内存泄漏。
清除当前线程及子线程ThreadLocal
public static void resetRequestAttributes() { requestAttributesHolder.remove(); inheritableRequestAttributesHolder.remove(); }
至此 HttpServletRequest的成员变量注入逻辑,即ThreadLocal变量,所以请求可以正常访问
总结
实际上这种用法很多项目都用了,只不过我们写代码下意思的通过方法参数来规避线程安全性,这种想法是有益的,可以从源头规避风险,不过实际上Spring也帮我们考虑了这个问题,相当于使用RequestContextFilter做了保底措施。源码分析实际上是知所以然,尤其是我们自己写公共SDK时,可以把这种设计放在代码中,实现优雅和保底逻辑。