当前位置: 首页 > article >正文

Spring Boot 中短时间连续请求时出现Cookie获取异常问题

Spring Boot 中短时间连续请求时出现Cookie获取异常问题

  • 一、问题描述:异步线程操作导致请求复用时 `Cookie` 解析失败
    • 1. 场景背景
    • 2. 问题根源
  • 二、问题详细分析
    • 1. 场景重现
    • 2. 问题分析
  • 三、如何避免影响下一次请求?
    • ✅方式 1:在主线程提前复制 `Cookie`(推荐)
    • ✅方式 2:使用 `HttpServletRequestWrapper` 包装 `request`(避免修改原始 request)
    • ✅方式 3:使用 ThreadLocal 传递 Cookie(适用于复杂场景)
  • 四、总结


一、问题描述:异步线程操作导致请求复用时 Cookie 解析失败

在 Spring Boot Web 应用中,每个请求都会携带 HttpServletRequest,其中包含 Cookie 等关键信息。然而,由于 TomcatHttpServletRequest复用机制,如果某个请求对象的 cookieParsed 标记在异步线程中被错误修改,可能会导致 短时间内的后续请求无法正确解析 Cookie

1. 场景背景

在一个 Web 应用中,通常每个请求都会有一个 HttpServletRequest 对象来保存该请求的上下文信息。例如,HttpServletRequest 存储了请求中的 Cookie 信息。为了提高性能和减少内存使用,Web 容器(例如 Tomcat)会对 HttpServletRequest 对象进行复用。也就是说,当一个请求完成后,Tomcat 会将 HttpServletRequest 对象放回池中,供下一次请求使用。

为了避免每次请求都重复解析某些信息(例如 Cookie),开发人员可能会在主线程中解析并标记请求对象的状态,例如通过设置一个 cookieParsed 标志位,表明 Cookie 已经解析过。这一过程本来是为了避免重复的解析操作,但如果在异步线程中修改了请求的标志位,可能会影响到请求复用时的行为,导致下一个请求复用时出现问题。

2. 问题根源

  1. 异步线程操作请求对象: 当主线程解析完 HttpServletRequest 中的 Cookie 信息后,标记 cookieParsed 为“已解析”,然后启动一个异步线程执行一些长时间的任务,然后主线程执行完毕,进行HttpServletRequest 回收操作(例如:清空上下文信息,cookieParsed置为未解析状态)。由于 HttpServletRequest 是一个共享对象(在主线程和异步线程之间共享),异步线程可能会修改该请求对象的状态,例如将 cookieParsed 设置为“已解析”。

  2. 请求复用机制: 当前请求完成后,HttpServletRequest 会被回收并返回到请求池中,准备供下一个请求复用。在复用时,Tomcat 会检查当前请求对象的状态。如果上一个请求对象的 cookieParsed 被标记为“已解析”,则下一个请求在复用这个请求对象时会跳过 Cookie 的解析步骤,从而导致下一个请求无法正确获取 Cookie 信息。

  3. 标志位未重置: 由于在主线程结束后,cookieParsed 标志位被设置为“已解析”,但异步线程没有在任务完成后重置该标志位,导致请求对象在复用时被错误地标记为已经解析过 Cookie。这会直接影响到下一个请求的处理,导致 Cookie 解析失败直到该Request再次被回收,再次进行Request回收操作,才会正常

二、问题详细分析

1. 场景重现

  1. 主线程获取 HttpServletRequestCookie:主线程在处理 HTTP 请求时,首先从 HttpServletRequest 中解析出 Cookie 信息,并标记其解析状态。通常,Tomcat 会在请求完成后将请求对象回收。

  2. 异步线程启动:主线程结束后,将继续执行异步任务(例如,长时间的导出任务),在此过程中,异步线程会继续访问同一个 HttpServletRequest 对象。

  3. 请求复用:由于 Tomcat 对请求对象进行复用,当一个请求处理完后,它会将请求对象归还到池中,以便下一个请求复用。如果异步线程修改了请求的某些状态标志(例如标记 Cookie 已经解析),下一个请求可能会复用已经被修改过的 HttpServletRequest 对象。

  4. 数据污染问题:由于复用的请求对象已经被标记为“Cookie 已解析”,这个状态可能会被复用,导致下一次请求跳过 Cookie 的解析逻辑,导致获取到的 Cookienull,进而影响请求的数据处理。

代码示例:

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. 问题分析

  1. Tomcat 请求复用机制
  • Tomcat 在请求处理结束后并不会立即销毁 HttpServletRequest 对象,而是将其初始化后放入对象池中以供下一个请求复用。当请求完成后,如果异步线程访问了 HttpServletRequest,会继续使用主线程的请求对象。

  • 如果主线程处理完请求后,已经对 HttpServletRequest 标记了“Cookie 已解析”,这个状态可能会被复用,导致下一次请求跳过 Cookie 的解析。

  1. 异步线程与请求对象状态冲突
  • 异步线程和主线程虽然共享同一个 HttpServletRequest 对象,但异步线程修改了请求的状态(例如 cookieParsed 标志),就会影响其他线程访问请求数据的能力。

  • 这种情况下,下一个请求使用了已经标记为“Cookie 解析完毕”的请求对象,导致解析失败。

  1. 请求上下文传递失败
  • 在异步线程中,由于线程隔离,主线程中的 HttpServletRequest 无法自动传递到异步线程中。即使使用 AsyncContext 来延迟清理请求,HttpServletRequest 中的数据也可能无法正确传递给异步线程。
  1. 请求标志和清理机制
  • Tomcat 使用请求标志(如 cookieParsed 或者 requestCompleted)来追踪请求的状态,并在请求处理完成后清理请求资源。异步线程和主线程共享同一个请求对象时,可能会意外地修改这些标志,影响复用请求的正确性。

  • 一旦请求进入异步模式,Tomcat 会将其状态标记为“处理完成”,并通过 asyncContext.complete() 延迟清理请求对象。这种延迟清理机制会让异步线程继续持有原始的请求对象,造成请求标志的冲突和数据污染。

三、如何避免影响下一次请求?

为了避免 HttpServletRequest 的状态被修改,并正确地将请求上下文传递给异步线程,以下是推荐的几种解决方案。

✅方式 1:在主线程提前复制 Cookie(推荐)

避免异步线程访问 request,在主线程获取 Cookie 副本并传递给异步线程:

Cookie[] cookiesCopy = Arrays.copyOf(request.getCookies(), request.getCookies().length);

AsyncContext asyncContext = request.startAsync();
new Thread(() -> {
    try {
        // 访问副本,避免修改原 request
        String cookieValue = cookiesCopy[0].getValue();
        System.out.println("异步线程的 Cookie:" + cookieValue);
    } finally {
        asyncContext.complete();
    }
}).start();

优点:

  • 主线程 获取 Cookie,不会影响 request 内部状态。
  • 避免了 cookieParsed 被提前设置为 true

✅方式 2:使用 HttpServletRequestWrapper 包装 request(避免修改原始 request)

如果你需要保持 request 可用性,可以使用 HttpServletRequestWrapper 拦截 getCookies(),防止它影响 requestcookieParsed 状态:

class SafeRequestWrapper extends HttpServletRequestWrapper {
    private final Cookie[] cookiesCopy;

    public SafeRequestWrapper(HttpServletRequest request) {
        super(request);
        // 提前复制 cookie,避免影响原始 request
        this.cookiesCopy = request.getCookies() != null ?
                Arrays.copyOf(request.getCookies(), request.getCookies().length) : new Cookie[0];
    }

    @Override
    public Cookie[] getCookies() {
        return cookiesCopy;
    }
}

public String handleRequest(HttpServletRequest request, HttpServletResponse response) {
    HttpServletRequest safeRequest = new SafeRequestWrapper(request);
    AsyncContext asyncContext = request.startAsync();

    new Thread(() -> {
        try {
            String cookieValue = safeRequest.getCookies()[0].getValue();
            System.out.println("异步线程的 Cookie:" + cookieValue);
        } finally {
            asyncContext.complete();
        }
    }).start();

    return "success";
}

优点:

  • SafeRequestWrapper 拦截 getCookies(),防止 cookieParsed 状态变化。
  • 异步线程仍然可以像正常 request 一样获取 Cookie,但不会污染主 request

✅方式 3:使用 ThreadLocal 传递 Cookie(适用于复杂场景)

如果异步线程可能会在多个地方访问 request,可以使用 ThreadLocal 预先缓存 Cookie

private static final ThreadLocal<Cookie[]> threadLocalCookies = new ThreadLocal<>();

public String handleRequest(HttpServletRequest request, HttpServletResponse response) {
    threadLocalCookies.set(request.getCookies()); // 复制 Cookie
    AsyncContext asyncContext = request.startAsync();

    new Thread(() -> {
        try {
            Cookie[] cookies = threadLocalCookies.get();
            if (cookies != null) {
                String cookieValue = cookies[0].getValue();
                System.out.println("异步线程的 Cookie:" + cookieValue);
            }
        } finally {
            threadLocalCookies.remove(); // 避免内存泄漏
            asyncContext.complete();
        }
    }).start();

    return "success";
}

优点:

  • 避免异步线程访问 request,但仍然可以获取 Cookie 副本。
  • 避免 cookieParsed 状态修改,不会污染后续请求。
  • 适用于 异步任务复杂且可能跨多个方法调用的情况。

四、总结

在处理异步线程时,特别是涉及到 HttpServletRequest 等请求对象时,可能会遇到请求复用和上下文传递问题。通过合理地使用在主线程提前复制 Cookie、使用 HttpServletRequestWrapper 包装 request、使用 ThreadLocal 传递 Cookie或者直接传递参数等方法,可以有效避免数据污染和请求对象复用问题,从而确保异步任务中的请求数据正确性。

核心问题

  • 请求复用:Tomcat 会复用请求对象,导致异步线程访问到已经修改过的请求。

  • 异步线程访问不到请求数据:由于请求对象在异步线程执行时可能已经被清理或标记为“完成”,导致访问不到请求数据。

解决方案

方案适用场景优势可能的缺点
提前复制 Cookie(推荐)简单场景线程安全、性能好适用于 Cookie 访问较少的场景
HttpServletRequestWrapper需要完整 request 功能透明使用 request需要额外封装
ThreadLocal 传递 Cookie复杂异步任务适用于跨线程、跨方法需要手动清理 ThreadLocal

最佳实践:

  • 如果只是读取 Cookie,建议在主线程复制数据后传递(方式 1)。
  • 如果异步线程需要多个 request 方法,建议用 HttpServletRequestWrapper(方式 2)。
  • 如果异步任务复杂,可以用 ThreadLocal 维护副本(方式 3)。

这样就可以保证异步线程访问 Cookie 而不会影响 request 的复用! 🚀


http://www.kler.cn/a/572092.html

相关文章:

  • uniapp+vue3搭建项目
  • 【powerjob】 powerjobserver注册服务IP错误
  • .h264/.h265文件 前端直接播放
  • 2 Redis 字符串(String) 命令大全
  • 【TCP/IP协议栈】【网络层】子网划分、子网掩码
  • STM32程序的加密与破解以及烧录方法
  • FastGPT 引申:基于 Python 版本实现 Java 版本 RRF
  • Kali CentOs 7代理
  • 【华为OD机试真题29.9¥】(E卷,100分) - IPv4地址转换成整数(Java Python JS C++ C )
  • vmware虚拟机安装银河麒麟高级服务器操作系统V10
  • 物联网感应层数据采集器实现协议转换 数据格式化
  • .NET内存居高不下排查怎么解决
  • 4-3自定义加载器,并添加功能
  • 策略模式的C++实现示例
  • Python 图像处理之 Pillow 库:玩转图片
  • 【我的Android进阶之旅】如何使用NanoHttpd在Android端快速部署一个HTTP服务器?
  • maven推送jar包到nexus
  • 如何将JAR交由Systemctl管理?
  • BambuStudio学习笔记:FlushVolCalculator类
  • Spring项目中常用操作记录