我是石页兄,朋友不因远而疏,高山不隔友谊情;偶遇美羊羊,我们互相鼓励欢迎关注微信公众号「架构染色」交流和学习
一、alibaba/jvm-sandbox 概述
alibaba/jvm-sandbox 是 JVM 沙箱容器,一种 JVM 的非侵入式运行期 AOP 解决方案。沙箱容器提供
- 动态增强类你所指定的类,获取你想要的参数和行信息甚至改变方法执行
- 动态可插拔容器框架
在其能力至上构建的上层应用有:
-
- 其chaosblade-io/chaosblade-exec-jvm 是一个基于 jvm-sandbox 的 chaosblade 执行器,用于通过增强类在 Java 应用程序上进行混沌实验。
-
- 基于 JVM-Sandbox 的录制/回放通用解决方案
在《【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 类 三者关系
- 源码中注入的方法中
Spy.spyMethodxxx
方法,就是 Spy 类的静态方法 - 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 |
afterReturning
和 afterThrowing
执行之后还会调用 after
afterCallReturning
和 afterCallThrowing
执行之后还会调用 afterCall
after
和afterCall
方法是 finally 里调用的;如下:
try {
adviceListener.afterCallReturning( ... );
} finally {
adviceListener.afterCall( ... );
}
一些afterxxx
中的公共功能可以放到after
或 afterCall
方法中。
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
;
五、最后说一句
我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。