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

一、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 的方法--
spyMethodOnBeforebeforeCall---
spyMethodOnReturnafterReturningafter
spyMethodOnThrowsafterThrowingafter
spyMethodOnCallBeforebeforeCall---
spyMethodOnCallReturnafterCallReturningafterCall
spyMethodOnCallThrowsafterCallThrowingafterCall

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;

五、最后说一句

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值