🚀 优质资源分享 🚀
学习路线指引(点击解锁) | 知识定位 | 人群定位 |
---|---|---|
🧡 Python实战微信订餐小程序 🧡 | 进阶级 | 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。 |
💛Python量化交易实战💛 | 入门级 | 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统 |
你好呀, 我是歪歪。
之前不是发布了这篇文章嘛:《千万不要把Request传递到异步线程里面!有坑!》
说的是由于 Request 在 tomcat 里面是复用的,所以如果在一个 Request 的生命周期完成之后,在异步线程里面调用了相关的方法,会导致这个 Request 被污染,然后在下一个请求中观察到一些匪夷所思的场景。
但是文章的评论区里面出现了个问题,还一下把我问住了:
由于我那篇文章关注的重点是把 Request 传递到异步线程这个骚操作,并没有特别的关注 Request 到底是怎么复用的。
我只是通过打印日志的方式去观察到了复用的这个现象:
把项目启动起来之后,分别访问 testRequest 和 testRequest1,从控制台的输出来看,Request 对象确实是一个对象。
但是从前面的线程名称来看,这是线程池里面两个完全不同的线程。
所以,虽然我还啥都没分析呢,基于日志就至少能看出这个问题的答案:
复用的request是和线程绑定的吗?
不是,没有绑定关系。
如果不是和线程绑定,那么问题就随之而来了:
如何决定哪个线程每次复用哪个request呢?
这是个好问题,我也不知道答案,所以我决定来盘一盘它。
但是在盘它之前,我们先想个问题:假设 Request 和请求线程绑定在一起了,这是一个合理的设计吗?
肯定不是的。
线程就应该是单纯的线程,不应该给它“绑定”一个 Request。这种绑定让线程不单纯了,线程和请求耦合在一起了。
好一点的设计应该是 Request 放在一个“池子”里面,来一个线程就从池子里面去取可以用的 Request。
这样可以实现线程和请求之间解耦的效果。
当然,这也只是我在进行探索之前的一个假设而已,先放在这里,最后看看这个猜想是否正确。
看这篇文章不需要你对 Tomcat 有多少了解,会用它就行,很多东西都是可以基于源码推理出来的。
对了,说一下 Tomcat 源码版本:9.0.58。
第一个断点
要找到问题的答案肯定得去翻源码,但是从哪里开始翻呢?
或者换个问题:第一个断点打在哪呢?
遇到这个问题我的第一反应还是从日志里面看看能不能找到相关的线索,从而找到打第一个断点的位置。
但是我分别把日志调整到 DEBUG 级别和 TRACE 级别,均没有发现有价值的信息,所以日志这条路感觉走不通了,怎么办?
不慌,这个时候就要冷静分析一下了。
悄悄的问自己一句:我可以把断点打在方法入口处吗?
当然可以了,这也是能想到的一个非常常规的手段:
但是如果把断点打在这里,相当于从业务代码的第一行反向去推源码,把路绕的稍微远了一点。
那么还可以把断点打在哪里呢?
我这里不是输出了 Request 这个对象的全类名吗:
http-nio-8080-exec-2:testRequest1 = org.apache.catalina.connector.RequestFacade@5db48dd3
RequestFacade,这个类能用,必然有一个 new 它的地方,而要 new 它,必定要调用它的构造方法。
那我是不是只要在其对应的构造方法上打个断点,当程序创建这个类的时候,不就是我要找的源头吗?
所以,我把第一个断点打在了 RequestFacade 的构造方法上。
从构造方法入手,这也是我的一个调试小技巧,送给你,不客气。
有的小伙伴就要问了:如果一个类有多个构造方法怎么办呢?
很简单,大力出奇迹,每个构造方法都打上断点,一定会有一个地方触发的。
调试源码
找到第一个断点的位置了,接下来就是把项目重启,发起调用了。
我连续发起了两次调用,从程序的表现上我就知道这个断点打对了。
我先给你上个动图,你就知道我为什么这么说了:
项目启动之后,第一次调用在断点的地方停下来了,接着第二次调用并没有在断点的地方停下来。
说明第二次确实没有新建 RequestFacade 对象,而是复用了第一次调用时产生的 RequestFacade 对象。
验证了断点打的位置没毛病之后,就可以开始慢慢的调试了。
首先,我们关注一下这个 RequestFacade 对象创建的地方:
有两个 if 判断。
第一个是判断 facade 是否为 null,不为 null 就 new。
第二个是把 facade 赋值给 applicationRequest 对象,接着返回 applicationRequest 对象。
第二个