【alibaba/jvm-sandbox#06】事件监听的关键设计

简介: 介绍jvm-sandbox中事件机制的设计,通过用事件Id与事件之间的映射关系进行解耦,通过Spy类的静态方法携带事件Id进行核心事件逻辑的埋点注入
我是石页兄,朋友不因远而疏,高山不隔友谊情;偶遇美羊羊,我们互相鼓励

欢迎关注微信公众号「架构染色」交流和学习

一、alibaba/jvm-sandbox 概述

alibaba/jvm-sandbox 是 JVM 沙箱容器,一种 JVM 的非侵入式运行期 AOP 解决方案。沙箱容器提供

  1. 动态增强类你所指定的类,获取你想要的参数和行信息甚至改变方法执行
  2. 动态可插拔容器框架

在其能力至上构建的上层应用有:

《【alibaba/jvm-sandbox#02】通过无侵入 AOP 实现行为注入和流控》 介绍了 JVM-SANDBOX 属于基于 Instrumentation 的动态编织类的 AOP 框架,通过精心构造了字节码增强逻辑,使得沙箱的模块能在不违反 JDK 约束情况下实现对目标应用方法的无侵入运行时 AOP 拦截。实现行为注入和流控。

本篇的目标是加深对其沙箱事件机制的理解

二、导读

前置条件,阅读《alibaba/jvm-sandbox#05】沙箱事件详解》《【alibaba/jvm-sandbox#04】通过 EventWatchBuilder 修改字节码》理解 注入式增强

  • 我们的 watch 方法 会增强代码(注入钩子方法)
  • 钩子方法名以及其逻辑位置标示了其所处理的事件
  • wath 方法中的 listener 实例跟这些钩子方法关联,来实现事件监听
  • 钩子方法中的 listenerId,标识了此方法对应的监听器
  • 钩子方法中的 namespace 标识了 此方法对应的 sandbox 实例

三、环境准备

在原版闹钟代码 中 增加了sleepSend(2000) 这个方法,
希望后续能监看方法调用的埋点,即 callxxx 相关的埋点代码,捕捉和修改方法参数。

package com.taobao.demo;

/**
 * 报时的钟
 */
public class Clock {

    private final java.text.SimpleDateFormat clockDateFormat
            = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    final void checkState() {
        throw new IllegalStateException("STATE ERROR!");
    }

    final void sleepSecond(int millis) throws InterruptedException {
        Thread.sleep(millis);
    }

    final java.util.Date now() {
        return new java.util.Date();
    }

    final String report() throws InterruptedException {

        sleepSecond(2000);

        checkState();
        //return "1";
        return clockDateFormat.format(now());
    }

    final void loopReport() throws InterruptedException {
        while (true) {
            try {
                System.out.println(report());
            } catch (Throwable cause) {
                cause.printStackTrace();
            }
            Thread.sleep(1000);
        }
    }

    public static void main(String... args) throws InterruptedException {
        new Clock().loopReport();
    }

}

基于官网示例实验一下增强,比官方实例多了几个事件回调,并增加 callxxx 系列的方法监视。

package com.alibaba.jvm.sandbox.demo;

import com.alibaba.jvm.sandbox.api.Information;
import com.alibaba.jvm.sandbox.api.Module;
import com.alibaba.jvm.sandbox.api.ProcessController;
import com.alibaba.jvm.sandbox.api.annotation.Command;
import com.alibaba.jvm.sandbox.api.http.printer.ConcurrentLinkedQueuePrinter;
import com.alibaba.jvm.sandbox.api.http.printer.Printer;
import com.alibaba.jvm.sandbox.api.listener.ext.Advice;
import com.alibaba.jvm.sandbox.api.listener.ext.AdviceListener;
import com.alibaba.jvm.sandbox.api.listener.ext.EventWatchBuilder;
import com.alibaba.jvm.sandbox.api.resource.ModuleEventWatcher;
import org.kohsuke.MetaInfServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;

@MetaInfServices(Module.class)
@Information(id = "broken-clock-tinker")
public class BrokenClockTinkerModule implements Module {

    private final Logger lifeCLogger = LoggerFactory.getLogger("broken-clock-tinker");

    @Resource
    private ModuleEventWatcher moduleEventWatcher;

    //final Printer printer = new ConcurrentLinkedQueuePrinter(writer);

    @Command("repairCheckState")
    public void repairCheckState() {

        new EventWatchBuilder(moduleEventWatcher)
                .onClass("com.taobao.demo.Clock")
                .onBehavior("report")
                .onWatching()
                .withCall()
                .onWatch(new AdviceListener() {

                    @Override
                    protected void beforeCall(Advice advice, int callLineNum, String callJavaClassName, String callJavaMethodName, String callJavaMethodDesc) {
                        lifeCLogger.info("beforeCall");
                        super.beforeCall(advice, callLineNum, callJavaClassName, callJavaMethodName, callJavaMethodDesc);
                    }

                    @Override
                    protected void afterCall(Advice advice, int callLineNum, String callJavaClassName, String callJavaMethodName, String callJavaMethodDesc, String callThrowJavaClassName) {
                        lifeCLogger.info("afterCall");
                        super.afterCall(advice, callLineNum, callJavaClassName, callJavaMethodName, callJavaMethodDesc, callThrowJavaClassName);
                    }

                    @Override
                    protected void afterCallReturning(Advice advice, int callLineNum, String callJavaClassName, String callJavaMethodName, String callJavaMethodDesc) {
                        lifeCLogger.info("afterCallReturning");
                        super.afterCallReturning(advice, callLineNum, callJavaClassName, callJavaMethodName, callJavaMethodDesc);
                    }

                    @Override
                    protected void afterCallThrowing(Advice advice, int callLineNum, String callJavaClassName, String callJavaMethodName, String callJavaMethodDesc, String callThrowJavaClassName) {
                        lifeCLogger.info("afterCallThrowing");
                        super.afterCallThrowing(advice, callLineNum, callJavaClassName, callJavaMethodName, callJavaMethodDesc, callThrowJavaClassName);
                    }

                    @Override
                    protected void after(Advice advice) throws Throwable {
                        lifeCLogger.info("after");
                        super.after(advice);
                    }

                    @Override
                    protected void afterReturning(Advice advice) throws Throwable {
                        lifeCLogger.info(String.valueOf("after return  value : " + advice.getReturnObj()));
                        super.afterReturning(advice);

                    }
                    /**
                     * 拦截{@code com.taobao.demo.Clock#checkState()}方法,当这个方法抛出异常时将会被
                     * AdviceListener#afterThrowing()所拦截
                     */
                    @Override
                    protected void afterThrowing(Advice advice) throws Throwable {

                        // 在此,你可以通过ProcessController来改变原有方法的执行流程
                        // 这里的代码意义是:改变原方法抛出异常的行为,变更为立即返回;void返回值用null表示
                        ProcessController.returnImmediately(null);
                    }
                });

    }

}

listener中回调的方法在源码中都有体现。
加上.onWatching() .withCall() 之后,增强里才出现了 callxxx 系列的代码;增强后 注入了许多事件代码,如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.taobao.demo;

import java.com.alibaba.jvm.sandbox.spy.Spy;
import java.com.alibaba.jvm.sandbox.spy.Spy.Ret;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Clock {
    private final SimpleDateFormat clockDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public Clock() {
    }

    final void checkState() {
        throw new IllegalStateException("STATE ERROR!");
    }

    final void sleepSecond(int millis) throws InterruptedException {
        Thread.sleep((long)millis);
    }

    final Date now() {
        return new Date();
    }

    final String report() throws InterruptedException {
        boolean var10000 = true;
        Ret var10002 = Spy.spyMethodOnBefore(new Object[0], "default", 1001, 1002, "com.taobao.demo.Clock", "report", "()Ljava/lang/String;", this);
        int var10001 = var10002.state;
        if (var10001 == 1) {
            return (String)var10002.respond;
        } else if (var10001 != 2) {
            boolean var7;
            Ret var8;
            int var10;
            try {
                var10000 = true;
                Clock var6 = this;
                short var9 = 2000;
                boolean var11 = true;
                Spy.spyMethodOnCallBefore(42, "com.taobao.demo.Clock", "sleepSecond", "(I)V", "default", 1001);
                var11 = true;

                try {
                    var6.sleepSecond(var9);
                } catch (Throwable var4) {
                    var7 = true;
                    Spy.spyMethodOnCallThrows(var4.getClass().getName(), "default", 1001);
                    var7 = true;
                    throw var4;
                }

                var10000 = true;
                Spy.spyMethodOnCallReturn("default", 1001);
                var10000 = true;
                var6 = this;
                var7 = true;
                Spy.spyMethodOnCallBefore(44, "com.taobao.demo.Clock", "checkState", "()V", "default", 1001);
                var7 = true;

                try {
                    var6.checkState();
                } catch (Throwable var3) {
                    var7 = true;
                    Spy.spyMethodOnCallThrows(var3.getClass().getName(), "default", 1001);
                    var7 = true;
                    throw var3;
                }

                var10000 = true;
                Spy.spyMethodOnCallReturn("default", 1001);
                var10000 = true;
                SimpleDateFormat var12 = this.clockDateFormat;
                Clock var14 = this;
                var11 = true;
                Spy.spyMethodOnCallBefore(46, "com.taobao.demo.Clock", "now", "()Ljava/util/Date;", "default", 1001);
                var11 = true;

                Date var15;
                try {
                    var15 = var14.now();
                } catch (Throwable var2) {
                    var7 = true;
                    Spy.spyMethodOnCallThrows(var2.getClass().getName(), "default", 1001);
                    var7 = true;
                    throw var2;
                }

                var11 = true;
                Spy.spyMethodOnCallReturn("default", 1001);
                var11 = true;
                var11 = true;
                Spy.spyMethodOnCallBefore(46, "java.text.SimpleDateFormat", "format", "(Ljava/util/Date;)Ljava/lang/String;", "default", 1001);
                var11 = true;

                String var13;
                try {
                    var13 = var12.format(var15);
                } catch (Throwable var1) {
                    var7 = true;
                    Spy.spyMethodOnCallThrows(var1.getClass().getName(), "default", 1001);
                    var7 = true;
                    throw var1;
                }

                var7 = true;
                Spy.spyMethodOnCallReturn("default", 1001);
                var7 = true;
                var7 = true;
                var8 = Spy.spyMethodOnReturn(var13, "default", 1001);
                var10 = var8.state;
                if (var10 != 1) {
                    if (var10 != 2) {
                        var7 = true;
                        return var13;
                    } else {
                        throw (Throwable)var8.respond;
                    }
                } else {
                    return (String)var8.respond;
                }
            } catch (Throwable var5) {
                var7 = true;
                var8 = Spy.spyMethodOnThrows(var5, "default", 1001);
                var10 = var8.state;
                if (var10 != 1) {
                    if (var10 != 2) {
                        var7 = true;
                        throw var5;
                    } else {
                        throw (Throwable)var8.respond;
                    }
                } else {
                    return (String)var8.respond;
                }
            }
        } else {
            throw (Throwable)var10002.respond;
        }
    }

    final void loopReport() throws InterruptedException {
        while(true) {
            try {
                System.out.println(this.report());
            } catch (Throwable var2) {
                var2.printStackTrace();
            }

            Thread.sleep(1000L);
        }
    }

    public static void main(String... args) throws InterruptedException {
        (new Clock()).loopReport();
    }
}

四、事件与 listener 的关系

4.1 理解 源码中注入的方法 、 Spy 类 、 listener 类 三者关系

  1. 源码中注入的方法中 Spy.spyMethodxxx 方法,就是 Spy 类的静态方法
  2. Spy 类中的方法,最终通过com.alibaba.jvm.sandbox.core.enhance.weaver.EventListenerHandlers,根据参数 listenerId 获取对应的 Listener 实例,再根据事件类型调用 Listener 实例对应的方法。

4.2 Spy 与 AdviceListener 方法的对应关系

埋点方法 回调方法 附加调用
Spy 类的静态方法 AdviceListener 的方法 --
spyMethodOnBefore beforeCall ---
spyMethodOnReturn afterReturning after
spyMethodOnThrows afterThrowing after
spyMethodOnCallBefore beforeCall ---
spyMethodOnCallReturn afterCallReturning afterCall
spyMethodOnCallThrows afterCallThrowing afterCall

afterReturningafterThrowing 执行之后还会调用 after
afterCallReturningafterCallThrowing 执行之后还会调用 afterCall
afterafterCall方法是 finally 里调用的;如下:

try {
    adviceListener.afterCallReturning( ... );
} finally {
    adviceListener.afterCall( ... );
}

一些afterxxx中的公共功能可以放到afterafterCall 方法中。

4.3 增强中的 spyMethodxxx 方法 如何 与 listener 对应起来,实现回调?

spyMethodxxx 代码中可以看到普遍存在的两个参数 "default", 1001,其中 1001 是我们的 listener 实例的标识 id,通过此 id 找到 listener 实例对象,进而调用 listener 的回调方法。

Spy.spyMethodOnCallThrows(var4.getClass().getName(), "default", 1001);

4.4 如果有多个 listener?

每个 listener 都会对应的注入 SpyMethodxxx 方法。与 listener 对应的方法的参数中都是其对应的 listener 的 Id;比如:

  • listener1 产生的注入中,方法里的 listenerId 是 1001

    Spy.spyMethodOnBefore(var4.getClass().getName(), "default", 1001);
  • listener2 产生的注入中,方法里 listenerId 是 1002

    Spy.spyMethodOnBefore(var1.getClass().getName(), "default", 1002);

实例放到实例缓存后,拿到一个 id

this.listenerId = ObjectIDs.instance.identity(eventListener);

4.5 如果是有多个 module 呢?

寻找跟 module 相关的线索,看不到 module 相关的东西;
继续思考每个 module 是一个类加载器,类加载器不同,即模块不同。想不到什么需求要 module 的标识。
module 的类加载器,通过 module 中的类的 getClassLoader 就能获取。

spyMethodOnBefore 方法中的 targetClassLoaderObjectID 是模块的类加载器吗?
不是的,从变量名称和源码看 targetClassLoaderObjectID 都是目标类(待增强的类)的定义类加载器。

public static Ret spyMethodOnBefore(final Object[] argumentArray,
                                        final String namespace,
                                        final int listenerId,
                                        final int targetClassLoaderObjectID,
                                        final String javaClassName,
                                        final String javaMethodName,
                                        final String javaMethodDesc,
                                        final Object target) throws Throwable {

4.6 多个 sandbox 实例

shell 命令中 -n 参数来指定 namespace,一个 namespace 来标识一个 sandbox;如果 shell 命令中未指定 则默认值是 default;

五、最后说一句

我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。

欢迎点击链接扫马儿关注、交流。

相关文章
|
Java jvm-sandbox 容器
【alibaba/jvm-sandbox#05】沙箱事件详解
alibaba/jvm-sandbox设计了完善且复杂的沙箱事件,用于实现事件探测和流程控制机制。但不建议对于同一个类、同一个方法多次增强
558 0
花20天刷完Alibaba JVM笔记去面阿里,却意外拿到京东Offer?
Java虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机,是Java语言的运行环境,它是Java 最具吸引力的特性之一。
|
存储 安全 Java
【alibaba/jvm-sandbox#07】事件监听的关键实现
代码中的 new 一个 Listener,sandbox 内部就创建并注册一个与之对应的事件处理器。Spy 的静态方法中将方法事件交给了事件处理器经过一些内部处理,最终回调 listener 的方法。
477 0
|
Java jvm-sandbox 开发者
【alibaba/jvm-sandbox#03】JavaAgent 修改字节码的机制
开发者一般采用建立一个 Agent 的方式来使用 JVMTI,使用 JVMTI 一个基本的方式就是设置回调函数,在回调函数体内,可以 获取各种各样的VM级信息,甚至控制VM行为,如类加载时修改类
400 0
|
Java jvm-sandbox 容器
【alibaba/jvm-sandbox#02】通过无侵入AOP实现行为注入和流控
任何一个 Java 方法的调用都可以分解为`BEFORE`、`RETURN`和`THROWS`三个环节,由此在三个环节上引申出对应环节的事件探测和流程控制机制。
351 0
|
Java jvm-sandbox Windows
【alibaba/jvm-sandbox#01】debug源码的技巧
alibaba/jvm-sandbox是 一种JVM的非侵入式运行期 AOP 解决方案。沙箱容器提供 1. 动态增强类你所指定的类,获取你想要的参数和行信息甚至改变方法执行 2. 动态可插拔容器框架
451 0
|
Java jvm-sandbox 容器
【alibaba/jvm-sandbox#04】通过EventWatchBuilder修改字节码
jvm-sandbox给使用者提供EventWatchBuilder对代码插桩进行了封装;把底层复杂的字节码操控隐藏起来,让使用者仅关注操控什么类、什么方法、方法中的什么逻辑。
351 0
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
10天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
8天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
8 1