一、问题描述:异步线程操作导致请求复用时 cookie 解析失败
1. 场景背景
在一个 web 应用中,通常每个请求都会有一个 httpservletrequest
对象来保存该请求的上下文信息。例如,httpservletrequest
存储了请求中的 cookie
信息。为了提高性能和减少内存使用,web 容器(例如 tomcat)会对 httpservletrequest
对象进行复用。也就是说,当一个请求完成后,tomcat 会将 httpservletrequest
对象放回池中,供下一次请求使用。
为了避免每次请求都重复解析某些信息(例如 cookie
),开发人员可能会在主线程中解析并标记请求对象的状态,例如通过设置一个 cookieparsed
标志位,表明 cookie
已经解析过。这一过程本来是为了避免重复的解析操作,但如果在异步线程中修改了请求的标志位,可能会影响到请求复用时的行为,导致下一个请求复用时出现问题。
2. 问题根源
- 异步线程操作请求对象: 当主线程解析完
httpservletrequest
中的cookie
信息后,标记cookieparsed
为“已解析”,然后启动一个异步线程执行一些长时间的任务,然后主线程执行完毕,进行request回收操作(例如:清空上下文信息,cookieparsed
置为未解析状态)。由于httpservletrequest
是一个共享对象(在主线程和异步线程之间共享),异步线程可能会修改该请求对象的状态,例如将cookieparsed
设置为“已解析”。 - 请求复用机制: 当前请求完成后,
httpservletrequest
会被回收并返回到请求池中,准备供下一个请求复用。在复用时,tomcat 会检查当前请求对象的状态。如果上一个请求对象的cookieparsed
被标记为“已解析”,则下一个请求在复用这个请求对象时会跳过 cookie 的解析步骤,从而导致下一个请求无法正确获取 cookie 信息。 - 标志位未重置: 由于在主线程结束后,
cookieparsed
标志位被设置为“已解析”,但异步线程没有在任务完成后重置该标志位,导致请求对象在复用时被错误地标记为已经解析过 cookie。这会直接影响到下一个请求的处理,导致 cookie 解析失败,直到该request再次被回收,再次进行request回收操作,才会正常
。
二、问题详细分析
1. 场景重现
- 主线程获取
httpservletrequest
的cookie
:主线程在处理 http 请求时,首先从httpservletrequest
中解析出cookie
信息,并标记其解析状态。通常,tomcat 会在请求完成后将请求对象回收。 - 异步线程启动:主线程结束后,将继续执行异步任务(例如,长时间的导出任务),在此过程中,异步线程会继续访问同一个
httpservletrequest
对象。 - 请求复用:由于 tomcat 对请求对象进行复用,当一个请求处理完后,它会将请求对象归还到池中,以便下一个请求复用。如果异步线程修改了请求的某些状态标志(例如标记
cookie
已经解析),下一个请求可能会复用已经被修改过的httpservletrequest
对象。 - 数据污染问题:由于复用的请求对象已经被标记为“cookie 已解析”,这个状态可能会被复用,导致下一次请求跳过
cookie
的解析逻辑,导致获取到的cookie
为null
,进而影响请求的数据处理。
代码示例:
public string handlerequest(httpservletrequest request, httpservletresponse response) { // 主线程开始执行,解析 cookie 信息 string cookievalue = null; cookie[] cookies = request.getcookies(); if (cookies != null) { for (cookie cookie : cookies) { if ("uid".equals(cookie.getname())) { cookievalue = cookie.getvalue(); break; } } } // 主线程完成后启动异步线程 asynccontext asynccontext = request.startasync(request, response); new thread(() -> { try { // 模拟延迟任务 thread.sleep(5000); // 异步线程尝试再次读取 cookie,将回收后的request中的 `cookieparsed` 设置为“已解析” string cookievaluefromasync = request.getcookies()[0].getvalue(); system.out.println("异步线程中的 cookie: " + cookievaluefromasync); asynccontext.complete(); } catch (interruptedexception e) { e.printstacktrace(); } }).start(); return "success"; }
问题:
- 当异步线程执行时,
request
已经被回收,request.getcookies()
返回的cookie
可能会是一个 空数组 或者是 错误的 cookie。这时,即使请求中存在有效的cookie
,异步线程依然无法获取到正确的值。 - 同时被回收的
request
已经被异步线程标记为“cookie 已解析”,导致下一次复用该request的请求跳过了cookie
的解析逻辑,造成下一次请求的获取cookie
为空。
2. 问题分析
tomcat 请求复用机制:
- tomcat 在请求处理结束后并不会立即销毁
httpservletrequest
对象,而是将其放入对象池中以供下一个请求复用。当请求完成后,如果异步线程访问了httpservletrequest
,会继续使用主线程的请求对象。 - 如果主线程处理完请求后,已经对
httpservletrequest
标记了“cookie 已解析”,这个状态可能会被复用,导致下一次请求跳过cookie
的解析。
异步线程与请求对象状态冲突:
- 异步线程和主线程虽然共享同一个
httpservletrequest
对象,但异步线程修改了请求的状态(例如cookieparsed
标志),就会影响其他线程访问请求数据的能力。 - 这种情况下,下一个请求使用了已经标记为“cookie 解析完毕”的请求对象,导致解析失败。
请求上下文传递失败:
- 在异步线程中,由于线程隔离,主线程中的
httpservletrequest
无法自动传递到异步线程中。即使使用asynccontext
来延迟清理请求,httpservletrequest
中的数据也可能无法正确传递给异步线程。
请求标志和清理机制:
- tomcat 使用请求标志(如
cookieparsed
或者requestcompleted
)来追踪请求的状态,并在请求处理完成后清理请求资源。异步线程和主线程共享同一个请求对象时,可能会意外地修改这些标志,影响复用请求的正确性。 - 一旦请求进入异步模式,tomcat 会将其状态标记为“处理完成”,并通过
asynccontext.complete()
延迟清理请求对象。这种延迟清理机制会让异步线程继续持有原始的请求对象,造成请求标志的冲突和数据污染。
三、解决方案
为了避免 httpservletrequest
的状态被修改,并正确地将请求上下文传递给异步线程,以下是推荐的几种解决方案。
使用 httpservletrequestwrapper
创建请求副本
在异步线程中创建请求副本,避免直接操作原始请求对象,从而解决请求复用问题。
public string handlerequest(httpservletrequest request, httpservletresponse response) { // 创建请求副本 httpservletrequest requestcopy = new httpservletrequestwrapper(request) { @override public cookie[] getcookies() { cookie[] cookies = super.getcookies(); // 解析 cookie 或者创建副本 return cookies; } }; asynccontext asynccontext = request.startasync(request, response); new thread(() -> { try { // 在异步线程中使用副本 string cookievaluefromasync = requestcopy.getcookies()[0].getvalue(); system.out.println("异步线程中的 cookie: " + cookievaluefromasync); asynccontext.complete(); } catch (interruptedexception e) { e.printstacktrace(); } }).start(); return "success"; }
优点:通过 httpservletrequestwrapper
创建的副本确保了异步线程不会直接修改原始请求对象,从而避免了请求复用时出现数据污染。
手动传递请求上下文
通过 requestcontextholder
手动传递请求上下文到异步线程,确保异步线程可以访问主线程的请求数据。
public string handlerequest(httpservletrequest request, httpservletresponse response) { asynccontext asynccontext = request.startasync(request, response); // 手动传递请求上下文到异步线程 new thread(() -> { try { // 设置当前请求上下文 servletrequestattributes attributes = new servletrequestattributes(request, response); requestcontextholder.setrequestattributes(attributes, true); // 在异步线程中获取请求参数 string cookievaluefromasync = request.getcookies()[0].getvalue(); system.out.println("异步线程中的 cookie: " + cookievaluefromasync); asynccontext.complete(); } catch (interruptedexception e) { e.printstacktrace(); } finally { // 清理请求上下文 requestcontextholder.resetrequestattributes(); } }).start(); return "success"; }
优点:手动传递请求上下文使得异步线程能够访问主线程的请求信息,避免了异步线程和主线程的上下文隔离问题。
延迟请求对象的清理
通过 asynccontext.complete()
延迟请求的清理,避免请求对象在异步线程执行期间被回收,从而保持请求数据的有效性。
public string handlerequest(httpservletrequest request, httpservletresponse response) { asynccontext asynccontext = request.startasync(request, response); new thread(() -> { try { // 执行异步任务 thread.sleep(5000); // 模拟长时间任务 asynccontext.complete(); // 延迟请求清理 } catch (interruptedexception e) { e.printstacktrace(); } }).start(); return "success"; }
优点:通过延迟清理请求对象,确保异步线程可以访问到有效的请求数据,避免了请求数据在异步任务执行期间被误清理。
四、总结
在处理异步线程时,特别是涉及到 httpservletrequest
等请求对象时,可能会遇到请求复用和上下文传递问题。通过合理地使用请求副本、手动传递请求上下文和延迟请求清理等方法,可以有效避免数据污染和请求对象复用问题,从而确保异步任务中的请求数据正确性。
核心问题:
- 请求复用:tomcat 会复用请求对象,导致异步线程访问到已经修改过的请求。
- 异步线程访问不到请求数据:由于请求对象在异步线程执行时可能已经被清理或标记为“完成”,导致访问不到请求数据。
解决方案:
- 使用
httpservletrequestwrapper
创建请求副本。 - 手动传递请求上下文到异步线程。
- 延迟请求对象的清理,确保异步线程在执行期间能够访问到请求数据。
到此这篇关于在 spring boot 中使用异步线程时的 httpservletrequest 复用问题的文章就介绍到这了,更多相关spring boot 异步线程httpservletrequest 内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论