基于自定义类加载器的在线代码执行器(线上编译执行)

499 阅读6分钟

目录

一、引言

二、原理

1、原理概述

2、原理详解

三、方法的特点

四、对线上的影响(内存和性能)

五、代码实现(服务端部分)

六、具体使用(客户端使用)

1、引入jar

2、引入RequestContextListener

3、新增Controller(远程方法执行接口)

4、具体调用

七、惯例


一、引言

在我们日常工作中总会遇到如下问题,那咱们平时都是怎么处理的呢(先想想)?

  1. 业务出错,怀疑上游接口问题,但是线上又没有打印接口返回数据怎么办?
  2. 业务出错,怀疑代码业务逻辑有问题,需要代码逻辑环节输出相应参数定位怎么办?
  3. 线上有业务数据异常,需要直接数据库操作修复怎么办?
  4. 上游接口偶尔异常,但是由于没有日志,没有证据怎么办?
  5. 需要对上游接口做各种场景测试怎么办?
  6. 想要执行线上某段业务代码逻辑怎么办?

针对上述问题,我们通常有如下常规解决办法:

对于只能在线上环境(生产环境)定位的问题:

  1. 新增相应的info日志,然后打包、上线,查看日志。
  2. 新增调试代码逻辑,通过controller调用,然后打包、上线,直接调用controller得到需要的结果。

可以在预发环境(测试床)重现或者定位的问题:

  1. 新增相应debug日志,然后打包,发布到预发环境,查看日志。
  2. 新增对应代码逻辑,通过controller调用,然后打包、发布到预发环境,直接调用controller得到需要的结果。

无论怎么处理,最终都需要重新打包、发布,甚至是上线。整个过程不复杂,但是很繁琐。那么有没有一种方法可以不打包,不上线,不发布,不重启就能够在线上直接执行某段代码,或者打印相应的日志呢?

答案是有,接下来本文将介绍一种基于”自定义类加载器“的在线执行方法实践。通过该方法,我们就可以在不打包,不上线,不发布,不重启的前提下,在线上直接执行某些代码逻辑,或者打印任何需要的日志

二、原理

1、原理概述

主要原理是参考了”深入理解Java虚拟机“类加载器章节,提到的实现,具体内容请查阅书籍。

首先在本地工程编写你需要的代码(一个完整的包含main函数的类)逻辑,所有日志通过System.out的方式输出,然后通过接口将源码直接提交到线上编译,并执行(反射执行main函数),最终返回所有System.out的所有输出结果。该方式的特点如下:

  1. 代码可以直接在线上执行:则可以执行任何已有代码,编写任何调试逻辑。
  2. 所有System.out的输出结果都会返回:这样就可以打印任何需要的数据(即日志)。

2、原理详解

其大致原理图和流程图如下:

  1. 先编写源码,源码可以引用任意线上实例。需要输出(返回)的内容使用System.out.println打印
  2. 将源码提交到线上
  3. 通过JavaCompiler对源码进行编译,得到编译后的字节码
  4. 将字节码中字符串常量池中的java/lang/System,修改为自定义的IntectionSystem。目的是修改System.out.print的输出行为。让其所有输出输出到一个PrintBuffer中,这样执行完后就能够将得到所有的输出内容。
  5. 使用自定义类加载器加载修改后的类字节码,得到Class类。然后初始化出对应的实例。
  6. 通过反射调用实例的main函数。
  7. 执行结果(全部在PrintBuffer中)返回给客户端。这样客户端就可以得到执行结果了。

三、方法的特点

方便、快捷。

方便体现在:可以编译任意代码,这样可以实现任何所需逻辑。

快捷体现在:无需打包、发布、上线和重启等繁琐的步骤,直接提交就可知结果。

四、对线上的影响(内存和性能)

对线上几乎没有影响,因为所有代码的编译是由自定义类加载器(一个局部变量)完成的,当调用完成之后,自定义类加载器就会被回收,那么对应的实例和类都会被回收。

所以不会对线上的内存和性能造成影响。

五、代码实现(服务端部分)

上述核心流程(服务端部分逻辑 )的具体实现已经上传到github。具体请访问:github.com/lifeofcoder…

六、具体使用(客户端使用)

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的视频和图文资料哦)。