在程序代码中,出现过这样一种情况,在一个新的线程组中,尝试去获取一个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,来监控相应的调用,当出现了满足条件的场景时,自动打印出相应的调用路径。
为演示此场景,设计了2个类,如下所示
public class Th1 implements Runnable { public void run() { double d = ThreadLocalRandom.current().nextDouble(); if(d > .5) { setRandom(d); } } public void setRandom(double d) { ThLocal1.s.set("随机数:" + d); } } public class Th2 implements Runnable { public void run() { String s = ThLocal1.s.get(); if(s != null) { findError(s); } } public void findError(String s) { System.out.println("找到一个不存在的问题:" + s); } }
如上的2个类,其中2个方法,是需要注意的。 Th1.setRandom 表示在某些场景中会调用此方法(这就是我们需要捕获的异常栈),而Th2.findError则表示在调用中获取到不应该获取到的数据值。这即是我们在最终定位到的出错的场景。
为处理这个问题,需要一个额外的类,用于存储相应的异常栈,可以用一个静态类来表示(实际场景中应该用一个threadLocal来表示,这里仅作演示)。
为处理此问题,这里使用了greys的script功能,即在拦截到某些调用时,执行一个script。当前所支持的为js脚本.详细文档参考此 https://github.com/oldmanpushcart/greys-anatomy/wiki/JavaScriptSupport.
拦截Th1.setRandom调用
这里即在调用了setRandom后,将相应的异常栈赋值到静态变量中,相应的脚本如下所示:
require(['greys'], function (greys) { greys.watching({ returning: function (output, advice, context) { var e = new java.lang.Exception() var s = com.google.common.base.Throwables.getStackTraceAsString(e); Packages.包名.ThHolder1.holder = s } }); })
拦截Th2.findError调用
这里可以在调用findError之前即调用,直接将之前的异常信息打印出来。脚本如下:
require(['greys'], function (greys) { greys.watching({ before: function (output, advice, context) { var s = Packages.包名.ThHolder1.holder if (!!s) { output.println("出问题时的堆栈" + s) } } }); })
使用greys监控并应用脚本
由于greys一个指令只能监控一个脚本,因此我们需要开两个终端,以运行2个greys指令。相应的指令如下所示:
js 包名.Th1 setRandom 绝对路径/th1js.js js 包名.Th2 findError 绝对路径/th2js.js
执行指令之后,在相应的终端2中,即会在满足条件时打印出相应的调用异常堆栈,如下所示:
出问题时的堆栈java.lang.Exception at jdk.nashorn.internal.scripts.Script$Recompilation$116$114AAA$\^eval\_#365\!12\^eval\_.L:1#returning(<eval>#365:12<eval>:6) ......这里一堆greys以及js调用的堆栈 at sun.reflect.GeneratedMethodAccessor2.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) ...以下即是我们期望的调用栈 at com.iflym.greys.gt.Th1.setRandom(Th1.java:21) at com.iflym.greys.gt.Th1.run(Th1.java:15) at com.google.common.util.concurrent.MoreExecutors$DirectExecutor.execute(MoreExecutors.java:398) at com.iflym.greys.gt.ThMain1.main(ThMain1.java:30)
总结
以上即是整个使用过程,在实际使用过程中,仅当相应的调用栈非项目代码时,可以使用greys临时地处理一下,以尽快查看到相应的场景,但缺陷在于相应的监控脚本必须在线运行,如果终端关闭了,则相应的脚本即被释放。如果相应的代码即为项目代码,并且允许在项目重新发布之后继续处理问题的场景中,还是可以直接将监控的代码调用直接写在代码中,以直接处理相应的问题。并且相应的处理代码可以封装为一个工具及扩展类调用,以直接将相应的问题栈以及拦截处理以插件的方式嵌入到代码中,这样可以在不影响原业务代码的情况下,嵌入逻辑以查找相应的问题。
转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201803120001.html