Instrument 底层的实现实际上也是调用 JVMTI 提供的 RetransformClasses 接口,RetransformClasses 实现对已经加载的类进行重新定义(redefine),而重新定义类也会触发 ClassFileLoadHook 事件,Instrument 同样会监听到这个事件并对被加载的类进行处理。到这里,JVM SandBox 底层依赖 JVM 的核心机制已经介绍完了,下面通过一张时序图将一个 JavaAgent 的加载过程涉及到的相关组件及行为串起来:
本文理解的 JVM SandBox 可插拔至少有两层含义:一层是 JVM 沙箱本身是可以被插拔的,可被动态地挂载到指定 JVM 进程上和可以被动态地卸载;另一层是 JVM 沙箱内部的模块是可以被插拔的,在沙箱启动期间,被加载的模块可以被动态地启用和卸载。
一个典型的沙箱使用流程如下:
复制代码
JVM 沙箱可以被动态地挂载到某个正在运行的目标 JVM 进程之上(前提是目标 JVM 没有禁止 attach 功能),沙箱工作完之后还可以被动态地从目标 JVM 进程卸载掉,沙箱被卸载之后,沙箱对对目标 JVM 进程产生的影响会随即消失(这是沙箱的一个重要特性),沙箱工作示意图如下:
图 4-1 沙箱工作示意图
客户端通过 Attach 将沙箱挂载到目标 JVM 进程上,沙箱的启动实际上是依赖 Java Agent,上文已经介绍过,启动之后沙箱会一直维护着 Instrument 对象引用,在沙箱中 Instrument 对象是一个非常重要的角色,它是沙箱访问和操作 JVM 的唯一通道,后续修改字节码和重定义类都要经过 Instrument。另外,沙箱启动之后同时会启动一个内部的 Jetty 服务器,这个服务器用于外部进程和沙箱进行通信,上面看到的./sandbox.sh -p 33342 -d ‘my-sandbox-module/addLog’ 这行代码,实际上就是通过 HTTP 协议来告诉沙箱执行 my-sanbox-module 这个模块的 addLog 这个功能的。
4.2 无侵入
沙箱内部定义了一个 Spy 类,该类被称为“间谍类”,所有的沙箱模块功能都会通过这个间谍类驱动执行。下面给出一张示意图将业务代码、间谍类和模块代码串起来来帮助理解:
图 4-2 沙箱无侵入核心实现
上图是沙箱 AOP 核心实现的伪代码,实际实现会比上图更复杂一些,沙箱内部通过修改和重定义业务类来实现上述功能的。在接口设计方面,沙箱通过事件驱动的方式,让模块开发者可以监听到方法执行的某个事件并设置回调逻辑,这一切都可以通过实现 AdviceListener 接口来做到,通过 AdviceListener 接口定义的行为,我们可以了解沙箱支持的监听事件如下:
4.3 隔离
JVM 沙箱有自己的工作代码类,而这些代码类在沙箱被挂在到目标 JVM 之后,其涉及到的相关功能实现类都要被加载到目标 JVM 中,沙箱代码和业务代码共享 JVM 进程,这里有两个问题:1)如何避免沙箱代码和业务代码之间产生冲突;2)如何避免不同沙箱模块之间的代码产生冲突。为解决这两个问题,JVM SandBox 定义了自己的类加载器,严格控制类的加载,沙箱的核心类加载器有两个:SandBoxClassLoader 和 ModuleJarClassLoader。SandBoxClassLoader 用于加载沙箱自身的工作类,ModuleJarClassLoader 用于加载三方自己开发的模块功能类,如上面的 MySandBoxModule 类。在沙箱中类加载器继承关系如下图所示:
图 4-3 沙箱类加载器继承体系
通过类加载器,沙箱将沙箱代码和业务代码以及不同沙箱模块之间的代码隔离开来。
4.4 多租户
JVM 沙箱提供的隔离机制也有两层含义,一层是沙箱容器和业务代码之间隔离以及沙箱内部模块之间隔离;另一层是不同用户的沙箱之间的隔离,这一层隔离用来支持多租户特性,也就是支持多个用户对同一个 JVM 同时使用沙箱功能且他们之间互不影响。沙箱的这种机制是通过支持创建多个 SandBoxClassLoader 的方式来实现的,每个 SandBoxClassLoader 关联唯一一个命名空间(namespace)用于标识不同的用户,示意图如下所示:
图 4-4 多租户实现示意图
五、JVM Sandbox 应用场景分析
JVM SandBox 让动态无侵入地对业务代码进行 AOP 这个事情实现起来非常容易,但是这个事情做起来非常容易只是前提条件,更重要的是我们基于 JVM SandBox 能做什么?可以做的很多,比如:故障模拟、动态黑名单,动态日志、动态开关、系统流控、热修复,方法请求录制和结果回放、动态去依赖、依赖超时时间动态修改、甚至是修改 JDK 基础类的功能等等,当然不限于此,这里大家可以打开脑洞,天马行空地思考一下,下面再给出两个 JVM SandBox 应用场景的实现思路。
5.1 故障模拟
我们可以开发一个沙箱模块,通过和前台页面的交互,我们可以对任意业务类的任意方法注入故障来达到故障模拟的效果,用户交互示意图如下:
图 5-1 故障模拟交互示意图
用户通过简单的界面操作即可完成故障注入,应用代码不需要提前埋点。
5.2 动态黑名单
我们还可以开发一个沙箱模块实现 IP 黑名单功能,针对指定 IP 的客户端,服务直接返回空结果,用户交互示意图如下:
图 5-2 动态黑名单交互示意图
引用 JVM SandBox 官网的一句话:“JVM-SANDBOX 还能帮助你做很多很多,取决于你的脑洞有多大了。”
JVM SandBox 是一种无侵入,可动态插拔,JVM 层的 AOP 解决方案,基于 JVM SandBox 我们可以很容易地开发出很多有意思的工具,这完全归功于 JVM SandBox 为我们屏蔽了底层技术细节和实现复杂性。JVM SandBox 很强大,这里需要感谢 JVM SandBox 的作者。除了无侵入,可动态插拔这两个优势之外,JVM SandBox 在 JVM 层支持 AOP 这件事情本身就是一个绝对优势,因为我们开发的 AOP 能力不再依赖应用层所使用的容器,比如不管你使用的是 Spring 容器还是 Plexus 容器,不管你的 Web 容器是 Tomcat 还是 Jetty、统统都没有关系。
回顾一下本文的内容:
回顾 AOP 技术;
介绍 JVM SandBox 是什么、来自哪里、怎么用;
通过 Java Agent 的加载介绍涉及到的 JVM 相关核心技术如:Attach 机制、JVMTI、Instrument 等;
介绍 JVM SandBox 的核心特性的设计与实现如:可插拔、无侵入、隔离、多租户;
介绍 JVM SandBox 可被应用的场景以及两个小例子。
【1】
http://developer.51cto.com/art/201803/568224.htm
【2】
https://github.com/alibaba/jvm-sandbox
【3】
https://www.jianshu.com/p/b72f66da679f