您的位置:首页 > 娱乐 > 明星 > Tomcat Request Cookie 丢失问题

Tomcat Request Cookie 丢失问题

2025/2/28 4:03:30 来源:https://blog.csdn.net/zhengzhaoyang122/article/details/142035371  浏览:    关键词:Tomcat Request Cookie 丢失问题

优质博文:IT-BLOG-CN

一、问题描述

生产环境偶尔(涉及到多线程处理)出现"前端传递`Cookie为空"的告警,导致前端请求丢失,出现请求失败问题。告警内容如下

前端传递Cookie为空
告警内容:服务端获取request Cookie为空,请尽快处理!!!
AppId:xxxxxx
ip:xx.xx.xxx.xx
告警事件:2024-03-15

背景:为什么要加Cookie告警:项目出海,需要保证多语言,语言信息从Cookie中获取,所以添加了Cookie告警,告警后发到工作群中,但是相关开发人员告知自己能够正常访问,没有问题,因为正好周五,自己觉得偶发性肯定和并发相关,所以周末研究了下代码,发现和Tomcat Rquest复用机制和ThreadLocal的使用存在缺陷,导致这个偶发性问题

在分析原因前,先需要搞懂一个概念:requesttomcat里面是循环使用的

二、Tomcat 中 Reqeust 复用机制

Request对象的复用机制是为了提高性能和减少垃圾收集压力而设计的。Tomcat使用了一种对象池的机制来管理Request对象和Response对象。通过复用这些对象,Tomcat可以避免频繁地创建和销毁对象,从而提高系统的效率。

复用机制的工作原理
【1】对象池:Tomcat维护一个对象池,用于存储Request对象和Response对象。当一个新的HTTP请求到达时,Tomcat从对象池中获取一个空闲的Request对象和Response对象。如果对象池中没有空闲的对象,Tomcat会创建新的对象。简单看个案例:

public class RequestPool {private Stack<Request> pool = new Stack<>();// 获取对象:getRequest 方法从对象池中获取一个 Request 对象。如果对象池为空,则创建一个新的 Request 对象。public Request getRequest() {if (pool.isEmpty()) {return new Request();} else {return pool.pop();}}// 释放对象:releaseRequest 方法将 Request 对象重置(调用 recycle 方法)并放回对象池中。public void releaseRequest(Request request) {request.recycle();pool.push(request);}
}

【2】对象重置:当一个请求处理完毕后,Request对象会被重置(通过调用recycle方法),以清除上一次请求的状态,使其可以安全地用于下一个请求。以下是org.apache.catalina.connector.Request类中recycle方法的简化源码和解释:

public class Request {// Various fields representing the state of the requestprivate String protocol;private String method;private String requestURI;private String queryString;private String remoteAddr;private String remoteHost;private String serverName;private int serverPort;private boolean secure;private InputStream inputStream;private Reader reader;private ServletInputStream servletInputStream;private BufferedReader bufferedReader;private Map<String, Object> attributes;private Map<String, String[]> parameters;private Cookie[] cookies;private HttpSession session;// Other fields and methods.../*** Recycle this request object.*/public void recycle() {// Reset the state of the request object// 重置基本属性:recycle 方法将 Request 对象的基本属性(如 protocol、method、requestURI 等)重置为初始状态(通常为 null 或默认值)。// 清空集合和数组:attributes 和 parameters 集合被清空,以确保没有残留的请求数据。cookies 数组也被重置为 null。// 重置流和读者:inputStream、reader、servletInputStream 和 bufferedReader 被重置为 null,以确保没有残留的输入流和读者对象。// 重置会话:session 被重置为 null,以确保没有残留的会话信息。protocol = null;method = null;requestURI = null;queryString = null;remoteAddr = null;remoteHost = null;serverName = null;serverPort = 0;secure = false;inputStream = null;reader = null;servletInputStream = null;bufferedReader = null;attributes.clear();parameters.clear();cookies = null;session = null;// Other reset logic...}
}

recycle执行的时机: recycle方法在Tomcat源码中的调用时机主要是在请求处理完毕之后,Request对象被返回到对象池之前。具体来说,recycle方法通常在以下几个场景中被调用:
【1】请求处理完毕后:在Tomcatorg.apache.coyote.Request类中,recycle方法通常在请求处理完毕后被调用。例如,在AbstractProcessorLight类中处理请求和响应的逻辑中,recycle方法被调用来重置Request对象。

// org.apache.coyote.AbstractProcessorLight
public class AbstractProcessorLight<S> implements Processor {// Various fields and methods...@Overridepublic SocketState process(SocketWrapperBase<S> socketWrapper, SocketEvent status) throws IOException {// Process the request and responsetry {// Request processing logic...} finally {// Recycle the request and response objectsrequest.recycle();response.recycle();}return SocketState.CLOSED;}
}

【2】连接关闭时:在Tomcatorg.apache.coyote.http11.Http11Processor类中,当连接关闭时,recycle方法也会被调用。例如,当处理完一个请求并决定关闭连接时,会调用recycle方法。

// org.apache.coyote.http11.Http11Processor
public class Http11Processor extends AbstractProcessorLight<SocketChannel> {// Various fields and methods...@Overridepublic SocketState service(SocketWrapperBase<SocketChannel> socketWrapper) throws IOException {// Service the request and responsetry {// Request servicing logic...} finally {// Recycle the request and response objectsrequest.recycle();response.recycle();}return SocketState.CLOSED;}
}

【3】异常处理:在处理请求的过程中,如果发生异常,Tomcat也会确保调用recycle方法来重置Request对象。例如:

// org.apache.coyote.http11.Http11Processor
public class Http11Processor extends AbstractProcessorLight<SocketChannel> {// Various fields and methods...@Overridepublic SocketState service(SocketWrapperBase<SocketChannel> socketWrapper) throws IOException {try {// Request servicing logic...} catch (Exception e) {// Handle exception and recycle requestrequest.recycle();response.recycle();throw e;}}
}

后期原因分析中需要使用到RequestFacade,这里解释下RequestFacadeRequest之间的关系:RequestFacade是一个包装类Facade,用于保护底层的Request对象,确保应用程序无法直接访问和修改内部实现细节。
【1】Request类: Request类是Tomcat内部用来表示HTTP请求的类,包含了请求的所有详细信息。该类提供了许多方法来访问和操作请求的各个部分,例如请求头、请求参数、输入流等。
【2】RequestFacade 类: RequestFacade类是一个包装器,用于保护Request对象。它实现了javax.servlet.http.HttpServletRequest接口,并将方法调用委托给内部的Request对象。通过使用RequestFacadeTomcat确保了应用程序只能通过标准的HttpServletRequest接口访问请求数据,而不能直接访问或修改Request对象的内部实现。

具体实现:在Tomcat中,RequestFacade类通常包含一个Request对象的引用,并将所有的接口方法调用委托给这个内部的Request对象。例如:

// org.apache.catalina.connector.RequestFacade
public class RequestFacade implements HttpServletRequest {private final Request request;public RequestFacade(Request request) {this.request = request;}@Overridepublic String getParameter(String name) {return request.getParameter(name);}// Other methods from HttpServletRequest interface// All methods delegate to the internal Request object
}

使用场景:在Tomcat处理请求的过程中,当需要将HttpServletRequest对象传递给应用程序时,Tomcat会创建一个RequestFacade实例,并将内部的Request对象传递给它。例如

// org.apache.catalina.connector.CoyoteAdapter
public class CoyoteAdapter implements Adapter {@Overridepublic void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {Request request = (Request) req.getNote(ADAPTER_NOTES);Response response = (Response) res.getNote(ADAPTER_NOTES);// Create a RequestFacade to pass to the applicationHttpServletRequest requestFacade = request.getRequest();// Pass the RequestFacade to the applicationcontext.getPipeline().getFirst().invoke(requestFacade, response);}
}

ThreadLocal的原理不清楚,可以参考ThreadLocal 类

三、原因分析

【1】第一次请求由线程A正常执行,执行完成后执行recycle方法,将RequestFacade中的属性修改为null,准备下次复用,但是当前线程的ThreadLocal没有被清理。
【2】第二次请求恰好也由线程A执行(这也是偶发的原因),通过ThreadLocal获取RequestFacade对象,并通过getCookies获取Cookie,因为第一次请求结束后将Cookie置为null并将cookiesParsed修改为了false,但是这次请求再次调用getCookies的时候,将cookiesParsed修改为了true。用来表示RequestFacade ACookies已经被解析过了。同时需要注意,此时第一次请求的生命周期已经结束了,所以重置cookiesParsed的操作就不复存在了,Tomcat重新复用RequestFacade A的时候Cookies就会获取到一个null

@Override
public Cookie[] getCookies() {if (!cookiesParsed) {parseCookies();}return cookies;
}protected void parseCookies() {cookiesParsed = true;Cookies serverCookies = coyoteRequest.getCookies();int count = serverCookies.getCookieCount();if (count <= 0) {returnl}cookies = new Cookie[count];
}

【3】第三次请求时,Tomcat复用了RequestFacade A,当正常解析Cookies的时候发现cookiesParsedtrue就跳过了正确解析的环节,当需要使用Cookie的时候发现为空,本次请求直接被中止。(灵异事件)

解决方案:
【1】ThreadLocal使用完后一定需要clean
【2】不要在跨线程中使用request对象。可以使用-Dorg.apache.catalina.connector.RECYCLE_FACADES=true禁止复用。在项目的extraenv.sh中设置参数后,如果有访问已经被回收的request对象,就会抛出The request object has been recycled and is no longer associated with this facade异常,以此就能定位到问题

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com