目录
一、引言
在我们日常工作中总会遇到如下问题,那咱们平时都是怎么处理的呢(先想想)?
-
业务出错,怀疑上游接口问题,但是线上又没有打印接口返回数据怎么办?
-
业务出错,怀疑代码业务逻辑有问题,需要代码逻辑环节输出相应参数定位怎么办?
-
线上有业务数据异常,需要直接数据库操作修复怎么办?
-
上游接口偶尔异常,但是由于没有日志,没有证据怎么办?
-
需要对上游接口做各种场景测试怎么办?
-
想要执行线上某段业务代码逻辑怎么办?
针对上述问题,我们通常有如下常规解决办法:
对于只能在线上环境(生产环境)定位的问题:
-
新增相应的info日志,然后打包、上线,查看日志。
-
新增调试代码逻辑,通过controller调用,然后打包、上线,直接调用controller得到需要的结果。
可以在预发环境(测试床)重现或者定位的问题:
-
新增相应debug日志,然后打包,发布到预发环境,查看日志。
-
新增对应代码逻辑,通过controller调用,然后打包、发布到预发环境,直接调用controller得到需要的结果。
无论怎么处理,最终都需要重新打包、发布,甚至是上线。整个过程不复杂,但是很繁琐。那么有没有一种方法可以不打包,不上线,不发布,不重启就能够在线上直接执行某段代码,或者打印相应的日志呢?
答案是有,接下来本文将介绍一种基于”自定义类加载器“的在线执行方法实践。通过该方法,我们就可以在不打包,不上线,不发布,不重启的前提下,在线上直接执行某些代码逻辑,或者打印任何需要的日志。
二、原理
1、原理概述
主要原理是参考了”深入理解Java虚拟机“类加载器章节,提到的实现,具体内容请查阅书籍。
首先在本地工程编写你需要的代码(一个完整的包含main函数的类)逻辑,所有日志通过System.out的方式输出,然后通过接口将源码直接提交到线上编译,并执行(反射执行main函数),最终返回所有System.out的所有输出结果。该方式的特点如下:
-
代码可以直接在线上执行:则可以执行任何已有代码,编写任何调试逻辑。
-
所有System.out的输出结果都会返回:这样就可以打印任何需要的数据(即日志)。
2、原理详解
其大致原理图和流程图如下:
-
先编写源码,源码可以引用任意线上实例。需要输出(返回)的内容使用System.out.println打印
-
将源码提交到线上
-
通过JavaCompiler对源码进行编译,得到编译后的字节码
-
将字节码中字符串常量池中的java/lang/System,修改为自定义的IntectionSystem。目的是修改System.out.print的输出行为。让其所有输出输出到一个PrintBuffer中,这样执行完后就能够将得到所有的输出内容。
-
使用自定义类加载器加载修改后的类字节码,得到Class类。然后初始化出对应的实例。
-
通过反射调用实例的main函数。
-
执行结果(全部在PrintBuffer中)返回给客户端。这样客户端就可以得到执行结果了。
三、方法的特点
方便、快捷。
方便体现在:可以编译任意代码,这样可以实现任何所需逻辑。
快捷体现在:无需打包、发布、上线和重启等繁琐的步骤,直接提交就可知结果。
四、对线上的影响(内存和性能)
对线上几乎没有影响,因为所有代码的编译是由自定义类加载器(一个局部变量)完成的,当调用完成之后,自定义类加载器就会被回收,那么对应的实例和类都会被回收。
所以不会对线上的内存和性能造成影响。
五、代码实现(服务端部分)
上述核心流程(服务端部分逻辑 )的具体实现已经上传到github。具体请访问:https://github.com/lifeofcoder/dynamic-executor。
六、具体使用(客户端使用)
1、引入jar
首先下载github上的源码,直接mvn编译即可得到jar,然后将jar导入到你的服务端web工程中即可。
2、引入RequestContextListener
在Web.xml中引入RequestContextListener,目标是让我们在自定义代码(非Spring管理对象)中能够获取到ApplicationContext,这样我们才能够获取到Spring管理的所有Bean对象。如果代码不需要获取Spring的Bean,则可以不引入。如果工程本身有其他的方式可以在普通java对象(非Spring管理对象)中获取到Spring管理的对象,则完全不用关系这一步骤了。
3、新增Controller(远程方法执行接口)
因为需要给客户端提供一个提交代码和得到结构的Api入口。所有我们需要在Web工程中增加一个Controller,并做好权限控制(是否需要权限控制大家根据实际情况而定)。如下,只是做了一个简单的权限控制,即要求传递一串指定的密文来做身份验证。具体实践的时候可以根据实际情况,做对应的权限控制。
4、具体调用
我们首先编写一个需要执行的类(类必须有main函数),如下举例,我们调用一个服务(Spring管理的类)的方法,并输出执行结果。
将TextExecutor类的完整代码编码,然后作为code参数传递,然后调用执行接口。
通过执行结果可以看到,我们的测试类TextExecutor中的System.out.print输出的内容被直接返回到客户端了。
七、惯例
如果你对本文有任何疑问或者高见,欢迎添加公众号共同交流探讨(添加公众号可以获得”Java高级架构“上10G的视频和图文资料哦)。