Tomcat Request Cookie 丢失问题解决
作者:程序猿进阶
一、问题描述
生产环境偶尔(涉及到多线程处理)出现"前端传递`Cookie为空"的告警,导致前端请求丢失,出现请求失败问题。告警内容如下
前端传递Cookie为空 告警内容:服务端获取request Cookie为空,请尽快处理!!! AppId:xxxxxx ip:xx.xx.xxx.xx 告警事件:2024-03-15
背景:为什么要加
Cookie
告警:项目出海,需要保证多语言,语言信息从Cookie
中获取,所以添加了Cookie
告警,告警后发到工作群中,但是相关开发人员告知自己能够正常访问,没有问题,因为正好周五,自己觉得偶发性肯定和并发相关,所以周末研究了下代码,发现和Tomcat Rquest
复用机制和ThreadLocal
的使用存在缺陷,导致这个偶发性问题
在分析原因前,先需要搞懂一个概念:request
在tomcat
里面是循环使用的
二、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 request private 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】请求处理完毕后:在Tomcat
的org.apache.coyote.Request
类中,recycle
方法通常在请求处理完毕后被调用。例如,在AbstractProcessorLight
类中处理请求和响应的逻辑中,recycle
方法被调用来重置Request
对象。
// org.apache.coyote.AbstractProcessorLight public class AbstractProcessorLight<S> implements Processor { // Various fields and methods... @Override public SocketState process(SocketWrapperBase<S> socketWrapper, SocketEvent status) throws IOException { // Process the request and response try { // Request processing logic... } finally { // Recycle the request and response objects request.recycle(); response.recycle(); } return SocketState.CLOSED; } }
【2】连接关闭时:在Tomcat
的org.apache.coyote.http11.Http11Processor
类中,当连接关闭时,recycle
方法也会被调用。例如,当处理完一个请求并决定关闭连接时,会调用recycle
方法。
// org.apache.coyote.http11.Http11Processor public class Http11Processor extends AbstractProcessorLight<SocketChannel> { // Various fields and methods... @Override public SocketState service(SocketWrapperBase<SocketChannel> socketWrapper) throws IOException { // Service the request and response try { // Request servicing logic... } finally { // Recycle the request and response objects request.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... @Override public SocketState service(SocketWrapperBase<SocketChannel> socketWrapper) throws IOException { try { // Request servicing logic... } catch (Exception e) { // Handle exception and recycle request request.recycle(); response.recycle(); throw e; } } }
后期原因分析中需要使用到RequestFacade
,这里解释下RequestFacade
与Request
之间的关系:RequestFacade
是一个包装类Facade
,用于保护底层的Request
对象,确保应用程序无法直接访问和修改内部实现细节。
【1】Request
类: Request
类是Tomcat
内部用来表示HTTP
请求的类,包含了请求的所有详细信息。该类提供了许多方法来访问和操作请求的各个部分,例如请求头、请求参数、输入流等。
【2】RequestFacade
类: RequestFacade
类是一个包装器,用于保护Request
对象。它实现了javax.servlet.http.HttpServletRequest
接口,并将方法调用委托给内部的Request
对象。通过使用RequestFacade
,Tomcat
确保了应用程序只能通过标准的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; } @Override public 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 { @Override public 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 application HttpServletRequest requestFacade = request.getRequest(); // Pass the RequestFacade to the application context.getPipeline().getFirst().invoke(requestFacade, response); } }
三、原因分析
【1】第一次请求由线程A
正常执行,执行完成后执行recycle
方法,将RequestFacade
中的属性修改为null
,准备下次复用,但是当前线程的ThreadLocal
没有被清理。
【2】第二次请求恰好也由线程A
执行(这也是偶发的原因),通过ThreadLocal
获取RequestFacade
对象,并通过getCookies
获取Cookie
,因为第一次请求结束后将Cookie
置为null
并将cookiesParsed
修改为了false
,但是这次请求再次调用getCookies
的时候,将cookiesParsed
修改为了true
。用来表示RequestFacade A
的Cookies
已经被解析过了。同时需要注意,此时第一次请求的生命周期已经结束了,所以重置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
的时候发现cookiesParsed
为true
就跳过了正确解析的环节,当需要使用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
异常,以此就能定位到问题
到此这篇关于Tomcat Request Cookie 丢失问题解决的文章就介绍到这了,更多相关Tomcat Request Cookie 丢失内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!