这篇其实是对一年前的一篇文章的补坑。
@Java Web 程序员,我们一起给程序开个后门吧:让你在保留现场,服务不重启的情况下,执行我们的调试代码
当时,就是在spring mvc应用里定义一个api,然后api里,进行如下定义:
大家看上面的注释,就是读取文件流,这个文件流里包含了我们要远程执行的代码;className和methodName,分别指定这个文件的类名和debug方法的方法名。
如果大家看得一脸懵的话,也没关系,下面我基于此次改版升级后的应用给大家举个例子。
假设我有下面这样一个controller。
里面就是调用了一个IRedisCacheService的getCache方法。
结果,上面这个api的结果不符预期,然后我们看看上面的这个getCache的实现。
这里的1处,调用了另一个方法,因为没有日志,也没有打印的返回值。问题可能是返回的不对,也可能是后续的逻辑,把这个返回值改了。现在要排查问题,怎么办呢?
本地调试?麻烦。本地环境和测试环境也不一样,本地能不能重现问题,都是个问题。
大家可以使用阿里出的arthas,但我们这里采用另一种方法。
写个调试文件:
- 1处,注入了一个bean,我们需要调用这个bean的getCount
- 2处,我们定义了一个debug方法,里面调用了,这里的-2这个参数,我是随便传的,这个不重要。我们希望,把这个代码丢到服务器上去执行,然后看3处打印出来的日志,不就可以判断,getCount这一步是否出错了吗?
所以,大家明白了我们要做的事情没?
写一个调试文件(文件里尽量只是查看操作,如果要做那种对数据库、缓存进行修改的话,要慎重一点,代码写稳一点),传到服务端的api,api执行这段代码。然后,我们可以查看服务端的日志,来帮助我们排查问题。
- 编译上传来的debug用途的java文件为class文件,获取其class文件的字节数组
- 定义一个类加载器,从我们第一步拿到的class文件的字节数组中,加载为一个class
- 对class进行反射,创建出对象
- (可选)对对象中的field进行注入(如果field上定义了autowired注解)
- 调用对象的指定方法,如前面的例子,就是调用debug方法
这篇文章,之所以等了这么久,就是一年前,那时候只能上传class文件;当时就想过直接上传java,服务端自动编译,奈何技术问题没搞定,所以后来就拖着了。
这次是怎么搞定了编译问题呢?差不多是直接拷贝了阿里的arthas代码中的相关的几个文件,只要有以下几个步骤,具体请大家克隆源码查看。
- new 一个 com.taobao.arthas.compiler.DynamicCompiler
- 添加要编译的类的源码
- 编译
这个返回的map,key就是类名,value就是class文件的字节码数组。
大家再仔细看看我们的debug代码:
这里面,是用到了我们的应用中的类的,比如上面这个bean。这个bean,在spring boot里,假设是由类加载器A加载的,那我们加载我们这段debug代码,应该怎么加载呢?还是用类加载器A?
ok,没问题。类加载器A,加载了我们的TempDebug这个类。那,假设我改动了一点代码:
这里1处,改了点代码,再次debug,那么,类加载器A还能加载我们的类吗?不能,因为已经缓存了这个类了,不会再次加载。
所以,我们干脆定义一个一次性的类加载器,每次用了就丢。我这里的方法,就是定义一个类加载器A的child。所谓的child,就是符合双亲委派,这个类加载器,除了加载我们的bug类,其他的类,全部丢给parent。
- 1处,把前面编译好的class的字节数组流,传进来
- 2处,重载了findClass,所以,我们是符合双亲委派的,这里,直接去getData,也就是获取字节流数组
- 3处,调用defineClass,生成Class对象。
上面类加载器好了,基本的代码就有了:
我们的service中,实现了ApplicationContextAware接口,让框架给我们注入了:
我们这一步很简单,调用就行了。
https://gitee.com/ckl111/remotedebug
感谢arthas,不然的话,编译java为class文件,我感觉我是暂时搞不出来的。多亏了有这么多优秀的前辈,我们才能走得更远。
大家如有问题,可加群讨论。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.bianchenghao6.com/java-jiao-cheng/9430.html