在程序代码中,出现过这样一种情况,在一个新的线程组中,尝试去获取一个threadLocal的值,按照相应的程序代码,应该是不能获取到的。但是在实际的运行过程中,却发现能够获取到相应的变量值。
在我们的程序代码中,使用了shiro来存储相应的用户信息,即一个简化版的的权限管理程序。其中,在标准的web程序中,shiro拦截器会把相应的会话信息存储在session中,并且在应用代码中,可以通过一个threadLocal的ThreadContext来获取相应的subject,进而拿到相应的会话值.相应的代码如下所示:
public static UserId getUserId() { Optional<Subject> optional = Optional.ofNullable(ThreadContext.getSubject()); return optional.map(t -> t.getSession(false)) .map(t -> t.getAttribute("userId")) .orElse(null); }
因为相应的数据为web调用时才会注入到sessionId,而如果是一个定时类的任务,那么理论上应该是不会有相应的session对象存在,那么即不会有相应的subject存在,在调用此方法时即会返回在调用getSubject时失败。但是在实际上时,却发生调用subject不为null,进而在调用getSession方法时出现了错误信息。
相应的调用链看起来应该像是这样
- 线程1调用了 ThreadContext.setSubject 方法,设置了session信息
- 线程1完成整个业务方法的运行,相应的session信息被销毁,但相应的removeSubject方法并没有被调用
- 线程1重新以任务的形式执行任务代码,其中调用了getSubject,但不能拿到相应的数据
在这种调用链中,我们只看到了最终的现场,但原始的设置值的现场即不能复现,即不清楚数据是什么时候,哪个调用任务进行处理的。整个问题称之为数据泄漏,即数据在不应该出现的地方出现了。
处理这种问题,一种作法即是异常标记法。详细的步骤如下所示:
- 在调用 ThreadContext.setSubject 时,设置一个异常线程栈(Exception),此里面封装了当前调用的整个路径
- 将异常线程栈(字符串形式)存储在一个threadLocal变量中,方便后面获取
- 在调用 getSubject 时,如果发生了相应的错误信息,即表示复现了相应的错误场景
- 这时候即将之前存储到threadLocal中的字符串获取出来,打印出原始的setSubject调用路径
整个作法参考于alibaba druid的数据库连接泄漏检测。
本文介绍了在不修改源代码的情况下,并且在线上环境,使用greys,来监控相应的调用,当出现了满足条件的场景时,自动打印出相应的调用路径。