【alibaba/jvm-sandbox#07】事件监听的关键实现

简介: 代码中的 new 一个 Listener,sandbox 内部就创建并注册一个与之对应的事件处理器。Spy 的静态方法中将方法事件交给了事件处理器经过一些内部处理,最终回调 listener 的方法。
我是石页兄,朋友不因远而疏,高山不隔友谊情;偶遇美羊羊,我们互相鼓励

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

一、alibaba/jvm-sandbox 概述

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

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

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

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

本篇的目标是

  • 理解事件的种类,调用顺序
  • 理解事件链路 事件与通知的转换 以及方法链和通知栈的关系。
  • 理解流程跳转的控制-通过自定义异常来实现流程的跳转。
  • 理解流程跳转后,通知的有效性。

二、事件处理器

1)概述

代码中的 new 一个 Listener,sandbox 内部就创建并注册一个与之对应的事件处理器。
Spy 的静态方法中将方法事件交给了事件处理器经过一些内部处理,最终回调 listener 的方法。

2)注册事件处理器

注册过程在com.alibaba.jvm.sandbox.core.enhance.weaver.EventListenerHandlers#active中。
先说明listenerId是什么:listener对象 放入对象池后,得到 listenerId ,程序中不需要操作listener实例,但是上下文中又需要用到listener的引用的地方都用listenerId

public void active(final int listenerId,
        final EventListener listener,
        final Event.Type[] eventTypes) {
        //对于一个listener对象,创建一个事件处理器并与之关联
        //mappingOfEventProcessor 容器中通过listenerId与EventProcessor 建立关联。
        //sandbox源码中注入的方法参数里 有 listenerid,mappingOfEventProcessor通过listenerId能找到事件处理器
        mappingOfEventProcessor.put(listenerId, new EventProcessor(listenerId, listener, eventTypes));
        logger.info("activated listener[id={};target={};] event={}",
                listenerId,
                listener,
                join(eventTypes, ",")
        );
    }

3)获取事件处理器

mappingOfEventProcessor.get(listenerId);

4) 事件处理器的功能

  1. 提供方法对应的事件对象
  2. 维护事件对应的方法之间的调用关系
  3. 跳转控制

三、处理单元

处理单元来负责事件处理器的 2 个功能:

  1. 提供方法对应的事件对象
  2. 维护事件对应的方法之间的调用关系是由

3.1 什么是处理单元

一个监听的方法链路在一个线程中对应着一个处理单元com.alibaba.jvm.sandbox.core.enhance.weaver.EventProcessor.Process。 方法肯定是在多个线程中执行,事件处理器通过 ThreadLocal的方法每个线程创建了一个处理单元Process的实例,这样处理单元之间就是线程隔离的,其内部创建的对象就是线程安全的。
两个基本的功能

  1. 提供方法对应的事件对象
    EventProcessor.ProcessSingleEventFactory 事件工厂负责创建各种事件对象,比如 makeBeforeEvent 就创建一个 before 事件对象;每个事件都是一个单例,下一个事件到来时,其内部通过 unsafe 方法来给对象的属性变更值,避免了每次创建新对象,减少新生代 GC 的工作量。
  2. .weaver.EventProcessor.Processstack 负责维护方法之间的调用关系,跟 JVM 的操作类似(JVM 通过栈结构来组织方法的调用关系,调用方法时方法栈帧压栈,方法执行结束后就方法栈帧出栈);栈的深度取决于监听的方法链路的深度。
    在处理链路的时候有两个 id 很关键,processIdinvokeId,每个方法的before事件即方法的入口处对应一个invokeId,对于方法链路来说,最外层的方法是的 invokeId 就标识了整个调用链路,即整个执行过程,存储为processId,内部方法的processId就是最外层方法的InvokeId

xxxEvent 对象是线程复用的,这样在用户使用时就有很多约束,所以处理单元所提供的方法事件,仅在 sandbox 内部使用,在用户视角的监听器里回调方法中就被封装成了Advice通知对象com.alibaba.jvm.sandbox.api.listener.ext.Advice
方法之间的调用栈关系,转换为com.alibaba.jvm.sandbox.api.listener.ext.AdviceAdapterListener.OpStack同时也在 Advice 上体现,通过 top 属性来指向栈底 Advice,通过 parent 属性指向上一个 Advice(朝栈底方向)。Advice 更重要的是提供了打标marks和挂载数据attatchment的功能;这样在通知栈中就增加了交互通信的手段。类似于传话的功能。

Advice 压栈和出栈如下:

image.png

四、事件的处理

4.1 report 方法的三个事件

  1. BEFORE

    BEFORE事件中,sandbox 内部通过makeBeforeEvent创建beforeEvent对象,根据这个Event创建了Advice后压栈;后续RETURN/THROWS中使用;所以一个方法内只在入口处创建一个advice对象。

  2. RETURN

    sandbox 内部通过makeReturnEvent创建returnEvent对象。通过invokeId,从栈顶拿到Advice对象,将事件包裹的函数返回值 放置到 advice中。

  3. THROWS

    sandbox 内部通过makeThrowsEvent创建throwsEvent对象。通过invokeId,从栈顶拿到Advice对象,将事件包裹的函数返回值 放置到 advice中。

4.2 report 方法中的 call 事件

  1. CALL_BEFORE

    xxxAdvice对象在一次方法的调用内部只有一个,在`before`事件中创建。其他的所有事件中复用此通知对象。
    

    call_before事件中也是通过invokeId,从栈顶拿到Advice对象,复用此advice中;call事件中创建了CallTarget 对象,记录了被call方法的一些信息;
    作为attach附加到 xxxAdvice 上。
    wrapAdvice.attach(target = new CallTarget( cbEvent.lineNumber,//代码行号 toJavaClassName(cbEvent.owner),//调用者的类名 cbEvent.name,//被调方法名 cbEvent.desc//被调方法的参数 ));
    之后调用 adviceListener.beforeCall

  2. CALL_RETURN

    也是通过`invokeId`,从栈顶拿到`Advice`对象,复用此`Advice`中;
    ```
    CallTarget target = wrapAdvice.attachment();
    ```
    从`target`中拿到方法名和调用者类名信息;之后调用`adviceListener.afterCallReturning`和
    

    adviceListener.afterCall

  3. CALL_THROWS

    也是通过`invokeId`,从栈顶拿到`Advice`对象,复用此`advice`中;
    ```
    CallTarget target = wrapAdvice.attachment();
    ```
    从`target`中拿到方法名和调用者类名信息;之后调用`adviceListener.afterCallThrowing`和
    

    adviceListener.afterCall

4.3 report 方法中的 LINE 事件

也是通过invokeId,从栈顶拿到Advice对象,复用此advice中;之后调用adviceListener.beforeLine

4.4 函数的嵌套调用,即多层调用的情况

image.png

  1. 子函数的invokeId 在自增变化
  2. adviceparenttop 能查找到整个通知栈的advice
  3. 再通过advice上的attachment可以在上下游之间传递信息。不用再额外的设计和引入其他的载体容器。

五、最后说一句

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

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

相关文章
|
缓存 Java Shell
【alibaba/jvm-sandbox#06】事件监听的关键设计
介绍jvm-sandbox中事件机制的设计,通过用事件Id与事件之间的映射关系进行解耦,通过Spy类的静态方法携带事件Id进行核心事件逻辑的埋点注入
387 0
|
Java jvm-sandbox 容器
【alibaba/jvm-sandbox#05】沙箱事件详解
alibaba/jvm-sandbox设计了完善且复杂的沙箱事件,用于实现事件探测和流程控制机制。但不建议对于同一个类、同一个方法多次增强
410 0
|
10月前
|
运维 监控 安全
花20天刷完Alibaba JVM笔记去面阿里,却意外拿到京东Offer?
Java虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机,是Java语言的运行环境,它是Java 最具吸引力的特性之一。
|
Java jvm-sandbox 容器
【alibaba/jvm-sandbox#04】通过EventWatchBuilder修改字节码
jvm-sandbox给使用者提供EventWatchBuilder对代码插桩进行了封装;把底层复杂的字节码操控隐藏起来,让使用者仅关注操控什么类、什么方法、方法中的什么逻辑。
279 0
|
Java jvm-sandbox 开发者
【alibaba/jvm-sandbox#03】JavaAgent 修改字节码的机制
开发者一般采用建立一个 Agent 的方式来使用 JVMTI,使用 JVMTI 一个基本的方式就是设置回调函数,在回调函数体内,可以 获取各种各样的VM级信息,甚至控制VM行为,如类加载时修改类
271 0
|
Java jvm-sandbox 容器
【alibaba/jvm-sandbox#02】通过无侵入AOP实现行为注入和流控
任何一个 Java 方法的调用都可以分解为`BEFORE`、`RETURN`和`THROWS`三个环节,由此在三个环节上引申出对应环节的事件探测和流程控制机制。
244 0
|
Java jvm-sandbox Windows
【alibaba/jvm-sandbox#01】debug源码的技巧
alibaba/jvm-sandbox是 一种JVM的非侵入式运行期 AOP 解决方案。沙箱容器提供 1. 动态增强类你所指定的类,获取你想要的参数和行信息甚至改变方法执行 2. 动态可插拔容器框架
349 0
|
4天前
|
存储 缓存 算法
深入浅出JVM(二)之运行时数据区和内存溢出异常
深入浅出JVM(二)之运行时数据区和内存溢出异常
|
13天前
|
存储 Java
深入理解Java虚拟机:JVM内存模型
【4月更文挑战第30天】本文将详细解析Java虚拟机(JVM)的内存模型,包括堆、栈、方法区等部分,并探讨它们在Java程序运行过程中的作用。通过对JVM内存模型的深入理解,可以帮助我们更好地编写高效的Java代码,避免内存溢出等问题。
|
2天前
|
Java Linux Arthas
linux上如何排查JVM内存过高?
linux上如何排查JVM内存过高?
16 0