最近线上出现了一个问题,客户端反应用户会异常退出。然后开始前后端一起排查问题,后端日志发现,该请求的cookie为null,导致获取用户信息,返回客户端请登录。

本地debug走起
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.30</version>
<scope>provided</scope>
</dependency>
上面的maven配置可以让你能够调试tomcat源码,注意版本号和你的本地的tocmat版本一致。



public Cookie[] getCookies() {
if (!cookiesConverted) {
convertCookies();//我设置断点的地方
}
return cookies;
}
看上面的代码,如果cookie没有被convert就会covertCookies
protected void convertCookies() {
if (cookiesConverted) {
return;
}
cookiesConverted = true;
if (getContext() == null) {
return;
}
parseCookies();
ServerCookies serverCookies = coyoteRequest.getCookies();
CookieProcessor cookieProcessor = getContext().getCookieProcessor();
int count = serverCookies.getCookieCount();
if (count <= 0) {
return;
}
cookies = new Cookie[count];
int idx=0;
for (int i = 0; i < count; i++) {
ServerCookie scookie = serverCookies.getCookie(i);
try {
/*
we must unescape the '\\' escape character
*/
Cookie cookie = new Cookie(scookie.getName().toString(),null);
int version = scookie.getVersion();
cookie.setVersion(version);
scookie.getValue().getByteChunk().setCharset(cookieProcessor.getCharset());
cookie.setValue(unescape(scookie.getValue().toString()));
cookie.setPath(unescape(scookie.getPath().toString()));
String domain = scookie.getDomain().toString();
if (domain!=null)
{
cookie.setDomain(unescape(domain));//avoid NPE
}
String comment = scookie.getComment().toString();
cookie.setComment(version==1?unescape(comment):null);
cookies[idx++] = cookie;
} catch(IllegalArgumentException e) {
// Ignore bad cookie
}
}
if( idx < count ) {
Cookie [] ncookies = new Cookie[idx];
System.arraycopy(cookies, 0, ncookies, 0, idx);
cookies = ncookies;
}
}
上面的这段代码是具体解析cookie的


回过头去看上面的代码,cookiesConverted为false则会重新covertCookies()。不然直接return,为啥子线程这个对象cookiesConverted被标记为false了呢!!主线程会把他标记为true的啊。我之前没有研读过tomcat的源码,但此时了解生命周期的人一定会有一定的猜想。那就是这个request对象的生命周期其实已经结束了,结束的时候会有一个清理的操作。把这个对象需要被清空的内容清空,方便对象池复用!所以子线程在getCookies的时候这个对象的生命周期理应当被结束了!!而子线程缺错误的把convert属性标记为true。下一个请求进来复用这个对象,由于看到你的标记为true,就不会给你解析cookie。
if (!cookiesConverted) {
convertCookies();//我设置断点的地方
}
return cookies;
下面画个图,来支撑我的观点,方便大家理解

主线程派生出来的子线程在执行getCookie()的情况可以分为两种。 第一种在主线程业务操作期间执行了getCookie(),这种情况不会有问题。
第二种情况在主线程执行clear操作之后把cookiesConverted标记为false的情况下又执行了getCookie()操作,这时就出现了问题,他把cookiesConverted又标记为true了。这时候对象池里的这个对象被下个请求拿到的时候它的cookiesConverted为true,自然不会解析cookie导致获取到cookie为null。所以HttpServletRequest对象不要传给子线程使用,因为在tocmat一个请求的生命周期结束之后这个对象就不应该再被别的地方所修改了。
最后当然就是需要验证下我的猜想,就需要仔细研读下这部分代码,最后证明下来,猜想并不完全正确,tomat的源码更加复杂,并不简单的是一个对象池。有兴趣的同学可以去研读下,这里就不展开了。但是对象肯定是复用了,而且线上这个问题也修复了。
最后的最后还有一个问题, 为啥同事的本地不能复现呢。

