透过JVM-SANDBOX源码,了解字节码增强技术

Posted by Binbin Zhang on Thu, Sep 7, 2023

介绍

JVM 沙箱容器是一种 JVM 的非侵入式运行期 AOP 解决方案。通过 JVM-SANDBOX 可以在不重启,不侵入目标 jvm 的前提下对目标方法进行代码增强。

无侵入,类隔离,可插拔,多租户,高兼容是它的特性,JVM-SANDBOX 是相对偏底层的代码增强框架利用它可以搞很多事情,例如线上系统流控、线上系统的请求录制、结果回放,线上故障定位等等。如开源项目 jvm-sandbox-repeaterchaosblade 都是在此基础上构建的,详细介绍可以到 github 中了解,项目地址: https://github.com/alibaba/jvm-sandbox

在本文中将通过分析 JVM-SANDBOX 的底层源码,从而了解一个字节码增强框架的核心实现。

架构设计

JVM Sandbox 内置 HTTP 服务器(Jetty)接受用户指令从而对模块进行管理,例如激活,冻结,刷新等。通过自定义的 ClassLoader 进行类隔离,基于 Java 虚拟机工具接口(JVMTI),利用 ASM 框架对目标方法进行代码增强。

JVM-SANDBOX 分为多个子项目,其中 agent,core,spy 是主程序库。agent:沙箱启动的代理,core:沙箱内核,spy:沙箱间谍类(代码增强的埋点类)。下图介绍了 sandbox 整体架构中最重要的模块/功能,这些能力都是在 agent,core,spy 三个子项目中提供的。

  1. 模块控制管理:负责管理 sandbox 自身模块以及使用者自定义模块,例如模块的加载,激活,冻结,卸载

  2. 事件监听处理:用户自定义模块实现 Event 接口对增强的事件进行自定义处理,等待事件分发处理器的触发。

  3. 沙箱事件分发处理器:对目标方法增强后会对目标方法追加三个环节,分别为方法调用前 BEFORE、调用后 RETURN、调用抛异常 THROWS、当代码执行到这三个环节时则会由分发器分配到对应的事件监听执行器中执行。

  4. 代码编织框架:通过 ASM 框架依托于 JVMTI 对目标方法进行字节码修改,从而完成代码增强的能力。

  5. 检索过滤器:当用户对目标方法创建增强事件时,沙箱会对目标 jvm 中的目标类和方法进行匹配以及过滤。匹配到用户设定目标类和方法进行增强,过滤掉 sandbox 内部的类以及 jvm 认为不可修改的类。

  6. 加载类检索: 获取到需要增强的类集合依赖检索过滤器模块

  7. HTTP 服务器:通过 http 协议与客户端进行通信(sandbox.sh 即可理解为客户端)本质是通过 curl 命令下发指令到沙箱的 http 服务器。例如模块的加载,激活,冻结,卸载等指令

相关技术

在分析之前先简单介绍一些字节码增强框架中会使用到的底层技术,本质上任何一个字节码增强框架都是围绕这些底层技术在上层进行建设。

JVM TI

JVM TI(JVM TOOL INTERFACE,JVM 工具接口)是 JVM 提供的一套对 JVM 进行操作的工具接口。通过 JVMTI 可以实现对 JVM 的多种操作,它通过接口注册各种事件勾子,在 JVM 事件触发的同时触发预定义的勾子,以实现对各个 JVM 事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC 开始和结束、方法调用进入和退出、临界区竞争与等待、VM 启动与退出等等。

当 JVM 加载类文件时会触发类文件加载钩子事件 ClassFileLoadHook,从而触发 Instrumentation 类库中的 ClassFileTransformer (字节码转换器)的 transform 方法,在 transfrom 方法中可以对字节码进行转换

Instrumentation

Instrumentation 是 JVM 提供的可以在运行时动态修改已加载类的基础库。java agent 是 JVM TI 接口的一种实现,Instrumentation 实例只能通过 agent 的 premain 或者 agentmian 方法的参数中获取。

加载 agent 常见的方式有两种

  1. 在启动脚本中增加-javaagent 参数这种方式会伴随着 JVM 一起启动,agent 中需要提供大名鼎鼎的 premain 方法,顾名思义 premain 是在 main 方法运行前执行的,然后才会去运行主程序的 main 方法,这样就要求开发者在应用启动前就必须确认代理的处理逻辑和参数内容等等。这种挂在 agent 方式的好处是如果 agent 启动需要加载大量的类,随着 jvm 启动时直接加载不会导致 JVM 在运行时卡顿或者 CPU 抖动,缺点是不够灵活。

  2. 利用 Attach API 在 JVM 运行时不需要重启的情况下即可完成挂载,agent 需要提供 agentmain 方法,即插即用的模式非常灵活,Attach API 提供了一种附加到 Java 虚拟机的机制,使用此 API 附加到目标虚拟机并将其工具代理加载到该虚拟机中,本质上就是提供了和目标 jvm 通讯的能力。例如我们常常使用的 jastck,jmap 等命令都是利用 attach api 先与目标 jvm 建立通讯再执行命令。但如果 agent 启动需要加载大量的类可能会导致目标 jvm 出现卡顿,cpu 抖动等情况

在 Instrumentation 接口中提供了多个 api 用来管理和操作字节码。我们重点关注以下几个即可:

  1. addTransformer:注册字节码转换器,当注册一个字节码转换器后,所有的类加载都会经过字节码转换器进行处理。

  2. retransformClasses 重新对 JVM 已加载的类进行字节码转换

  3. removeTransformer 删除已注册的字节码转换器,删除后新加载的类不会再经过字节码转换器处理,但是已经“增强”过的类还是会继续保留

ClassFileTransformer

ClassFileTransformer(字节码转换器)是一个接口,接口中只有 transform 一个方法。

1byte[] transform(  ClassLoader         loader,
2            String              className,
3            Class<?>            classBeingRedefined,
4            ProtectionDomain    protectionDomain,
5            byte[]              classfileBuffer)
6    throws IllegalClassFormatException;

在 transform 可以返回转换后的字节码 byte[],可以通过 Instrumentation#addTransformer 方法将实现的字节码转换器进行注册,一旦注册后字节码转换器就会在合适的时机被触发。

  1. 新加载类的时候,例如 ClassLoader.defineClass

  2. 重新定义类的时候,例如 Instrumentation.redefineClasses

  3. 对类重新转换的时候,例如 Instrumentation.retransformClasses

字节码生成

在 ClassFileTransformer#transform 方法会返回转换后的字节码 byte[],那如何动态生成字节码呢,这就要提到大名鼎鼎的 ASM 框架了。

ASM 是一个通用的 Java 字节码操作和分析框架。它能够以二进制形式修改已有的类或是动态生成类。很多利用字节码增强技术的开源项目都是基于 ASM 进行构建的,如 CGLIB,Groovy,Kotlin 编译器等等,ASM 提供了一些常见的字节码转换和分析算法,从中可以构建定制的复杂转换和代码分析工具;几个核心的类:

  • ClassReader:此类主要功能就是读取字节码文件,然后把读取的数据通知 ClassVisitor;

  • ClassVisitor:用于生成和转换编译类的 ASM API 基于 ClassVisitor 抽象类,接收 ClassReader 发出的对 method 的访问请求,并且替换为另一个自定义的 MethodVisitor

  • ClassWriter:其继承于 ClassVisitor,主要用来生成类;

大体的执行流程是首先需要加载原 Class 文件,然后通过访问者模式访问所有元素,在访问的过程中对各元素进行改造,最后重新生成一个字节码的 byte[],如图:

ClassLoader

沙箱中另一个特性类隔离是通过实现自定义的 classLoader 来完成的,ClassLoader(类加载器),在 java 中所有的类必须通过类加载器正确加载后才能运行,类加载器虽然只用于实现类的加载动作,但它在 Java 程序中起到的作用却远超类加载阶段。

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个 Java 虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

加载器种类

在 java8 中默认 jvm 会提供三个类加载器,分别是

  1. 启动类加载器(Bootstrap Class Loader):这个类加载器负责加载存放在 <JAVA_HOME>\lib 目录,或者被-Xbootclasspath 参数所指定的路径中存放的,而且是 Java 虚拟机能够 识别的(按照文件名识别,如 rt .jar、t ools.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类 库加载到虚拟机的内存中

  2. 扩展类加载器(Extension Class Loader):这个类加载器是在类 sun.misc.Launcher$ExtClassLoader 中以 Java 代码的形式实现的。它负责加载<JAVA_HOM E>\lib\ext 目录中,或者被 java.ext.dirs 系统变量所 指定的路径中所有的类库。

  3. 应用程序类加载器(Application Class Loader):这个类加载器由 sun.misc.Launcher$App ClassLoader 来实现。由于应用程序类加载器是 ClassLoader 类中的 getSystem- ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有 自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

  4. 自定义类加载器:顾名思义是由开发者自定义的类加载器

双亲委派模型

上面介绍了三个系统提供的类加载器,那么他们之间以及和自定义类加载器是如何协作的呢?

各种类加载器的协作关系如图,这种层次关系就是类加载器的“双亲委派模型”,除了顶层的启动类加载器以外,其余的类加载器都应有自己的父类加载器。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是 Java 中的类随着它的类加载器一起具备了一 种带有优先级的层次关系。例如类 java. lang.Object,它存放在 rt . jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类 在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个 类加载器自行去加载的话,如果用户自己也编写了一个名为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中就会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱 。

自定义类加载器

通常情况下自定义类加载器只需要继承 URLClassLoader,重写 findClass 方法即可,当然也可以直接继承 ClassLoader,不过 ClassLoader 只能加载 classpath 下面的类,而 URLClassLoader 可以加载任意路径下的类。

URLClassLoader 是 ClassLoader 的子类,它用于从指向 JAR 文件和目录的 URL 的搜索路径加载类和资源。也就是说通过 URLClassLoader 就可以加载指定 jar 中的 class 到内存中。

沙箱类隔离策略

在了解了 classloader 相关知识后,我们看一下 jvm-sandbox 提供的官方类隔离策略图

在沙箱中自定义了 SandboxClassLoader 以及 ModuleJarClassLoader 来分别加载沙箱内部类和模块中的类,sandbox agent 则是由 AppClassLoader 进行加载的,而 sandbox spy 间谍类是用 BootstrapClassLoader 进行加载,目的就是利用双亲委派加载模型,保证间谍类可以正确的被目标 JVM 加载,从而植入到目标 jvm 中完成与业务代码的交互。

SPI

SPI 全称 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,提供了通过 interface 寻找 implement 的方法。类似于 IOC 的思想,将装配的控制权移到程序之外,从而实现解耦。

关于 SPI 的底层实现在这里就不分析了,在沙箱中自定义模块的实现则是利用了 SPI 机制,当我们自定义模块除了要实现 Module interface,还需要按照 SPI 的约定在 resource/META-services 下以 com.alibaba.jvm.sandbox.api.Module 为文件名,文件内容则是 Module 的实现类路径。当使用 SPI 加载实现类时需要传递一个 classLoader,这个 classLoader 是 moduleClassLoader,moduleClassLoader 中是继承自 URLClassLoader,SPI 加载类则查找当前 classLoader 对应的资源路径,从而找到匹配 Module interface 路径的文件,然后加载对应的实现类。

适应场景:调用者根据需要,使用、扩展或替换实现策略。

启动过程

在 sandbox 启动过程中涉及到 Agent 挂载,自定义 classloader 加载 sandbox 内部类,初始化 http 服务器,模块的加载以及初始化。

挂载 Agent

JVM-SANDBOX 对两种 agent 的挂载模式都支持。

  1. -javaagent 启动脚本中直接挂载

  2. 利用 attach api 在运行时挂载

如果启动时直接加载则直接在目标 JVM 的启动脚本中增加-javaagent 指定 sandbox-agent.jar 即可。当执行./sandbox -p pid(目标 jvm 进程 id)是使用的 attach api 方式进行 agent 的挂载

在运行上述指令时执行的是 sandbox.sh 中 attach_jvm func,在 attach_jvm 中是通过 java -jar 启动 sandbox-core,并指定了 sandbox-agent.jar 的路径地址等参数。

 1function attach_jvm() {
 2  # attach target jvm
 3  "${SANDBOX_JAVA_HOME}/bin/java" \
 4    ${SANDBOX_JVM_OPS} \
 5    -jar "${SANDBOX_LIB_DIR}/sandbox-core.jar" \
 6    "${TARGET_JVM_PID}" \
 7    "${SANDBOX_LIB_DIR}/sandbox-agent.jar" \
 8    "home=${SANDBOX_HOME_DIR};token=${token};server.ip=${TARGET_SERVER_IP};server.port=${TARGET_SERVER_PORT};namespace=${TARGET_NAMESPACE}" ||
 9    exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail."
10}

在 sandbox-core 项目的 pom.xml 文件中指定了 core 程序的启动类 CoreLauncher.java

1<manifest>
2    <mainClass>com.alibaba.jvm.sandbox.core.CoreLauncher</mainClass>
3</manifest>

在 CoreLauncher 中利用 Attach api 对目标 jvm 进行 jvm-sandbox agent 挂载。

1private void attachAgent(final String targetJvmPid,
2                         final String agentJarPath,
3                         final String cfg) throws Exception {
4        vmObj = VirtualMachine.attach(targetJvmPid);
5        if (vmObj != null) {
6            vmObj.loadAgent(agentJarPath, cfg);
7        }
8}

加载 Agent

在 agent 项目中只有两个 java 文件:AgentLaucher.java、SandboxClassLoader.java

在 agent 项目的 pom.xml 中指定了程序的 Premain-Class 以及 Agent-Class,分别对应上面两种 agent 的挂载方式,当使用-javaagent 挂载 agent 时则会加载 Premain-Class 的 premain 方法,当使用 Attach api 挂载 agent 时则会加载 Agent-Class 的 agentmain 方法

1<manifestEntries>
2    <Premain-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Premain-Class>
3    <Agent-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Agent-Class>
4    <Can-Redefine-Classes>true</Can-Redefine-Classes>
5    <Can-Retransform-Classes>true</Can-Retransform-Classes>
6</manifestEntries>

在两个 main 方法中接收的参数是相同的分别为:String featureString 和 Instrumentation inst

  1. String featureString 是脚本中的执行脚本执行 attach 时传递过来的参数 如 sandbox 路径地址,token,namspace(租户)等

  2. Instrumentation 是 JVM 提供的可以在运行时动态修改已加载类的基础库,获取 Instrumentation 实例只能通过 premain 或者 agentmian 方法参数中获取。

 1//启动加载 -javaagent
 2public static void premain(String featureString, Instrumentation inst) {
 3    LAUNCH_MODE = LAUNCH_MODE_AGENT;
 4    install(toFeatureMap(featureString), inst);
 5}
 6//动态加载 attach api
 7public static void agentmain(String featureString, Instrumentation inst) {
 8    LAUNCH_MODE = LAUNCH_MODE_ATTACH;
 9    final Map<String, String> featureMap = toFeatureMap(featureString); //解析惨参数
10    writeAttachResult(
11            getNamespace(featureMap), //获取租户所属 namespace
12            getToken(featureMap), //获取 token
13            install(featureMap, inst)
14    );
15}

无论是哪种方式挂载 agent 核心都在 install 方法中,install 中是开始加载 agent 的业务逻辑

初始化 Agent

在 install 方法中完成对 agent 的初始化,在初始化的过程中使用到了自定义的 SandboxClassLoader 对沙箱类进行加载,实现沙箱内部类与业务类隔离。

Spy 间谍类

在 install 中首先会利用 Instrumentation 实例将 sandbox-spy.jar 添加到 BootstrapClassLoader 的搜索范围内。

1// 将 Spy 注入到 BootstrapClassLoader
2inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
3        getSandboxSpyJarPath(home)
4        // SANDBOX_SPY_JAR_PATH
5)));

为什么这么做,在解释前我们先了解下 Spy 间谍类是什么。在沙箱的世界观中,任何一个 Java 方法的调用都可以分解为 BEFORE、RETURN 和 THROWS 三个环节,由此在三个环节上引申出对应环节的事件探测和流程控制机制。

 1// BEFORE
 2try {
 3   /*
 4    * do something...
 5    */
 6    // RETURN
 7    return;
 8} catch (Throwable cause) {
 9    // THROWS
10}

而 Spy 间谍类就是实现了 before,return,throws 等钩子函数。当将 Spy 间谍类买点到业务代码中时,触发类加载机制时利用双亲委派模型则可以层层向上查找,在 BootstrapClassLoader 中就可以将 Spy 间谍类正确加载,而在 Spy 的间谍类中内置了沙箱的 SpyHandler。这样就完成了目标类和沙箱内核的通讯。本质的类增强策略如下图:

在加载 agent 执行 install 方法中首先会将 Spy 间谍类追加到 BootstrapClassLoader 的搜索范围内,这样当对业务代码增强时通过 classloader 双亲委派机制,Spy 间谍类一定会被正确加载,Spy 间谍类会将 before,return,throws 等钩子函数顺利的注入到业务代码中。

SandBoxClassLoader

在将 Spy 间谍类追加到 BootstrapClassLoader 中后创建 SandBoxClassLoader,目的是使用自定义的 classLoader 可以尽量减少对目标 JVM 的侵入。

1final ClassLoader sandboxClassLoader = loadOrDefineClassLoader(
2        namespace,
3        getSandboxCoreJarPath(home)
4        // SANDBOX_CORE_JAR_PATH
5);

在自定义的 SandboxClassLoader 中加载类破坏了双亲委派模型,首先让自身加载,如果自身加载失败后再向上委托加载。这样做的目的是明确知道 SandboxClassLoader 只会加载沙箱 jar 文件的类,而这些 jia 文件路径并不在目标的 JVM 的 ClasssLoader 可搜索的路径上,所以向上委托加载无任何意义,破坏掉双亲委派模型,优先自身加载性能更好。

 1 @Override
 2    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
 3        final Class<?> loadedClass = findLoadedClass(name);
 4        if (loadedClass != null) {
 5            return loadedClass;
 6        }
 7        try {
 8            Class<?> aClass = findClass(name);
 9            if (resolve) {
10                resolveClass(aClass);
11            }
12            return aClass;
13        } catch (Exception e) {
14            return super.loadClass(name, resolve);
15        }
16    }

启动 HTTP 服务

在创建完 SandboxClassLoader 后则会利用 SandboxClassLoader 加载 core.jar 中的代理类(ProxyCoreServer),然后反射调用 ProxyCoreServer 的 bind 方法来初始化 HTTP 服务以及加载所有模块。

 1@Override
 2public synchronized void bind(final CoreConfigure cfg, final Instrumentation inst) throws IOException {
 3    this.cfg = cfg;
 4    try {
 5        initializer.initProcess(new Initializer.Processor() {
 6            @Override
 7            public void process() throws Throwable {
 8                logger.info("initializing server. cfg={}", cfg);
 9                jvmSandbox = new JvmSandbox(cfg, inst);
10                initHttpServer(); //初始化 http server
11                initJettyContextHandler(); //初始化 jetty context 处理器
12                httpServer.start();// 启动 http server
13            }
14        });
15        // 初始化加载所有的模块
16        try {
17            jvmSandbox.getCoreModuleManager().reset();
18        } catch (Throwable cause) {
19            logger.warn("reset occur error when initializing.", cause);
20        }
21        ......
22}

在 bind 中会初始化 HttpServer 并初始化上下文处理器,在初始化上下文处理器中将会绑定/module/http/*的 Servlet 为 ModuleHttpServlet,这样的话当我们对模块进行操作的话都会先经过 ModuleHttpServlet 进行匹配然后分发到具体的模块中执行命令。

 1private void initJettyContextHandler() {
 2     ......
 3    // module-http-servlet
 4    final String pathSpec = "/module/http/*";
 5    logger.info("initializing http-handler. path={}", contextPath + pathSpec);
 6    context.addServlet(
 7            new ServletHolder(new ModuleHttpServlet(cfg, jvmSandbox.getCoreModuleManager())),
 8            pathSpec
 9    );
10
11    httpServer.setHandler(context);
12}

小结

在 sandbox 启动时首先需要利用 Attach API 或者 java -javaagent 挂在 agent。agent 需要提供对应的 agetmain 方法或者 premain 方法,在 agent 对应的 main 方法中进行初始化。

首先将 spy 间谍类追加到 BootstrapClassLoader 中为后面代码增强做准备,最终目的是让目标 JVM 业务类可以和沙箱进行通讯,然后创建自定义的 SandboxClassLoader 用来加载沙箱内部类从而实现类隔离,最后启动 HttpServer 并初始化对应的 Servlet,启动完成后加载并初始化所有 module

模块管理

目前在 sandbox 中有两个地方会存储模块:

  1. $sandbox_home/module/沙箱系统模块目录,由配置项 system_module 进行定义。用于存放沙箱通用的管理模块,比如用于沙箱模块管理功能的 module-mgr 模块,未来的模块运行质量监控模块、安全校验模块也都将存放在此处,跟随沙箱的发布而分发。系统模块不受刷新(-f)、**强制刷新(-F)功能的影响,只有容器重置(-R)**能让沙箱重新加载系统模块目录下的所有模块。

  2. $sandbox_home/sandbox-module/沙箱用户模块目录,由 sandbox.properties 的配置项 user_module 进行定义,默认为${HOME}/.sandbox-module/。一般用于存放用户自研的模块。自研的模块经常要面临频繁的版本升级工作,当需要进行模块动态热插拔替换的时候,可以通过**刷新(-f)或强制刷新(-F)**来完成重新加载。

模块的生命周期

在沙箱中模块一共有四种状态

  1. 【加载模块】被沙箱正确加载,沙箱将会允许模块进行命令相应、代码插桩等动作

  2. 【激活模块】加载成功后默认是冻结状态,需要代码主动进行激活。模块只有在激活状态下才能监听到沙箱事件

  3. 【冻结模块】进入到冻结状态之后,之前侦听的所有沙箱事件都将被屏蔽。需要注意的是,冻结的模块不会退回事件侦听的代码插桩,只有 delete()、wathcing()或者模块被卸载的时候插桩代码才会被清理

  4. 【卸载沙箱】不会再看到该模块,之前给该模块分配的所有资源都将会被回收,包括模块已经侦听事件的类都将会被移除掉侦听插桩,干净利落不留后遗症

模块的状态分别对应着模块的生命周期,一个模块在沙箱中的生命周期如下:

MODULE_LOAD(模块加载),MODULE_UNLOAD(模块卸载),MODULE_ACTIVE(模块激活),MODULE_FROZE(模块冻结),MODULE_LOAD_COMPLETED(模块加载完成)

模块加载

在启动过程中初始化 http server 的最后一步是加载并初始化所有 module

1 jvmSandbox.getCoreModuleManager().reset();

我们来看一下加载 module 都做了哪些事情,因为这里是初始化操作,所以在 reset 中首先强制卸载所有模块,避免之前有已加载的模块。然后遍历 moduleLibDirArray 加载模块,moduleLibDirArray 指的是模块的存储路径,

在 reset 中会遍历这两个路径对路径下的模块进行加载。

 1public synchronized CoreModuleManager reset() throws ModuleException {
 2
 3    logger.info("resetting all loaded modules:{}", loadedModuleBOMap.keySet());
 4
 5    // 1. 强制卸载所有模块
 6    unloadAll();
 7
 8    // 2. 加载所有模块
 9    for (final File moduleLibDir : moduleLibDirArray) {
10        // 用户模块加载目录,加载用户模块目录下的所有模块
11        // 对模块访问权限进行校验
12        if (moduleLibDir.exists() && moduleLibDir.canRead()) {
13            new ModuleLibLoader(moduleLibDir, cfg.getLaunchMode())
14                    .load(
15                            new InnerModuleJarLoadCallback(),
16                            new InnerModuleLoadCallback()
17                    );
18        } else {
19            logger.warn("module-lib not access, ignore flush load this lib. path={}", moduleLibDir);
20        }
21    }
22
23    return this;
24}

在上面经过方法层层调用会走到 ModuleJarLoader.java 的 load 方法中,这里面接收的参数则是上面的 InnerModuleLoadCallback 对象,在这个方法中我们可以看到又创建了一个 ModuleJarClassLoader,通过字面意思理解这个 ClassLoader 主要是为了加载模块的类.

创建后并指定了当前加载模块的线程使用 ModuleJarClassLoader,这样的话后面真正加载模块中的类时即可默认使用 ModuleJarClassLoader 了。

 1void load(final ModuleLoadCallback mCb) throws IOException {
 2
 3    boolean hasModuleLoadedSuccessFlag = false;
 4    ModuleJarClassLoader moduleJarClassLoader = null;
 5    logger.info("prepare loading module-jar={};", moduleJarFile);
 6    try {
 7        moduleJarClassLoader = new ModuleJarClassLoader(moduleJarFile);
 8
 9        final ClassLoader preTCL = Thread.currentThread().getContextClassLoader();
10        Thread.currentThread().setContextClassLoader(moduleJarClassLoader);
11
12        try {
13            hasModuleLoadedSuccessFlag = loadingModules(moduleJarClassLoader, mCb);
14        } finally {
15            Thread.currentThread().setContextClassLoader(preTCL);
16        }

ModuleJarClassLoader

ModuleJarClassLoader 继承自一个叫 RoutingURLClassLoader,它的构造方法则是调用父类 RoutingURLClassLoader 的构造方法并传递了两个参数,两个参数都是待加载模块 jar 文件。

 1public class ModuleJarClassLoader extends RoutingURLClassLoader {
 2
 3    private ModuleJarClassLoader(final File moduleJarFile,
 4                                 final File tempModuleJarFile) throws IOException {
 5        super(
 6                new URL[]{new URL("file:" + tempModuleJarFile.getPath())},
 7                new Routing(
 8                        ModuleJarClassLoader.class.getClassLoader(),
 9                        "^com\\.alibaba\\.jvm\\.sandbox\\.api\\..*",
10                        "^javax\\.servlet\\..*",
11                        "^javax\\.annotation\\.Resource.*$"
12                )
13        );
14    }

RoutingURLClassLoader

RoutingURLClassLoader 中我们重点关注下构造方法和 loadClass 方法,在构造方法中接收了 ModuleJarClassLoader 传递过来的要加载的模块 jar 路径以及 Routing 对象,Routing 对象中包含了 ModuleJarClassLoader 的 ClassLoader(SandboxClassLoader)以及一些正则匹配的类路径。

通过 loadClass 方法的实现不难看出,当要加载的 className 正则匹配成功则直接委托 SandboxClassLoader 加载,这样的好处是不需要每个模块都加载一遍这些通用的类。当要加载的 className 正则匹配失败,则用自身进行加载,如果加载不成功则向上继续委托加载。

 1public class RoutingURLClassLoader extends URLClassLoader {
 2
 3    private static final Logger logger = LoggerFactory.getLogger(RoutingURLClassLoader.class);
 4    private final ClassLoadingLock classLoadingLock = new ClassLoadingLock();
 5    private final Routing[] routingArray;
 6
 7    public RoutingURLClassLoader(final URL[] urls,
 8                                 final Routing... routingArray) {
 9        super(urls);
10        this.routingArray = routingArray;
11    }
12    
13    @Override
14    protected Class<?> loadClass(final String javaClassName, final boolean resolve) throws ClassNotFoundException {
15      return classLoadingLock.loadingInLock(javaClassName, new ClassLoadingLock.ClassLoading() {
16        @Override
17        public Class<?> loadClass(String javaClassName) throws ClassNotFoundException {
18            // 优先查询类加载路由表,如果命中路由规则,则优先从路由表中的 ClassLoader 完成类加载
19            if (ArrayUtils.isNotEmpty(routingArray)) {
20                for (final Routing routing : routingArray) {
21                    if (!routing.isHit(javaClassName)) {
22                        continue;
23                    }
24                    final ClassLoader routingClassLoader = routing.classLoader;
25                    try {
26                        return routingClassLoader.loadClass(javaClassName);
27                    } catch (Exception cause) {
28                        // 如果在当前 routingClassLoader 中找不到应该优先加载的类(应该不可能,但不排除有就是故意命名成同名类)
29                        // 此时应该忽略异常,继续往下加载
30                        // ignore...
31                    }
32                }
33            }
34            // 先走一次已加载类的缓存,如果没有命中,则继续往下加载
35            final Class<?> loadedClass = findLoadedClass(javaClassName);
36            if (loadedClass != null) {
37                return loadedClass;
38            }
39            try {
40                Class<?> aClass = findClass(javaClassName);
41                if (resolve) {
42                    resolveClass(aClass);
43                }
44                return aClass;
45            } catch (Exception cause) {
46                DelegateBizClassLoader delegateBizClassLoader = BusinessClassLoaderHolder.getBussinessClassLoader();
47                try {
48                    if(null != delegateBizClassLoader){
49                        return delegateBizClassLoader.loadClass(javaClassName,resolve);
50                    }
51                } catch (Exception e) {
52                    //忽略异常,继续往下加载
53                }
54                return RoutingURLClassLoader.super.loadClass(javaClassName, resolve);
55            }
56        }
57    });
58}
59}

在 ModuleJarLoader.java 的 load 方法中除了创建 ModuleJarClassLoader,还调用了 loadModules 方法,在方法中利用 ServiceLoader(SPI)加载 Module 接口的实现,然后遍历检查接口实现是否符合要求,例如是否有@Infomation 注解,模块唯一 id 是否合法,模块的启动方式是否和沙箱的启动方式一致。当这些前置检查都通过后,调用 ModuleLoadCallback.load 进行模块的加载。

 1private boolean loadingModules(final ModuleJarClassLoader moduleClassLoader,
 2                               final ModuleLoadCallback mCb) {
 3
 4    final Set<String> loadedModuleUniqueIds = new LinkedHashSet<String>();
 5    final ServiceLoader<Module> moduleServiceLoader = ServiceLoader.load(Module.class, moduleClassLoader);
 6    final Iterator<Module> moduleIt = moduleServiceLoader.iterator();
 7    while (moduleIt.hasNext()) {
 8
 9        final Module module;
10        try {
11            module = moduleIt.next();
12        } catch (Throwable cause) {
13            logger.warn("loading module instance failed: instance occur error, will be ignored. module-jar={}", moduleJarFile, cause);
14            continue;
15        }
16
17        final Class<?> classOfModule = module.getClass();
18
19        // 判断模块是否实现了@Information 标记
20        if (!classOfModule.isAnnotationPresent(Information.class)) {
21            logger.warn("loading module instance failed: not implements @Information, will be ignored. class={};module-jar={};",
22                    classOfModule,
23                    moduleJarFile
24            );
25            continue;
26        }
27
28        final Information info = classOfModule.getAnnotation(Information.class);
29        final String uniqueId = info.id();
30
31        // 判断模块 ID 是否合法
32        if (StringUtils.isBlank(uniqueId)) {
33            logger.warn("loading module instance failed: @Information.id is missing, will be ignored. class={};module-jar={};",
34                    classOfModule,
35                    moduleJarFile
36            );
37            continue;
38        }
39
40        // 判断模块要求的启动模式和容器的启动模式是否匹配
41        if (!ArrayUtils.contains(info.mode(), mode)) {
42            logger.warn("loading module instance failed: launch-mode is not match module required, will be ignored. module={};launch-mode={};required-mode={};class={};module-jar={};",
43                    uniqueId,
44                    mode,
45                    StringUtils.join(info.mode(), ","),
46                    classOfModule,
47                    moduleJarFile
48            );
49            continue;
50        }
51
52        try {
53            if (null != mCb) {
54                mCb.onLoad(uniqueId, classOfModule, module, moduleJarFile, moduleClassLoader);
55            }
56        }
57        ......
58    }

@Infomation

注解@Infomation 是沙箱内部定义的,主要是用来描述模块的信息。在上面 loadingModules 方法中使用 SPI 加载完对应的模块实现类,会查找对应的@Infomation 信息做一些前置检查操作。

1@Information(id = "broken-clock-tinker")
2public class BrokenClockTinkerModule implements Module 

onLoad(真正的加载)

在上面的 loadingModules 方法中根据 SPI 机制已经获取到模块的实现类了,在 loadingModules 最后会调用 ModuleLoadCallback 的 onLoad 方法,onLoad 调用 DefaultCoreModuleManager#load 方法在这里则是真正加载模块实现类里面的内容。

加载的过程主要分为如下几步:

  1. 初始化模块信息 CoreModule 在 coreModule 中有 module 的实现类,moduleJar,ModuleClassLoader,以及最重要的模块类转换器集合 sandboxClassFileTransformers,类转换器则是字节码增强的关键,代码增强章节会详细介绍

  2. 注入注解@Resource 资源,例如 ModuleEventWatcher 事件观察者。关于 ModuleEventWatcher 代码增强章节会详细介绍

  3. 通知生命周期中模块加载的对应实现(module 实现类可以同时实现 Module 接口以及沙箱模块生命周期接口 ModuleLifecyle 接口),在 ModuleLifecyle 接口对应的方法中,用户可以自定义实现业务逻辑。

  4. 激活模块并通知生命周期中模块激活的对应实现,只有被激活的模块才能响应模块的增强事件。

  5. 将模块唯一 id 和当前的 coreModule 实例存入模块列表,模块列表是一个全局的 ConcurrentHashMap。还记得在 Httpserver 启动时会初始化 ModuleHttpServlet,在 ModuleHttpServlet 接收请求时会从参数中解析出模块 id,从而获取到对应的 coreModule.

  6. 通知生命周期中模块加载完成的对应实现

 1private synchronized void load(final String uniqueId,
 2                               final Module module,
 3                               final File moduleJarFile,
 4                               final ModuleJarClassLoader moduleClassLoader) throws ModuleException {
 5    ......
 6    // 初始化模块信息
 7    final CoreModule coreModule = new CoreModule(uniqueId, moduleJarFile, moduleClassLoader, module);
 8    // 注入@Resource 资源
 9    injectResourceOnLoadIfNecessary(coreModule);
10    // 通知生命周期,模块加载
11    callAndFireModuleLifeCycle(coreModule, MODULE_LOAD);
12    // 设置为已经加载
13    coreModule.markLoaded(true);
14    // 如果模块标记了加载时自动激活,则需要在加载完成之后激活模块
15    markActiveOnLoadIfNecessary(coreModule);
16    // 注册到模块列表中
17    loadedModuleBOMap.put(uniqueId, coreModule);
18    // 通知生命周期,模块加载完成
19    callAndFireModuleLifeCycle(coreModule, MODULE_LOAD_COMPLETED);
20
21
22}

小结

在模块加载过程中,首先会通过遍历存储 module 的路径,然后通过自定义的模块 ClassLoader(ModuleJarClassLoader)以 SPI 的方式加载模块 jar 文件中的 Module 接口实现类,加载成功后会对模块进行前置检查,检查通过后则会对实现类注入 Resouce 资源并通知对应的生命周期事件,最后将 CoreModule 注册到模块列表中。

模块激活

模块在激活后就可以接收用户的自定义指令了,自定义指令其实就是通过@Command 注解在模块中自定义的 http 接口。在 http 接口对应的实现方法中可以做任何事情,例如触发代码增强。

sandbox.sh 的启动脚本中可以看到通过使用-a 参数激活指定模块,本质将会发送请求调用"sandbox-module-mgr/active",sandbox-module-mgr 是通用的沙箱系统管理模块

1# -a active module
2[[ -n ${OP_MODULE_ACTIVE} ]] &&
3  sandbox_curl_with_exit "sandbox-module-mgr/active" "&ids=${ARG_MODULE_ACTIVE}"

在 sandbox-mgr-module 项目的 ModuleMgrModule 类中,找到 active 方法的定义,因为 sandbox-mgr-module 属于系统模块,在上面介绍的模块加载中也会将系统模块进行加载,所以当使用-a 参数激活模块时也会通过 ModuleHttpServlet 将请求分发到 ModuleMgrModule 的 active 方法中进行处理。

在 active 中会解析参数,获取到要激活的模块 id,通过模块 id 找到对应的 module 实例(还记得在模块加载章节中介绍的当模块加载完成后会将模块的 coreModule 存储到 loadedModuleBOMap 中吧,这里的 search 本质就是从 map 中查找对应的 coreModule)

获取 module 实现类中的@Information 注解 id,判断是否已经激活,如果没激活则进行激活

 1@Command("active")
 2public void active(final Map<String, String> param,
 3                   final PrintWriter writer) throws ModuleException {
 4    int total = 0;
 5    final String idsStringPattern = getParamWithDefault(param, "ids", EMPTY);
 6    for (final Module module : search(idsStringPattern)) {
 7        final Information info = module.getClass().getAnnotation(Information.class);
 8        final boolean isActivated = moduleManager.isActivated(info.id());
 9        if (!isActivated) {
10            try {
11                moduleManager.active(info.id());
12                total++;
13            } catch (ModuleException me) {
14                logger.warn("active module[id={};] occur error={}.", me.getUniqueId(), me.getErrorCode(), me);
15            }// try
16        } else {
17            total++;
18        }
19    }// for
20    output(writer, "total %s module activated.", total);
21}

小结

激活的本质就是将 coreModule 实例的 isActivated 标记设置为 true,这样模块才可以接收到代码增强触发的事件

模块冻结

模块冻结是将已加载的模块的打上冻结标记,冻结后用户自定义的增强代码还存在,只不过沙箱不会处理用户自定义的代码增强逻辑。

在启动脚本中,冻结指令也是发往 sandbox-module-mgr 沙箱系统管理模块

1# -A frozen module
2[[ -n ${OP_MODULE_FROZEN} ]] &&
3  sandbox_curl_with_exit "sandbox-module-mgr/frozen" "&ids=${ARG_MODULE_FROZEN}

小结

模块冻结的处理过程和模块激活的处理过程基本相同,但是多了一步冻结事件处理器。

事件处理器在代码增强和事件处理章节中将会介绍。

模块卸载

模块卸载将会把模块整个从沙箱中清理掉,之前给该模块分配的所有资源都将会被回收,包括模块已经侦听事件的类都将会被移除掉侦听插桩,干净利落不留后遗症

在启动脚本中,冻结指令也是发往 sandbox-module-mgr 沙箱系统管理模块

1# -u unload module
2[[ -n ${OP_MODULE_UNLOAD} ]] &&
3  sandbox_curl_with_exit "sandbox-module-mgr/unload" "&action=unload&ids=${ARG_MODULE_UNLOAD}"

模块卸载的流程

  1. 尝试冻结模块,让事件处理器暂停不在执行用户自定义的增强逻辑。

  2. 通知生命周期模块卸载的对应实现

  3. 从模块注册表中删除对应的模块

  4. 标记卸载,isLoaded=true

  5. 释放资源,在模块加载流程中有一步是注入@Resource 资源,在注入 ModuleEventWatcher 资源时,实际上是构建了 ReleaseResource 对象,并实现了 release 方法。在 ModuleEventWatcher.delete 中会利用 Instrumentation 类库删除类增强转换器 SandboxClassFileTransformer,这样有新的类加载时将不会植入 spy 间谍类了。但是已加载的类总还是有间谍类的代码。通过 Instrumentation 类库重新渲染目标类字节码。

 1@Override
 2public void delete(final int watcherId,
 3                   final Progress progress) {
 4    final Set<Matcher> waitingRemoveMatcherSet = new LinkedHashSet<Matcher>();
 5    // 找出待删除的 SandboxClassFileTransformer
 6    final Iterator<SandboxClassFileTransformer> cftIt = coreModule.getSandboxClassFileTransformers().iterator();
 7    int cCnt = 0, mCnt = 0;
 8    while (cftIt.hasNext()) {
 9        final SandboxClassFileTransformer sandboxClassFileTransformer = cftIt.next();
10        if (watcherId == sandboxClassFileTransformer.getWatchId()) {
11            // 冻结所有关联代码增强
12            EventListenerHandler.getSingleton()
13                    .frozen(sandboxClassFileTransformer.getListenerId());
14            // 在 JVM 中移除掉命中的 ClassFileTransformer
15            inst.removeTransformer(sandboxClassFileTransformer);
16            // 计数
17            cCnt += sandboxClassFileTransformer.getAffectStatistic().cCnt();
18            mCnt += sandboxClassFileTransformer.getAffectStatistic().mCnt();
19            // 追加到待删除过滤器集合
20            waitingRemoveMatcherSet.add(sandboxClassFileTransformer.getMatcher());
21            // 清除掉该 SandboxClassFileTransformer
22            cftIt.remove();
23        }
24    }
25    // 查找需要删除后重新渲染的类集合
26    final List<Class<?>> waitingReTransformClasses = classDataSource.findForReTransform(
27            new GroupMatcher.Or(waitingRemoveMatcherSet.toArray(new Matcher[0]))
28    );
29    logger.info("watch={} in module={} found {} classes for delete.",
30            watcherId,
31            coreModule.getUniqueId(),
32            waitingReTransformClasses.size()
33    );
34    beginProgress(progress, waitingReTransformClasses.size());
35    try {
36        // 应用 JVM
37        reTransformClasses(watcherId, waitingReTransformClasses, progress);
38    } finally {
39        finishProgress(progress, cCnt, mCnt);
40    }
41}
  1. 关闭 ModuleJarClassLoader,可以将其加载的模块类全部从 jvm 清理掉。在关闭前会利用 SPI 找到模块中 ModuleJarUnloadSPI 接口的实现类,通知模块已经被卸载了可以做一些自定义的业务处理。
 1public void closeIfPossible() {
 2    onJarUnLoadCompleted();
 3    try {
 4
 5        // 如果是 JDK7+的版本, URLClassLoader 实现了 Closeable 接口,直接调用即可
 6        if (this instanceof Closeable) {
 7            logger.debug("JDK is 1.7+, use URLClassLoader[file={}].close()", moduleJarFile);
 8            try {
 9                ((Closeable)this).close();
10            } catch (Throwable cause) {
11                logger.warn("close ModuleJarClassLoader[file={}] failed. JDK7+", moduleJarFile, cause);
12            }
13            return;
14        }
15        .......
16      }
17 }

小结

模块卸载中首先会暂停事件的处理,然后利用 Instrumentation 卸载增强的字节码以及恢复原始字节码,最后关闭 ModuleClassLoader

代码增强

本章节将重点介绍如何利用 sandbox 对代码进行增强以及背后的实现原理

自定义模块

参考官方 demo 对代码增强前我们首先自定义模块 如下所示:

在自定义模块中需要实现 Module 接口,声明@Information 注解指定唯一 id,定义 ModuleEventWatcher 注解,声明自定义@Command 指令的处理方法。

 1@Information(id = "broken-clock-tinker")
 2public class BrokenClockTinkerModule implements Module {
 3
 4    @Resource
 5    private ModuleEventWatcher moduleEventWatcher;
 6
 7    @Command("repairCheckState")
 8    public void repairCheckState() {
 9
10        new EventWatchBuilder(moduleEventWatcher)
11                .onClass("com.taobao.demo.Clock")
12                .onBehavior("checkState")
13                .onWatch(new AdviceListener() {
14
15                    /**
16                     * 拦截{@code com.taobao.demo.Clock#checkState()}方法,当这个方法抛出异常时将会被
17                     * AdviceListener#afterThrowing()所拦截
18                     */
19                    @Override
20                    protected void afterThrowing(Advice advice) throws Throwable {
21
22                        // 在此,你可以通过 ProcessController 来改变原有方法的执行流程
23                        // 这里的代码意义是:改变原方法抛出异常的行为,变更为立即返回;void 返回值用 null 表示
24                        ProcessController.returnImmediately(null);
25                    }
26                });
27
28    }
29
30}

demo 中会利用 EventWatchBuilder 对 Clock 类中的 checkState 方法进行代码增强,当 checkState 方法抛出异常后会执行 afterThrowing 方法中的代码,将异常忽略掉直接返回。

watch

重点关注下 onWatch 方法,在经过层层调用会到达 DefaultModuleEventWatch 的 watch 方法,而 watch 方法则是真正触发代码增强的入口。

  1. 在 watch 方法中首先会创建 SandboxClassFileTransformer 沙箱类转换器,并将其注册到 CoreModule 中

  2. 利用 Instrumentation 类库的 addTransformer api 注册 SandboxClassFileTransformer 沙箱类转换器实例注册类转换器后,后面所有的类加载都会经过 SandboxClassFileTransformer

  3. 查找需要渲染的类集合,利用 matcher 对象查找当前 jvm 已加载的类matcher 对象则是 EventWatchBuilder 中指定的目标类和目标方法的包装对象,在 matcher 对象中指定了匹配规则。

  4. 将查找到的类进行渲染(代码增强),利用 Instrumentation 的 retransformClasses 方法重新对 JVM 已加载的类进行字节码转换。

 1private int watch(final Matcher matcher,
 2                  final EventListener listener,
 3                  final Progress progress,
 4                  final Event.Type... eventType) {
 5    final int watchId = watchIdSequencer.next();
 6    // 给对应的模块追加 ClassFileTransformer
 7    final SandboxClassFileTransformer sandClassFileTransformer = new SandboxClassFileTransformer(
 8            watchId, coreModule.getUniqueId(), matcher, listener, isEnableUnsafe, eventType, namespace);
 9
10    // 注册到 CoreModule 中
11    coreModule.getSandboxClassFileTransformers().add(sandClassFileTransformer);
12
13    //这里 addTransformer 后,接下来引起的类加载都会经过 sandClassFileTransformer
14    inst.addTransformer(sandClassFileTransformer, true);
15
16    // 查找需要渲染的类集合
17    final List<Class<?>> waitingReTransformClasses = classDataSource.findForReTransform(matcher);
18    logger.info("watch={} in module={} found {} classes for watch(ing).",
19            watchId,
20            coreModule.getUniqueId(),
21            waitingReTransformClasses.size()
22    );
23
24    int cCnt = 0, mCnt = 0;
25
26    // 进度通知启动
27    beginProgress(progress, waitingReTransformClasses.size());
28    try {
29
30        // 应用 JVM
31        reTransformClasses(watchId,waitingReTransformClasses, progress);
32
33        // 计数
34        cCnt += sandClassFileTransformer.getAffectStatistic().cCnt();
35        mCnt += sandClassFileTransformer.getAffectStatistic().mCnt();
36
37
38        // 激活增强类
39        if (coreModule.isActivated()) {
40            final int listenerId = sandClassFileTransformer.getListenerId();
41            EventListenerHandler.getSingleton()
42                    .active(listenerId, listener, eventType);
43        }
44
45    } finally {
46        finishProgress(progress, cCnt, mCnt);
47    }
48
49    return watchId;

字节码转换

在基础知识章节中介绍了对字节码转换主要是需要实现 ClassFileTransformer 接口,然后将实现类注册到 Instrumentation 中即可在合适的时机触发字节码转换,也可以通过 Instrumentation 的 api 例如 retransformClasses 手动触发。

在 SandboxClassFileTransformer#_transform 方法中主要就是当类加载或者重新定义时匹配是不是我们要增强的目标类和目标方法,如果是的话则利用沙箱的代码增强框架进行字节码生成,如果不是则忽略不处理。

 1private byte[] _transform(final ClassLoader loader,
 2                          final String internalClassName,
 3                          final Class<?> classBeingRedefined,
 4                          final byte[] srcByteCodeArray) {
 5    // 如果未开启 unsafe 开关,是不允许增强来自 BootStrapClassLoader 的类
 6    if (!isEnableUnsafe
 7            && null == loader) {
 8        logger.debug("transform ignore {}, class from bootstrap but unsafe.enable=false.", internalClassName);
 9        return null;
10    }
11
12    final ClassStructure classStructure = getClassStructure(loader, classBeingRedefined, srcByteCodeArray);
13    final MatchingResult matchingResult = new UnsupportedMatcher(loader, isEnableUnsafe).and(matcher).matching(classStructure);
14    final Set<String> behaviorSignCodes = matchingResult.getBehaviorSignCodes();
15
16    // 如果一个行为都没匹配上也不用继续了
17    if (!matchingResult.isMatched()) {
18        logger.debug("transform ignore {}, no behaviors matched in loader={}", internalClassName, loader);
19        return null;
20    }
21
22    // 开始进行类匹配
23    try {
24        final byte[] toByteCodeArray = new EventEnhancer().toByteCodeArray(
25                loader,
26                srcByteCodeArray,
27                behaviorSignCodes,
28                namespace,
29                listenerId,
30                eventTypeArray
31        );
32        if (srcByteCodeArray == toByteCodeArray) {
33            logger.debug("transform ignore {}, nothing changed in loader={}", internalClassName, loader);
34            return null;
35        }
36
37        // statistic affect
38        affectStatistic.statisticAffect(loader, internalClassName, behaviorSignCodes);
39
40        logger.info("transform {} finished, by module={} in loader={}", internalClassName, uniqueId, loader);
41        return toByteCodeArray;
42    } catch (Throwable cause) {
43        logger.warn("transform {} failed, by module={} in loader={}", internalClassName, uniqueId, loader, cause);
44        return null;
45    }
46}

在 EventEnhancer#toByteCodeArray 方法中利用 ASM 框架对代码进行增强,通过 ASM 的 ClassReader 读取目标类字节码,然后通过 ClassWriter 将转换后的字节码写入 dump 文件夹,并将转换后的字节码返回。

 1@Override
 2public byte[] toByteCodeArray(final ClassLoader targetClassLoader,
 3                              final byte[] byteCodeArray,
 4                              final Set<String> signCodes,
 5                              final String namespace,
 6                              final int listenerId,
 7                              final Event.Type[] eventTypeArray) {
 8    // 返回增强后字节码
 9    final ClassReader cr = new ClassReader(byteCodeArray);
10    final ClassWriter cw = createClassWriter(targetClassLoader, cr);
11    final int targetClassLoaderObjectID = ObjectIDs.instance.identity(targetClassLoader);
12    cr.accept(
13            new EventWeaver(
14                    ASM7, cw, namespace, listenerId,
15                    targetClassLoaderObjectID,
16                    cr.getClassName(),
17                    signCodes,
18                    eventTypeArray
19            ),
20            EXPAND_FRAMES
21    );
22    return dumpClassIfNecessary(cr.getClassName(), cw.toByteArray());
23}

转换过程则是通过继承 ClassVisitor 抽象类实现方法事件织入者 EventWeaver,在 EventWeaver 中会对目标方法进行匹配,如果匹配成功则织入 Spy 间谍类中的埋点方法。具体代码在 EventWeaver#visitMethod 方法中,内容太长就不粘贴了。

小结

在代码增强流程中利用 Instrumentation 类库的 addTransformer api 注册 SandboxClassFileTransformer 沙箱类转换器实例,当类加载、类重新定义、类转换的时候会触发 SandboxClassFileTransformer 的 transformer 方法进行字节码转换,本质是使用 ASM 框架在对目标方法和类进行转换时将 SPY 间谍类中的方法织入到目标方法中,最后输出增强后的字节码 byte[]

事件处理

在代码增强章节中介绍了是如何将间谍类织入到目标方法中的生成的字节码会生成类重新应用到 jvm 中,那么增强后的方法是如何执行到用户自定义的增强逻辑中呢?

拿 spyMethodOnBefore 举例,假如我们对目标方法监听的是 Before 事件,因为增强后的代码在目标方法执行前会执行 spyMethodOnBefore 方法,在 spyMethodOnBefore 中会找到模块对应的 SpyHandler 间谍处理器实例 EventListenHandler。

 1public static Ret spyMethodOnBefore(final Object[] argumentArray,
 2                                    final String namespace,
 3                                    final int listenerId,
 4                                    final int targetClassLoaderObjectID,
 5                                    final String javaClassName,
 6                                    final String javaMethodName,
 7                                    final String javaMethodDesc,
 8                                    final Object target) throws Throwable {
 9    final Thread thread = Thread.currentThread();
10    if (selfCallBarrier.isEnter(thread)) {
11        return Ret.RET_NONE;
12    }
13    final SelfCallBarrier.Node node = selfCallBarrier.enter(thread);
14    try {
15        final SpyHandler spyHandler = namespaceSpyHandlerMap.get(namespace);
16        if (null == spyHandler) {
17            return Ret.RET_NONE;
18        }
19        return spyHandler.handleOnBefore(
20                listenerId, targetClassLoaderObjectID, argumentArray,
21                javaClassName,
22                javaMethodName,
23                javaMethodDesc,
24                target
25        );
26    } catch (Throwable cause) {
27        handleException(cause);
28        return Ret.RET_NONE;
29    } finally {
30        selfCallBarrier.exit(thread, node);
31    }
32}

EventListenHandler

EventListenHandler 是 SpyHandler 接口的实现类,在 handleOnBefore 中将会通过 listenerID 找到事件处理器 processor,在事件处理器中有用户自定义的 listenr 实例,这样就可以回调 listener 的 OnEvent 方法,执行真正用户在自定义模块中编写的代码了。

 1@Override
 2public Spy.Ret handleOnBefore(int listenerId, int targetClassLoaderObjectID, Object[] argumentArray, String javaClassName, String javaMethodName, String javaMethodDesc, Object target) throws Throwable {
 3
 4    // 在守护区内产生的事件不需要响应
 5    if (SandboxProtector.instance.isInProtecting()) {
 6        logger.debug("listener={} is in protecting, ignore processing before-event", listenerId);
 7        return newInstanceForNone();
 8    }
 9
10    // 获取事件处理器
11    final EventProcessor processor = mappingOfEventProcessor.get(listenerId);
12
13    // 如果尚未注册,则直接返回,不做任何处理
14    if (null == processor) {
15        logger.debug("listener={} is not activated, ignore processing before-event.", listenerId);
16        return newInstanceForNone();
17    }
18
19    // 获取调用跟踪信息
20    final EventProcessor.Process process = processor.processRef.get();
21
22    // 如果当前处理 ID 被忽略,则立即返回
23    if (process.isIgnoreProcess()) {
24        logger.debug("listener={} is marked ignore process!", listenerId);
25        return newInstanceForNone();
26    }
27
28    // 调用 ID
29    final int invokeId = invokeIdSequencer.getAndIncrement();
30    process.pushInvokeId(invokeId);
31
32    // 调用过程 ID
33    final int processId = process.getProcessId();
34
35    final ClassLoader javaClassLoader = ObjectIDs.instance.getObject(targetClassLoaderObjectID);
36    //放置业务类加载器
37    BusinessClassLoaderHolder.setBussinessClassLoader(javaClassLoader);
38    final BeforeEvent event = process.getEventFactory().makeBeforeEvent(
39            processId,
40            invokeId,
41            javaClassLoader,
42            javaClassName,
43            javaMethodName,
44            javaMethodDesc,
45            target,
46            argumentArray
47    );
48    try {
49        return handleEvent(listenerId, processId, invokeId, event, processor);
50    } finally {
51        process.getEventFactory().returnEvent(event);
52    }
53}

EventProcessor

EventProcessor 事件处理器中包含了用户自定义的 listener 以及 listenerId,在执行增强代码时会记录调用的堆栈。

在代码增强 watch 章节中,watch 方法执行的最后会激活增强类,在这里就是主要是将 listenerId 当作 key,将 EventProcessor 当作 value 存入到 map 中。这样当接收到事件时则可以根据 listenerId 查找到对应的事件处理器

 1public void active(final int listenerId,
 2                   final EventListener listener,
 3                   final Event.Type[] eventTypes) {
 4    mappingOfEventProcessor.put(listenerId, new EventProcessor(listenerId, listener, eventTypes));
 5    logger.info("activated listener[id={};target={};] event={}",
 6            listenerId,
 7            listener,
 8            join(eventTypes, ",")
 9    );
10}

listenerId

listenerId 可以理解为自定义模块每对一个目标方法进行 watch 都会生成一个全局唯一的 id,通过这个 id 来绑定对应的事件处理器

listener

事件处理器最终的目的就是回调 listener 中的方法,listener 是我们在自定义模块中对目标方法进行 watch 时所传递的参数,在 listener 的 onEvent 或者 AdviceListener 的各个埋点方法中可以任意实现要对目标方法增强的逻辑。

小结

在事件处理流程中通过 SpyHandler 的实现类 EventListenHandler 进行事件的分发,匹配到当前事件对应的事件处理器 EventProcessor,在 EventProcessor 中会真正的回调用户实现的 listener,从而完成真正的用户自定义代码增强逻辑。

总结

JVM-SANDBOX 是一种 JVM 的非侵入式运行期 AOP 解决方案,通过它我们可以很轻松的开发出很多有趣的项目如录制回放、故障模拟、动态日志、行链路获取等等,在本文中通过源码分析介绍了 JVM-SANDBOX 实现细节,了解到它的核心实现。

回顾下关键内容:

  1. JVM-SANDBOX 基于 JVM TI(JVM 工具接口)实现 agent 对目标 jvm 进行挂载,利用 Instrumentation 类库注册自定义的类转换器(SandboxClassFileTransformer),在类转换器中使用 ASM 框架对目标类的方法织入 Spy 间谍类中的埋点方法,从而实现字节码增强功能。

  2. JVM-SANDBOX 利用 ClassLoader 双亲委派模型,自定义 SandboxClassLoader 加载沙箱内部类(spy-core),使用自定义 ModulerJarClassLoader 和 SPI 机制对自定义模块进行加载,从而实现类隔离。

  3. 可插拔以及多租户的特性更多的是偏于 JVM-SANDBOX 内部的业务逻辑,在模块管理章节模块的加载和卸载中有详细描述。

最后在解释下为什么要对 jvm-sandbox 进行源码分析,是因为在工作中以及开源社区中都有使用到 jvm-sandbox 也发现了一些 bug 例如多次模块加载和卸载会导致 metaspace oom等。后面有精力也会对 bug 以及解决过程进行分享。

作者介绍

Github 账号:binbin0325,公众号:柠檬汁CodeSentinel-Golang Committer 、ChaosBlade Committer 、 Nacos PMC 、Apache Dubbo-Go Committer。目前主要关注于混沌工程、中间件以及云原生方向。